- 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
Approved plan execution now requires reading the durable local plan file instead of embedding the plan body in synthetic execution prompts. This keeps execution recoverable when Headroom-compressed inline content expires.
Fixes#4164
Follow-up on the review: the widened parseThinkingSuffix recognized :auto unconditionally, so parseModelString and extractExplicitThinkingSelector would silently strip a literal provider/model:auto id before the isLiteralModelId guard the :max path already uses.
- Add allowAutoAlias option and require callers to opt in, mirroring allowMaxAlias.
- MAX_THINKING_SUFFIX_OPTIONS enables both aliases; parseModelPatternWithContext still tries exact match first, so a real :auto id wins there too.
- sdk.ts (session restore x2), agent-session.ts (retry fallback selector, context-promotion/compaction targets), and model-registry.ts (normalizeSuppressedSelector) now pass allowAutoAlias: true alongside allowMaxAlias.
- Regressions: literal example/runtime:auto wins over the sentinel in parseModelPattern, parseModelString (with and without isLiteralModelId), resolveModelFromString, and extractExplicitThinkingSelector; auto is still extracted when the id isn't literal.
The model selector's persistence path dropped the `:auto` selector when parsing role values, producing a warning ('Invalid thinking level "auto"') and rendering the badge as `inherit` instead of `auto`. Reload of the default role also lost the auto state whenever the role value carried an explicit `:auto` suffix instead of relying on `defaultThinkingLevel`.
Widen the resolver chain (`parseThinkingSuffix`, `splitThinkingSuffix`, `parseModelString`, `parseModelPattern*`, `ResolvedModelRoleValue`, `ResolvedRoleModel`, `ResolveCliModelResult`) to carry the `AUTO_THINKING` sentinel end to end, and coerce it back to `undefined` at concrete-only boundaries (glob scope patterns, retry fallback, advisor, commit pipeline, guided-goal, bench).
Regression tests cover:
- `resolveModelRoleValue("provider/model:auto")` returns explicit auto without a warning.
- `ModelSelector` renders `DEFAULT (auto)` and `SMOL (auto)` when the role value has `:auto`.
- `cycleRoleModels` activates auto thinking on entering a `:auto` role.
- Startup resume activates auto thinking when `modelRoles.default` carries `:auto`.
Fixes#4128
The postmortem SIGTERM/SIGHUP/uncaughtException handlers only ran the registered
cleanup callback list before process.exit, and the only session-related callback
was session-manager-flush. So a real kernel signal (terminal close, process
manager killing omp, IDE stop) skipped saveDraft, session.dispose (session_shutdown
emit, owned async job disposal, kernel disposal, MCP disconnect, browser tab
release), and violated the SessionShutdownEvent docstring contract that promises
delivery on SIGINT/SIGTERM. The LSP client also owned its own SIGINT/SIGTERM
handlers that called shutdownAll then process.exit(0), which could race postmortem's
async runCleanup and short-circuit the session teardown.
- Extracted a promise-memoized createSessionTeardown helper (modes/session-teardown.ts)
that snapshots the editor draft, persists it via sessionManager.saveDraft, then
invokes session.dispose. A saveDraft failure is logged but never aborts disposal.
- Memoized AgentSession.dispose so the keypress path and the signal path share one
settled promise and cannot double-emit session_shutdown or double-drain the owned
AsyncJobManager.
- Registered the teardown on postmortem as "session-teardown" in InteractiveMode.init,
replacing the narrower session-manager-flush callback. InteractiveMode.shutdown
now delegates the draft+dispose steps to the same helper.
- Replaced the LSP client's SIGINT/SIGTERM handlers with a "lsp-shutdown" postmortem
callback so LSP cleanup runs alongside every other session teardown instead of
racing them via process.exit(0). beforeExit is unchanged.
- Added session-teardown.test.ts covering: draft-then-dispose ordering, disposal
after saveDraft rejects, empty-string clears stale sidecar, promise memoization
under concurrent invocation, and snapshot-at-first-call semantics.
Fixes#4080
- Removed the canonical model variant indexing, selection, and tracking logic from the model registry and resolver.
- Eliminated the `canonical` sub-command, tab view, search tokens, and equivalence configuration structures from the CLI and model selector components.
- Refined model identification, lookup, and provider fallback resolution to bind exclusively to standard, raw model IDs.
- Relocated the equivalence utility script within the catalog package to support script-only policy generation.
Added a cross-turn tool-call loop guard that hashes canonical tool names and arguments, ignores intent metadata, and injects a hidden redirect when identical calls reach the configured threshold.
Fixes#3971
Propagated the native shell working directory in ShellRunResult so AgentSession can refresh cwd without running a hidden pwd command in the persistent shell.
Added regression coverage for cd plus a failing command followed by echo $?, proving cwd sync no longer overwrites the user's last shell status.
Fixes#3958
Two termination boundaries in the browser tool leaked browser-owned OS resources into the long-lived coding-agent process.
1. Aborted 'open' published an orphan. #open wrapped acquisition in untilAborted, which rejects its outer wrapper on abort but lets the inner launch resolve in the background; acquireBrowser then unconditionally stored the resolved handle in the module-global browsers map. releaseAllTabs walks tabs, not browsers, so the refCount:0 handle stayed alive to process exit.
2. Session dispose had no browser teardown. Browser/tab state lives in module-global maps, and AgentSession.dispose() had no hook to walk them, so headless/spawned Chromium the session opened survived it.
acquireBrowser now short-circuits before launch on a pre-aborted signal and disposes the handle when the launch completes after abort. TabSession records the creating session's id (opts.ownerSessionId, threaded through BrowserTool.#open), preserved across reuse so a subagent re-driving an existing tab does not yank teardown responsibility. AgentSession.dispose() invokes releaseTabsForOwner bounded by withTimeout(3s), mirroring the async-job/MCP disposal pattern.
Regression tests exercise both boundaries via spied CmuxSocketClient (no real puppeteer/socket) and cover: pre-aborted open short-circuit, aborted-mid-launch cleanup, releaseTabsForOwner reaping only owned tabs, and reuse preserving original ownership.
Fixes#3963
- Replaced leaf-to-root unshift path assembly with push plus one reverse in buildSessionContext and SessionEntryIndex.pathTo.
- Added regression coverage that keeps deep linear context and branch paths root-to-leaf without Array.unshift work.
Fixes#3961
Updated interactive bash execution to query the persistent shell PWD after commands and move the session cwd when it changes, keeping the status line and session-scoped settings aligned with shell navigation.
Added regression coverage for syncing persistent shell directory changes back to the owning session.
Fixes#3958
- Replaced the map-based persisted message index with a Set of key identities.
- Removed the message content equality check during branch verification to avoid false out-of-order skips when display-side content variants are present.
- Added a hidden system notice prompt to instruct the model to break repetitive behaviors when a thinking or response loop is detected.
- Injected the redirect notice into the retried turn's context when resetting the active context after a loop retry.
- Added unit tests to verify the custom redirect message is appended, configured as non-displaying, and visible in subsequent LLM contexts.
Captured the configured thinking selector when entering plan mode so approving a plan restores auto instead of the provisional concrete effort. Reloaded DEFAULT(auto) badges from defaultThinkingLevel and covered the plan-approval handoff plus /model display.
Fixes#3901
- Added a last-resort recovery step to run `shake("elide")` on oversized message tails when auto-compaction cannot otherwise free enough context.
- Re-tests the context headroom and auto-continue predicates after a successful rescue before falling back to pausing maintenance.
- Updated the dead-end warning message to suggest running `/shake images` for irreducible, image-only tails.
fix(compaction): cap snapcompact frame payloads (#3866)
Bound rebuilt snapcompact image payloads by a per-request base64 byte
budget so long sessions stop re-sending multi-megabyte standing image
archives on every provider request; auto-compaction falls back to
context-full summaries when snapcompact output is too large.
Resolved snapcompact.ts conflict against the main font-rendering refactor
by keeping both renderabilityProbeText and the frame-budget helpers.
Fixed historyBlocks to emit the omitted-frame notice before the kept
(newer) images, since the byte budget drops the oldest frames — keeping
reconstructed blocks oldest-to-newest (addresses Codex P2 review).
Fixes#3792
Forwarded persisted provider stream timeout settings into model requests so slow local LLM streams can widen or disable first-event and idle watchdogs without environment variables.
Fixes#3878
Apply the snapcompact frame byte-budget cap even when the active model has no known context window, avoiding 80-frame archives on custom vision models.
Bounded persisted snapcompact image archives by base64 byte size so large sessions stop re-sending multi-megabyte frame walls on every provider request.
Auto snapcompact now falls back to context-full summaries when rendered frame payloads exceed the byte budget, and legacy oversized archives omit over-budget frames during LLM context rebuilds.
Fixes#3792
Four non-overlapping algorithmic complexity reductions in hot paths.
(Streaming-reveal throughput is owned separately by #3843.)
1. session/session-manager.ts pathTo: O(n^2) branch.unshift() leaf->root
walk -> O(n) push + single reverse(). Hot path (5-10x/turn via getBranch).
2. edit/streaming.ts extractAddedLines: O(n^2) progressive string
concat per streaming tick -> array push + single join.
3. edit/modes/patch.ts: collapseConsecutiveSharedLines O(n*m) filter
+ includes -> Set (O(n+m)); collapseRepeatedBlocks O(n^3) with
per-iteration slice allocations + every() -> index arithmetic with
a single shared.has() guard. Semantics preserved.
Honorable: tui/src/utils.ts replaceTabs reallocated
" ".repeat(DEFAULT_TAB_WIDTH) every call -> hoisted TAB_SPACES const.
- Migrated global service tier settings to a per-model-family architecture (OpenAI, Anthropic, Google).
- Implemented `ServiceTierByFamily` mapping to allow independent configuration and resolution per provider.
- Added automatic migration logic for legacy service tier and fast-mode application settings.
- Updated telemetry, session management, and task execution to support provider-specific tier resolution.
Address review: the per-chunk Buffer.from(chunk).toString("utf8") string concat corrupted multibyte sequences (e.g. ✓, emoji) that straddled a chunk boundary — silent data loss in exactly the ≥8MiB long sessions that take this path. Keep the buffer as a Uint8Array and pass it directly to Bun.JSONL.parseChunk (it accepts typed arrays and parses UTF-8 natively), so no decoding happens until a complete record is parsed. Only the title-slot first line is decoded, and only after the full line (up to its '\n') is buffered, so it is a complete UTF-8 sequence.
Also: drop the defensive 'read || 0' (read is number per typings); add a multibyte multi-chunk parity test (>128KiB fixture) that would have caught the bug.