- Carried startup-selected fallback role and primary selector into AgentSession.
- Continued remaining role fallback entries after the startup fallback fails.
- Added regression coverage for chained startup failover.
Fixes#6283
A non-retriable provider error on the continuation turn after a failed
tool result ended the run, but #persistSessionMessageIfMissing dropped
the empty error turn as reload poison, so the session JSONL stopped at
the last tool result and the provider errorMessage was lost with no
durable record of why the run stopped. When retry, model fallback, and
compaction all decline the turn, the non-retry terminal error tail now
persists it via the same helper the retry-lifecycle dead-ends use; the
empty turn stays off the wire on reload via the transform-messages
empty-assistant filter, matching the existing retry-exhaustion path.
Fixes#6249
The agent_end handler only recorded stopReason/provider/model at debug and dropped errorMessage/errorStatus/errorId, so a session dying repeatedly on provider stream failures left no actionable trace in the main log. Extract logProviderTurnError and emit one warn-level entry carrying provider, model, errorMessage, errorStatus, and errorId when a turn ends in stopReason:error.
Fixes#6177
Short-circuited session_stop emission when an abort or disposal is already in progress, avoiding extension work whose result cannot be used.
Added deterministic coverage for an abort racing the final settle pass.
Fixes#6134
A stream that stalls or aborts mid-tool-call ends the assistant turn with
stopReason error/aborted, then appends a synthetic tool_result per un-run
tool call to keep the provider's tool_use/tool_result pairing intact. That
placeholder trailed the failed turn, so AgentSession.retry() — which only
inspected the last message and required role assistant — short-circuited to
false and /retry printed 'Nothing to retry'.
retry() now walks back over trailing synthetic tool results (details
__synthetic true) before the assistant + stopReason check, stripping both
the placeholders and the failed turn. Only synthetic results are skipped, so
a turn whose tools actually ran stays non-retryable. Adds an exported
isSyntheticToolResultMessage guard in agent-loop.ts.
Fixes#6056
- Stopped asynchronous tool-result handling from overwriting newer todo state.
- Added an AgentSession regression for six exclusive completion calls.
Fixes#6148
Installed the normal plan proposal handler in print mode, persisted the resolved plan artifact, and silently stopped the completed planning turn for later review.
Shared plan proposal validation with interactive mode and added regression coverage.
Fixes#6017
Main renamed/refactored #tryShakeRescueForDeadEnd + #emitShakeRescueNotice into
the tiered #rescueCompactionDeadEnd (elide, then image drop, notices emitted
internally), so a textually-clean merge of this branch left calls to undefined
private methods. Re-express the !preparation rescue through the new API:
progress = prepareCompaction succeeding on the rewritten branch, skipElide when
falling through from a shake pass (the image tier still gets a chance), and
historyRewritten flagged whenever a tier freed content even without progress.
Restore the baseline dead-end remedy text (image drop is automated now, so the
manual /shake images suggestion is stale) and align the rescue-refit regression
test with main's continuation gating (auto-continue after compaction now
requires an active goal or queued work).
- Moved `#todoReminderAwaitingProgress` clearing into tool result handler for synchronous state update.
- Moved `#lastAssistantMessage` tracking to message_end to prevent stale reads when events land in same tick.
- Added `#subscriberEmitGate: Promise<void>` FIFO ticket to order concurrent `#emitSessionEvent` calls and prevent event reordering.
- Modified `#emitSessionEvent` to serialize subscriber fan-out: waits for previous gate before emitting, ensuring `message_start` arrives before `message_end` regardless of extension handler asymmetry.
- Added `#prunedTerminalRefusal` field to retain classifier-refusal turns for post-settle readers.
- Added `#prunedTerminalRefusal` field to store the pruned refusal for post-settle consumers.
- Modified `getLastAssistantMessage()` to return the pruned refusal before active-context lookup.
- Reset `#prunedTerminalRefusal` on `agent_start` so a fresh run supersedes the settled refusal.
- Updated test mock helpers to include `getLastAssistantMessage` for consistency.
Waited for the final assistant message_end persistence slot before reparenting past the capped empty turn.
Added a delayed extension hook regression proving the prompt cannot settle before persistence and the active branch remains clean.
Removed the final zero-content assistant after the empty-stop retry cap so its failed-request usage cannot re-anchor context maintenance.
Made the terminal error name model switching and /shake images as recovery options.
Fixes#5959
Long sessions re-walked the full live AgentMessage[] every turn: convertToLlm
re-converted the unchanged prefix and estimateTokens re-tokenized settled tool
results and assistants, redoing work only the newest suffix can change.
- Added a per-message estimate cache in agent-core keyed by identity, with a
settle gate (assistants cache only with real usage + terminal non-error
stopReason; streaming partials bypass) and dual option-split WeakMaps for the
default vs compaction-floor estimates.
- Memoized convertToLlm per message identity + assistant interruptedNext flag,
with an exact-repeat outer-array reuse and slice-on-growth for append-only
turns, guarded by a boundary-identity check against interior splice-replaces.
- Invalidated both caches at the mutation seams: prune, shake, strip-images, and
the prewalk plan-nudge scrub, via invalidateMessageCache /
registerMessageCacheInvalidator across the package boundary.
- Added the llm-assembly bench (N=5000, robust MAD-noise gate): steady/append
convert and repeat estimate are all >10x faster with noise under 20%.
Fixes#5934
Bounded aborted post-prompt drains and ran independent subsystem cleanup under one barrier while preserving writers-before-close ordering.
Kept long interactive shutdowns visible with a delayed status refresh.
Fixes#5932
resolveBlobRefsInEntries handed every non-session entry to the recursive
async resolvePersistedBlobRefs walk, allocating and awaiting child promises
even for plain-text entries with no blob:sha256: refs. On large text-heavy
histories this dominated the blob_resolve phase of session open.
Add a cheap synchronous containsBlobRef precheck that early-exits on the
first ref and allocates nothing. Interleave the precheck with per-entry
initiation so positive entries still start resolution at the same relative
point as the old filter+map schedule (a later entry that gains a ref during
an earlier BlobStore.get is still scanned after that mutation).
Blob-free N=5000 fixture: blob_resolve median 19.5ms -> 1.1ms, zero
BlobStore.get calls.
Fixes#5922
Review follow-up: a live subagent focused from the Agent Hub renders its
session name in the status line (session_name segment reads
sessionManager.getSessionName()), so the blanket agentKind === "sub" skip
made the user-enabled title.refreshOnReplan silently ineffective and left
focused subagents untitled after their first todo replan.
Focus only exists in an interactive host, and subagents run in-process, so
gate the skip on a process-global interactive-host flag: subagents skip the
replan title refresh only in non-interactive hosts (print/RPC/ACP/eval/SDK/
CI) where no session tree is focusable. The interactive entrypoint declares
the host via setInteractiveHost(isInteractive); the flag defaults false, so
bun test and headless embedders keep the optimization without leaking state.
Fixes#5910
Subagent sessions run `todo init` per the eager-todo prelude, which
triggered `#scheduleReplanTitleRefresh()` and a tiny-model title
generation call. The result is written to JSONL but never displayed —
subagents surface their registry id and generated task label, not a
session title.
Short-circuit `#scheduleReplanTitleRefresh()` when `#agentKind === "sub"`.
Uses the session-level subagent marker rather than `hasUI` so print/RPC
top-level sessions keep persisting their auto title for `--resume`.
Fixes#5910
- Fixed xd:// mount notices forcing their own model turn by deferring them until the next user prompt instead.
- Added `#pendingXdevMountDelta` field and `#takePendingXdevMountNotice()` to coalesce mount/unmount events and ride along with prompts.
- Mount and unmount events that cancel each other out before the next prompt are now dropped from the coalesced delta.
- Notices remain buffered during quiet startup mode (`startup.quiet`) and are delivered on the subsequent user prompt.
- Added per-invocation task schemas with strict and permissive validation.
- Shared task and eval agent policy, artifacts, isolation, and lifecycle handling.
- Enabled host-restricted plan-mode eval agents and persisted their capability clamp.
Fixes#5279
The #5800 drain guard suppressed the abort-finally stranded-message
drain while the session was disconnected from the agent event stream.
newSession/switchSession drop the agent queues on transition, so nothing
is lost there. compact() preserves the queues and only reconnected in
its finally — it never re-drained — so a steer/follow-up arriving during
compaction (async IRC, an xd:// mount notice, an SDK steer) stayed
stranded until the next explicit prompt.
Re-drain in compact()'s finally after #reconnectToAgent (and after the
compaction AbortController is cleared, so isCompacting is false and the
scheduled agent.continue() actually runs). Added a regression test that
queues a follow-up mid-compaction and asserts it resumes.
Fixes#5800
newSession() disconnects the agent listener and awaits abort() before
agent.reset(). abort()'s finally clears #abortInProgress and calls
#drainStrandedQueuedMessages(), which scheduled agent.continue() on the
still-old context — starting an unsolicited provider turn (e.g. from a
queued xdev-mount hidden steer) that raced the reset and appended its
late output to the fresh session.
Guard the drain to no-op while the session is disconnected from the
agent event stream (#unsubscribeAgent === undefined): a transition owns
the queue, and there is no listener to persist or render output. A plain
user-interrupt abort() stays connected, so its legitimate stranded drain
still runs.
Fixes#5800