Plan-approval's 'Approve and compact context' used to pass the rendered
plan-mode-compact-instructions prompt as the first positional argument
to handleCompactCommand -> session.compact(), which landed on the
session_before_compact extension hook as customInstructions. Extensions
treating that field as user focus (e.g. to bias a query-focused summary)
would then see plan-mode boilerplate instead of operator intent and
produce query-biased compactions.
Add CompactOptions.internalGuidance: a private summarizer-only channel.
session.compact() reads it into the fallback-model summarizer while the
session_before_compact hook payload still only carries the public
customInstructions arg (undefined for the plan-compact path). The
snapcompact-disable predicate and the /compact rejectsFocus guard cover
both fields so a directed summary is never silently downgraded.
Extend the interactive-mode handleCompactCommand facade + command
controller with a fourth internalGuidance parameter, and switch the
plan-approval callsite in interactive-mode.ts to route the plan prompt
through it.
Fixes#4359
SessionManager.#runFencedAtomicRewrite's finally now only clears #atomicRewriteFenceEpoch when it still matches the unwinding task's epoch. When flushSync supersedes an in-flight rewrite (bumping #diskEpoch and resetting #diskTail), a fresh atomic task scheduled at the new epoch can take ownership of the fence before the stale rewrite finally settles; the previous unconditional clear stranded the newer rewrite's bookkeeping so subsequent sync appends took the hot writer path and were then detached by the newer publish.
Regression: SequencedRewriteStorage pauses the first N writeTextAtomic calls on per-call gates. Test schedules a stale rewrite, forces flushSync to bump the epoch via a fenced append, schedules a newer rewrite that parks at pauses[1], releases the stale gate (stale unwinds and guard-rejects), then appends a custom entry — asserts writerOpens does not grow (fence preserved) and the fenced entry lands in the newer publish's body. Without the fix, writerOpens grows from 1 to 2.
Fixes#4338
- Added `#hasPendingAsyncWake` to detect running or pending background jobs owned by the agent.
- Deferred todo reminders and `session_stop` hook passes until all agent-owned background async jobs complete.
- Ensured scheduling pauses caused by async jobs do not trigger terminal session stops or premature todo nags.
Replaced the boolean #atomicRewriteActive flag with #atomicRewriteFenceEpoch: number | null. The fence branch in #appendToSessionFile now applies only while the pending atomic rewrite's epoch still matches #diskEpoch. Once flushSync -> #rewriteSynchronously bumps the epoch, the in-flight writeTextAtomic is guaranteed to abandon via its commitGuard, so subsequent sync appends can (and must) take the hot path against the freshly-published body instead of being stranded in memory when close() returns without another rewrite.
New regression: pauses writeTextAtomic mid-flight, appends a fenced custom entry, calls flushSync (which captures it into the durable body), then appends a message + custom entry after the epoch bump. Reads the current JSONL BEFORE releasing the paused atomic and asserts both post-flushSync entries are already on disk; then releases the atomic (commitGuard rejects) and closes the session and asserts nothing is lost.
Fixes#4338
- Added `getModel` to `AgentLoopConfig` to allow runtime model resolution.
- Updated `streamAssistantResponse` to resolve the model dynamically per provider call instead of using the stale configuration snapshot.
- Enabled mid-run model switches to take effect immediately for context promotion and retry fallbacks.
SessionManager.#persistTitleChangeEntry's catch fallback previously did a single-shot atomic rewrite: any prompt/tool appended while it awaited was fenced with #atomicRewriteDirty=true but never re-serialized. Extracted the fenced-rewrite do-while loop into #runFencedAtomicRewrite and used it from both #rewriteAtomically and #persistTitleChangeEntry, so fenced entries during either path are captured before the task resolves.
Added SessionStorage.drain(): for FileSessionStorage and MemorySessionStorage it is a no-op; IndexedSessionStorage already had one and now conforms to the interface. SessionManager.flush() and close() await it so a graceful shutdown does not exit while a fire-and-forget writeTextSync publish (queued by flushSync on an indexed backend) is still on the wire — reducing the residual publish-window race for Redis/SQL where the backend cannot be aborted mid-flight.
Regression covers the title fallback loop: TitleFallbackPausingStorage forces updateSessionTitle to throw, pauses the fallback's writeTextAtomic, appends a message and a custom entry during the pause, and asserts (a) both fenced entries land on the current JSONL, (b) the final title is applied, and (c) writeTextAtomicCalls >= 2 proving the loop iterated.
Fixes#4338
IndexedSessionStorage.writeTextAtomic no longer delegates directly to writeText, which yielded on #awaitPath between the guard check and the backend publish. The new impl consults the guard three times — up front, again after #awaitPath resolves, and finally inside the enqueued task immediately before #backend.writeFull — so a flushSync that bumps #diskEpoch while the atomic rewrite is suspended cannot land stale content on Redis/SQL backends. When the enqueue-time guard rejects, the optimistic index update is restored only when nothing has advanced it past our mtime, so a concurrent writer's state is preserved.
Added a PausableWriteFullBackend regression: the first writeTextAtomic parks inside backend.writeFull holding the per-path tail; the second queues with a guard that flips after the first is released. The backend records only the first content, confirming the guard is honored at publish time.
Fixes#4338
FileSessionStorage.#replaceSessionFileAfterEpermSync now unlinks the staged temp file when commitGuard returns false in both fallback branches: the ENOENT-vanished-target path and the post-move-aside path (where the moved-aside backup is also restored). Honors the writeTextAtomic contract that a guard-rejected stage is discarded.
Regressions cover all three guard-reject exits: the direct rename pre-check, the ENOENT branch inside the EPERM fallback, and the move-aside branch that also restores the backup. Each asserts no orphan .tmp remains in the session dir.
Fixes#4338
SessionManager.#rewriteAtomically now enables #atomicRewriteActive before #closeWriterHandle() and keeps it set until the rewrite task exits, so a sync append landing in the close-yield window is fenced and cannot open a fresh writer that the pending writeTextAtomic would then detach from the current JSONL path. Same pattern applied to the #persistTitleChangeEntry atomic fallback.
Added a regression that pauses the fake storage's writer.close() gate, appends a message and a custom entry during the pause, and asserts (1) no new writer opens (writerOpens counter unchanged) and (2) the fenced entries land on the current JSONL path after the rewrite completes.
Fixes#4338
SessionStorage.writeTextAtomic now accepts a commitGuard the backend calls synchronously immediately before publishing the staged body. FileSessionStorage performs the guard check and rename in the same tick via fs.renameSync (both on the direct path and the EPERM move-aside fallback), so a concurrent #rewriteSynchronously (flushSync -> Ctrl+C / session exit) that bumps the disk epoch cannot be overwritten by the stale body serialized before it ran. MemorySessionStorage and IndexedSessionStorage honor the same guard.
SessionManager.#rewriteAtomically threads a guard that returns false when the disk epoch changes, and re-checks the epoch after every writeTextAtomic before touching #fileIsCurrent / #rewriteRequired. #persistTitleChangeEntry's atomic fallback wires the same guard.
Added a regression that pauses the fake storage's writeTextAtomic mid-flight, appends a session_exit custom entry (which the fence records in memory), calls flushSync, releases the paused rewrite, and asserts the exit record is still on the JSONL path and the atomic publish was rejected by the guard.
Fixes#4338
Fenced synchronous session appends while an atomic full-file replacement is active so Windows EPERM fallback cannot detach the append writer from the current JSONL path.
Added a deterministic storage fake regression covering superseded compaction rewrites, title changes, session-exit diagnostics, resume, and post-rewrite tool/assistant tail persistence.
Fixes#4338
Compared the resolved advisor runtime inputs before rebuilding on explicit enable so unchanged /advisor on calls keep the live advisor agent and history.
Added regression coverage for the unchanged-config path preserving advisor context.
Fixes#4302
Rebuilt live advisor runtimes when the explicit enable path runs so changed modelRoles.advisor values are re-resolved without restarting the session.
Added a regression test covering a live advisor moving from one role model to another through setAdvisorEnabled(true).
Fixes#4302
The settle-time reminder queues a hard 'required' tool choice paired with a
scheduled continuation. If that continuation never runs (user prompt bumps
the generation, dispose, compaction/handoff) or plan mode is exited first,
the queued directive leaked onto the next unrelated turn as a forced tool
call. Remove it by label on continuation skip, on user-initiated prompts,
and when plan mode is disabled.
Removed the live-context guard that let default model selection persist a new role without changing the active session model. The next prompt's compaction path now owns oversized-context recovery after a switch.
Fixes#4219
- Replaced `grep`, `glob`, and `ast_grep` `paths` inputs with optional single `path` strings while preserving default workspace-root behavior.
- Added shared `toPathList` normalization for legacy arrays and JSON-encoded arrays across tool execution and TUI renderers.
- Updated prompts, fixtures, shims, transcript summaries, and tests to send and display the new `path` argument.
- Updated collab-web search tool cards to read `path` while falling back to legacy `paths` for historical transcripts.
- Recorded the contiguous coding-agent changelog run for the tool-path breaking change and adjacent TTS entries.
- Persist signed message blocks (`text`, `thinking`, `toolCall`) and encrypted reasoning payloads verbatim during session serialization instead of clearing or truncating them.
- Preserve signature keys instead of replacing them with empty strings when they exceed persistence size limits.
- Exempt official first-party OpenAI and Anthropic API endpoints from the leaked-thinking stream healing wrapper to prevent misfires on legitimate visible text fences.
- Excluded signed `thinking` blocks and `redactedThinking` blobs from size-based persistence truncation.
- Preserved signature-bound reasoning verbatim to prevent provider validation failures on session replay.
- Maintained normal truncation behavior for unsigned thinking and standard text blocks.
The branch-scan rehydrator only rebuilt `#lastCompletedRewind` and wiped
`#checkpointState` unconditionally at entry — so a branch whose latest
checkpoint had not yet been rewound came back with neither an active
checkpoint nor completed-rewind guidance. Reloading such a session (or
`switchSession()` on the same file) made the next `rewind` fail with
"No active checkpoint" even though the checkpoint entry was still the
branch leaf.
Extended the walker to also track the last unresolved checkpoint entry
and, when the branch ends without a rewind-report, seed `#checkpointState`
from that entry (id, `details.startedAt`) so `rewind` can complete
normally. Renamed the method to `#rehydrateCheckpointRewindState` to
reflect the widened responsibility and added a regression test that
truncates the branch to the checkpoint entry, resumes into a fresh
`AgentSession`, and calls `rewind` end-to-end.
Fixes#4187
Cleared checkpoint rewind runtime state when starting new sessions or creating branch sessions so stale completed-rewind guidance cannot leak into unrelated contexts.
Added regression coverage for /new and branch reset paths.
Fixes#4187
Reconstructed the completed rewind marker from the active branch so resumed sessions keep repeat-rewind recovery guidance.
Covered resume rehydration with the checkpoint rewind branch regression test.
Fixes#4187
Reconstructed the completed rewind marker from the active branch so resumed sessions keep repeat-rewind recovery guidance.
Covered resume rehydration with the checkpoint rewind branch regression test.
Fixes#4187
- Refactored the mid-run todo nudge to trigger on mutating tools (bash, eval, edit, write, ast_edit) rather than overall tool turns.
- Simplified the nudge prompt template to a concise, non-escalating reminder.
- Migrated nudge messages from "developer" role with public events to a hidden "custom" role that is excluded from the TUI and transcript.
- Introduced a separate per-cycle reminder cap of 2 to decouple mid-run hints from the user-visible stop-time escalation budget.
- Avoided triggering the todo nudge when read-only exploration tools (e.g. grep, read, glob, lsp) or errored results are returned.
Wrapped retained rewind reports with completion guidance so the post-rewind turn knows the checkpoint is closed.
Added repeat-rewind recovery errors and regression coverage for both the retained context and no-active-checkpoint path.
Fixes#4187
- Projected full tool-call arguments down to a compact summary containing only `command` and `path`.
- Truncated summarized argument fields to 200 characters to prevent inflating session log sizes.
- Replaced routine clean session disposal warnings with debug logs to reduce noise.
- Streamlined debug context in assistant message removal and agent continuation skip paths.
- Extracted duplicate user-facing compaction warning strings into a helper function.
- Thread the postmortem reason through the session teardown pipeline to the session dispose process.
- Prevent generic "dispose" logs from overwriting real triggers like SIGTERM, SIGHUP, or uncaught exceptions.
- Ensure the first teardown trigger's reason is preserved when concurrent disposal calls occur.
- Add comprehensive test coverage verifying signal-specific reason mapping inside exit diagnostics.
- Fix a minor unhandled-exception test utility expectation in input controller tests.
Added AnthropicOptions.fallbacks + wire types + response parsing gated on the opt-in — server-side fallback stays fully inert on every request that does not set the option.
Coding-agent surfaces the feature via providers.anthropic.serverSideFallback (default off). When enabled, Fable/Mythos requests inject fallbacks: [{ model: claude-opus-4-8 }]; caller-supplied fallbacks always win.
transformMessages centrally strips persisted fallback blocks on cross-provider hops and non-official Anthropic replays so a stored fallback turn never wedges downstream converters. Retry resets restore output.model to the requested id.
Fixes#4177
- Removed a duplicated branch.reverse() left by the PR #3862 merge in
SessionEntryIndex.pathTo(), which returned branches leaf-to-root and made
getLastModelChangeRole() read the oldest model change instead of the
newest — pinning the ctrl+p cycle to one slot and breaking session model
restore.
- Hardened getRoleModelCycle() to trust the recorded role only while its
resolved model still equals the active model, falling back to matching by
model after switches through alt+m, /model, or retry fallback.
- Added mutation-verified regression tests for branch ordering and the
stale-role fallback.
Cherry-pick of the reserve-budget clamp only (resolveBudgetReserveTokens + no-op compaction guard): applies compaction.ts + agent-session.ts + compaction/shake/progress-guard tests. Excludes unrelated Julia prelude timeout and ai/test churn from the PR head.
/quit and /exit hung for many seconds because AgentSession.dispose()
awaited MnemopiSessionState.dispose() unconditionally, and that path
runs consolidate() (state.ts:421) which fires a fresh LLM fact
extraction for the just-retained transcript and then awaits
flushExtractions() per owned bank. One LLM round-trip per shutdown,
no upper bound, no visible status.
- Add a timeoutMs option to MnemopiSessionState.dispose. When the cap
fires the in-flight consolidate is detached to the background and the
SQLite handles close once it settles, so writes never race a closed
handle.
- AgentSession.dispose passes SHUTDOWN_CONSOLIDATE_BUDGET_MS = 1_500 on
the user-visible shutdown path. Per-turn maybeRetainOnAgentEnd has
already retained earlier turns, so the worst case is losing episodic
promotion for the last few turns. State-replacement disposes
(mnemopiBackend.start) stay unbounded.
- InteractiveMode.shutdown surfaces a 'Closing session…' status before
dispose runs so the brief pause is explained rather than mysterious.
Two regression tests in memory-tools.test.ts cover (1) dispose returns
within the budget when flushExtractions stalls and the deferred close
still runs once consolidate settles, and (2) unbounded dispose still
runs the full #2320 consolidate-then-close pipeline.
Fixes#3641