🤖 feat: add token-budget context window rollovers - #4097
Conversation
--- _Generated with [`mux`](https://github.com/coder/mux) • Model: `coder:openai/gpt-6-astra` • Thinking: `high`_
Integration checkpoint; full validation follows the parallel recovery and budget components. --- _Generated with [`mux`](https://github.com/coder/mux) • Model: `coder:openai/gpt-6-astra` • Thinking: `high`_
…licy Add browser experiment snapshots, rollover labels, collapsible warnings, session history icon, full-app desktop/phone stories, and ADR/user documentation. Depends on shared DisplayedMessage rollover/warning metadata fields owned by the integration branch.
Add shared DisplayedMessage fields for rollover boundaries and machine warnings. Clarify append/cleanup ordering and caller epoch synchronization in ADR0005.
…sals --- _Generated with [`mux`](https://github.com/coder/mux) • Model: `coder:openai/gpt-6-astra` • Thinking: `high`_
Keep historical recovery experiment-gated but independent of implicit agent allowlists, with explicit tool disables honored. Bound disk scanning, authenticate append-stable cursors, and enforce manual-reset privacy floors.\n\n---\n_Generated with [`mux`](https://github.com/coder/mux) • Model: `coder:openai/gpt-6-astra` • Thinking: `high`_ Signed-off-by: Thomas Kosiewski <tk@coder.com>
--- _Generated with [`mux`](https://github.com/coder/mux) • Model: `coder:openai/gpt-6-astra` • Thinking: `high`_
Document the existing atomic temp-and-rename batch writer. Keep legacy/external partial prefixes as a recovery-test requirement rather than a current writer crash outcome. --- _Generated with `xum` • Model: `coder:openai/gpt-6-astra` • Thinking: `high`_ <!-- mux-attribution: model=coder:openai/gpt-6-astra thinking=high -->
Add pure budget decisions and media-aware request estimates, reserve existing workspace notes inside hot-memory caps, and gate every provider assembly with a structured over-budget result. Keep memory guidance permission-aware. Validation: 217 targeted tests, 27 memory-policy gate tests, changed-file ESLint and formatting pass. Typecheck awaits the parent-owned ModelFallbackOptions error union widening from string to string | ContextBudgetExceeded.
Expose the final node ContextBudgetExceededError.details contract and add a visible context_budget_blocked send result. Prevent automatic retries of local preflight refusals and terminal budget blocks. Validation: 126 targeted tests and changed-file ESLint/format checks pass. Typecheck still awaits the parent-owned ModelFallbackOptions error union.
Forward final post-policy memory write availability for each primary and fallback attempt so budget warnings never ask read-only agents to write notes. Validation: request-builder/system-assembler and existing memory/intuition gate tests pass. Parent-owned stream request type additions are integrated separately.
--- _Generated with [`mux`](https://github.com/coder/mux) • Model: `coder:openai/gpt-6-astra` • Thinking: `high`_
Add durable-history regressions for rollover admission, recovery, queue dispatch, warning attribution, bounded overflow retries, and cache invalidation. Behavioral execution awaits the sibling budget-helper module; targeted lint and formatting pass.\n\n---\n_Generated with [`mux`](https://github.com/coder/mux) • Model: `coder:openai/gpt-6-astra` • Thinking: `high`_ Signed-off-by: Thomas Kosiewski <tk@coder.com>
Exercise the shared force buffer without prematurely resetting the warning band, allocate real history sequences for stopped partials, and assert persisted continuation attribution at its actual schema fields. Validation: 169 tests pass across all three touched files; make typecheck, targeted ESLint, and Prettier pass.
Handle the desktop Expand sidebar control before navigating to Settings. Clarify the five-percentage-point rollover force buffer and hard-ceiling precedence without changing production UI labels. --- _Generated with `xum` • Model: `coder:openai/gpt-6-astra` • Thinking: `high`_ <!-- mux-attribution: model=coder:openai/gpt-6-astra thinking=high -->
Exclude current compaction requests from assembled token-budget preflight using the resolved compact agent, explicit send metadata, or final effective user row. Older compact commands never disable preflight for an ordinary current request. Validation: six red-first identity regressions, 136 request/AIService/assembler tests, ESLint and formatting pass. Standalone typecheck reports only the known parent-owned fallback error union and contextBudgetMemoryWritable additions. --- _Generated with [`mux`](https://github.com/coder/mux) • Model: `coder:openai/gpt-6-astra` • Thinking: `high` • Cost: `$36.44`_ <!-- mux-attribution: model=coder:openai/gpt-6-astra thinking=high costs=36.44 -->
Use snake-case history inputs, explicit scan completion and oversized-row markers, and a read-specific envelope budget so fitting default pages retain all 8000 characters. Keep existing output IDs and scan cursors.
Preserve pre-turn provenance under token budgets and avoid repeating a published rollover after an append acknowledgment error. Retain durable reset failure diagnostics and display rollover countdowns at desktop and phone widths. Regenerate tool docs for the corrected bounded history API. Validated with 1,120 regression tests, eight Storybook cases, and make static-check-full. Live evidence covers notes, warnings, automatic rollovers, and bounded prior-window recovery. --- _Generated with [`mux`](https://github.com/coder/mux) • Model: `coder:openai/gpt-6-astra` • Thinking: `high`_
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Automations to automatically generate PRs for you. |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
💡 Codex Security Review
Here are some automated security review suggestions for this pull request.
Reviewed commit: c90c03e611
ℹ️ About Codex security reviews in GitHub
This is an experimental Codex feature. Security reviews are triggered when:
- You comment "@codex security review"
- A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review
Once complete, Codex will leave suggestions, or a comment if no findings are found.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c90c03e611
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…atches Fail closed on unreadable reset candidates during initial history scans and cursor append validation. Rotate the last durable boundary only after an atomic batch publication, preserving non-fatal rotation failure semantics. Cover malformed syntax/message shape, list/search/read privacy, append-stable cursor invalidation, primed lazy rotation, active-only rewrites, request slices, sequence ordering, and post-publication rotation failure with real history.
Honor regex denies through the standard last-match policy evaluator and seed baseline history access before explicit policies. Resolve current agent policy before rollover so restoration is not blocked by a stale availability claim. Retain invoked skill snapshots on normal and emergency rollovers; emergency retries reuse accepted snapshots without rerunning dynamic commands. Defer restart warnings until settled memory permissions are known. Stabilize countdown numerals and document the intentionally fail-closed pre-append cleanup tradeoff. Validated red-green regressions, 1270 integrated tests, eight Storybook cases, make static-check-full, and a final targeted/static pass after the last edit. --- _Generated with [`mux`](https://github.com/coder/mux) • Model: `coder:openai/gpt-6-astra` • Thinking: `high` • Cost: $171.24_
|
@codex review Pushed Validation: 1,270 integrated regressions, additional final targeted tests (including emergency skill reuse), eight Storybook cases, full static checks and a final Generated with |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3ce1127e24
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3b0bf89c46
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Address PRRT_kwDOPxxmWM6f8XW4 and PRRT_kwDOPxxmWM6f8XW-. Preserve media-shaped ordinary tool JSON and literal data URLs while omitting validated chat file parts and canonical tool-output attachments using the shared media/display-file predicates. Keep nested tool arguments distinct from attachment outputs. Return success:false when read_item exhausts without finding its reference; intermediate scan pages remain successful and resumable. Validation: four red-first real-history regressions; 483 history/privacy tests; make typecheck; scoped ESLint, Prettier and git diff --check. --- _Generated with [`mux`](https://github.com/coder/mux) • Model: `coder:openai/gpt-6-astra` • Thinking: `high`_ _Generated with `xum` • Model: `coder:openai/gpt-6-astra` • Thinking: `high` • Cost: `$312.33`_ <!-- mux-attribution: model=coder:openai/gpt-6-astra thinking=high costs=312.33 -->
Charge JSON punctuation and escaping omitted from real-encoded text leaves with a conservative one-token-per-ASCII-byte bound. Preserve genuine media payload exclusions, repeated-reference accounting, cycle termination, and bounded tokenizer calls. Cover dense empty arrays/objects and escaped keys/values against actual encoding. Capture the canonical baseline of the accepted file snapshot before later reads can replace it. Restore only that copied snapshot's original bytes and timestamp, synchronously after a successful current-owner emergency rollover commit. Never reread snapshots or restore unrelated old-window files; discard copied tracking on rejection and clear it with context state. Cover edits before and after rollover, newer tracked content, unrelated files, failed append, canceled ownership, rejected retry, and deferred compaction. Leave history-service/session_history changes out of scope. Validation: 2,235 tests across 58 suites; both TypeScript projects; make static-check; make static-check-full; git diff --check. --- _Generated with [`mux`](https://github.com/coder/mux) • Model: `coder:openai/gpt-6-astra` • Thinking: `high`_ --- _Generated with `xum` • Model: `coder:openai/gpt-6-astra` • Thinking: `high` • Cost: `$711.43`_ <!-- mux-attribution: model=coder:openai/gpt-6-astra thinking=high costs=711.43 -->
Document the conservative allowance for omitted JSON structure/escaping and the in-process, post-commit restoration of only the copied snapshot's original file-tracking baseline. --- _Generated with [`mux`](https://github.com/coder/mux) • Model: `coder:openai/gpt-6-astra` • Thinking: `high`_ _Generated with `xum` • Model: `coder:openai/gpt-6-astra` • Thinking: `high` • Cost: `$1448.96`_ <!-- mux-attribution: model=coder:openai/gpt-6-astra thinking=high costs=1448.96 --> Signed-off-by: Thomas Kosiewski <tk@coder.com>
Merge pinned main 76f0ce3 onto the validated parent f0f2562 without rewriting either history. Resolve the sole AgentSession import conflict by retaining both budget imports and main's AsyncLocalStorage import. Preserve subscriber-local replay publication, observed engine ownership, relay buffering, and router handoff from main alongside prepared admission, ready-candidate cancellation, history privacy/display caps, structural JSON accounting, and copied-file snapshot baselines from the branch. Validation: 189 replay/router/store/coordinator tests plus 2,237 related lifecycle/budget/history/subscription tests (2,426 total across 65 files); both TypeScript projects; make static-check; make static-check-full; git diff --check. No GitHub mutations or pushes. --- _Generated with [`mux`](https://github.com/coder/mux) • Model: `coder:openai/gpt-6-astra` • Thinking: `high`_ --- _Generated with `xum` • Model: `coder:openai/gpt-6-astra` • Thinking: `high` • Cost: `$721.28`_ <!-- mux-attribution: model=coder:openai/gpt-6-astra thinking=high costs=721.28 -->
|
@codex review |
|
@codex security review |
JSON accounting, history recovery, and copied-file trackingPublished 74ed28b; replied to and resolved all four findings:
Also integrated the latest reconnect/replay ownership fix through a proper two-parent merge. The only textual conflict was the AgentSession import block; both sides were retained. Local validation: 3,734 tests across 106 suites, both static gates, and independent safety/merge reviews passed. The recording is from this exact merged revision: real Node/tokenizer and disk-backed history, plus five real-file emergency-tracking cases with a controlled provider. Playback is accelerated; this is integration evidence, not live-provider reasoning. Fresh CI and code/security reviews are pending. json-tracking-merged.webmGenerated with Generated with |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 74ed28b5fd
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Real same-size writes can retain identical nanosecond mtime/ctime values, as reproduced in minimal Bun and Node probes. The unknown-write fixture previously assumed that changing bytes guaranteed an observable stamp change. Seed an old mtime before cursor creation, then assert changed content, equal size and changed observed mtime before retaining the epoch-invalidation check. This is a fixture correction, not a production detection fix. Identical fixed stamps remain indistinguishable under the bounded O(1) provenance contract. CI did not record stamps, so its individual collision cannot be proven. Validation: 1,000 target repeats each on normal storage and tmpfs; 483 history and privacy tests; make typecheck; scoped ESLint, Prettier and diff checks. --- _Generated with [`mux`](https://github.com/coder/mux) • Model: `coder:openai/gpt-6-astra` • Thinking: `high`_ _Generated with `xum` • Model: `coder:openai/gpt-6-astra` • Thinking: `high` • Cost: `$332.91`_ <!-- mux-attribution: model=coder:openai/gpt-6-astra thinking=high costs=332.91 -->
Address PRRT_kwDOPxxmWM6f-zNO by checking manual-reset privacy floors before counting generic durable boundaries. Readable reset markers remain included, but skip/fallback cannot cross them into older active or archived history. Valid automatic rollovers and compactions remain skippable; malformed reset evidence retains its existing exclusion behavior. Validation: three red-first real-HistoryService regressions; all 28 provider privacy tests and 486 broader history tests; make typecheck; scoped ESLint, Prettier and diff checks. This production privacy fix is separate from the preceding fixture-only timestamp clarification. --- _Generated with [`mux`](https://github.com/coder/mux) • Model: `coder:openai/gpt-6-astra` • Thinking: `high`_ _Generated with `xum` • Model: `coder:openai/gpt-6-astra` • Thinking: `high` • Cost: `$344.67`_ <!-- mux-attribution: model=coder:openai/gpt-6-astra thinking=high costs=344.67 -->
Real Node and Bun probes show same-size writes can retain all observed file stamps within one filesystem tick. Clarify that bounded receipts detect observable stamp changes, not content identity; stronger detection requires write isolation or whole-prefix verification. This does not change production detection behavior. --- _Generated with [`mux`](https://github.com/coder/mux) • Model: `coder:openai/gpt-6-astra` • Thinking: `high`_ _Generated with `xum` • Model: `coder:openai/gpt-6-astra` • Thinking: `high` • Cost: `$1488.17`_ <!-- mux-attribution: model=coder:openai/gpt-6-astra thinking=high costs=1488.17 --> Signed-off-by: Thomas Kosiewski <tk@coder.com>
|
@codex review |
|
@codex security review |
Reset-floor privacy and provenance fixture clarificationPublished 3e093e6 and resolved PRRT_kwDOPxxmWM6f-zNO. Production privacy fixManual reset floors are evaluated before generic skippable boundaries. Older-window requests cannot cross a manual reset in chat or archive, including excessive skips and mixed rollover/compaction chains. Readable reset markers remain included; malformed evidence stays excluded; legal automatic boundaries remain skippable. CI diagnosis and fixture-only correctionMinimal real-filesystem probes reproduced changed same-size content with identical device/inode/size/mtimeNs/ctimeNs: 190/500 Bun and 18/500 Node cases on tmpfs, without mocks. The CI failure had no stamp telemetry, so its individual cause cannot be proven directly. The fixture now seeds an old mtime before cursor creation, performs the same real same-size rewrite, and checks changed bytes, equal size, and changed mtime before retaining the original epoch-invalidation assertion. 2,000 fixed repeats passed across normal storage and tmpfs. No production provenance logic, sleeps, added mocks, retries, or skips were introduced. The ADR now explicitly states that a bounded stamp-only receipt cannot detect a rewrite whose observed metadata is unchanged. This is a documented limitation, not a production detection fix; stronger detection requires write isolation or whole-prefix verification. Final local validation: 3,737 tests across 106 suites, both static gates, and independent safety review passed. The recording shows real-Node reset-skip checks, three history-floor cases, and 100 additional fixture repeats. Playback is accelerated. Production/test source is fe1f14d; the final commit adds the validated ADR clarification only. Fresh CI and code/security reviews are pending. reset-provenance-final.webmGenerated with Generated with |
|
Codex Review: Didn't find any major issues. Swish! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
This comment has been minimized.
This comment has been minimized.
Merge main 7c27339 (#4097) into the compaction coordination branch. Preserve raw reset privacy boundaries and append provenance, guard token-budget single/batch admission, retain prepared request snapshots, and keep committed rollover ownership separate from cancellation retirement. Signed-off-by: Thomas Kosiewski <tk@coder.com> --- _Generated with `xum` • Model: `unavailable` • Thinking: `unavailable` • Cost: `$unavailable`_ <!-- mux-attribution: model=unavailable thinking=unavailable costs=unavailable --> Change-Id: I321cbfd3af5c8466dea2834035c459e440b98827
…aming) into mike/bash-wake-turn-end
…only by bash-monitor wakes main (#4097) codifies that a late cancelSignal abort cannot revoke an accepted send; the wake dispatch opts into withdrawal so a Stop landing during acceptance or goal sync is still not followed by the wake's stream.



Summary
Add an opt-in Token-budget context windows experiment that replaces automatic LLM summarization with hard context-window rollovers. Earlier messages remain in the transcript and on disk; agents recover details on demand through a bounded
session_historytool and carry concise working notes through an optional extra memory entry while token-budget mode is active.Implementation
/memories/workspace/context-notes.mdas a ninth entry while token-budget mode is active. Ordinary hot-memory selection and budgets stay unchanged; the extra has a separate bounded allowance and is never duplicated. Effective mode and Memory/HotSet changes keep the cache coherent.list_windows, literalsearch, and pagedread_itemrecovery with authenticated cursors, aggregate byte/row caps, oversized-row handling, and manual-reset privacy floors. Private cross-process append receipts preserve cursors through tracked appends and expire them after observable untracked changes; fixed stamps cannot detect same-size rewrites with unchanged metadata. Automatic rewrites preserve raw reset evidence. Manual reset floors remain unskippable when requesting older context windows.session_historyunder ordinary inherited tool policy: built-in agents retain access through wildcard grants, while narrow custom agents opt in. Missing access blocks rollover before sealing existing context.Validation and dogfooding
make static-check-full; repeatedmake static-checkimmediately before push. Nix-only format checks were skipped because Nix is unavailable locally.read_itemcalls used snake_case inputs and recovered seven character pages that exactly reconstruct an 872-character historical item. Default 8,000-character reads are separately covered by automated tests.Live rollover recording
live-validated-rollovers.webm
Live history paging recording
read-item-paging.webm
375px phone verification
Latest recorded proof uses real Node/tokenizer and disk-backed history: textual data URLs reach the hard ceiling while typed media retains its allowance, and post-reset windows report reset kind without exposing old content. Six real StreamManager/history cases with a mocked provider verify inactive catalogs, fitting/oversized activation, search-off, thinking rebuilds, and fallback limits. This is integration evidence, not live-provider reasoning.
26 full-builder admission cases cover complete pinned system/schema admission before reset, deferred catalogs, both rollover paths, cancellation/revocation/disposal, failed append, actual prepared runtime use after sandbox discard, lazy fallback limits, cache/goal/sequence semantics, and both rollback outcomes. Recorded on the final production revision with controlled provider/model discovery.
Latest recorded proof compares punctuation-only JSON against real Node tokenization, reads/searches media-shaped ordinary JSON through disk-backed history, checks missing-item failure status, and exercises five real-file emergency-tracking scenarios. Structural accounting is a conservative allowance, not exact serialized-token measurement.
Real-Node checks and chat/archive/mixed-boundary regressions verify that skips cannot cross a manual reset. The provenance fixture now guarantees an observable timestamp change while retaining its real same-size rewrite and epoch-invalidation assertion; 2,000 repeats passed across normal storage and tmpfs. Separate Node/Bun probes demonstrated unchanged-metadata collisions, documented as a bounded-receipt limitation—not a production detection fix.
Risks and implementation notes
session_historytool blocks threshold rollover instead of silently losing access.📋 Implementation Plan
Token-budget context windows (Codex-style) for xum — synthesized plan
Goal
Opt-in token-budget context strategy replacing lossy LLM summarization for automatic context management with:
session_historytool — list prior windows, search them, page items back in, all bounded./memories/workspace/context-notes.md, optionally appended as a ninth<hot_memories>entry while token-budget mode is active.Non-goals (v1): changing
/compact, idle compaction, continuous/RLM compaction (they take precedence; rollover disabled when on); per-turn "N tokens left" injection; post-compaction diff/skill carryover across rollovers (D8); a new durable-event kind (the warning is a chat row, replayable by construction); a transcript migration or DB.Verified seams (explorer-confirmed, file:line)
docs/adr/0003…,src/common/constants/contextBoundary.ts,compactionBoundary.ts:151-170contextBoundaryKind:"reset") → exclusive provider slice; compaction → inclusive. Both found by byte needles (HistoryService.BOUNDARY_NEEDLESL794) and rotate sealed epochs tochat-archive.jsonl(rotateSealedHistoryUnlockedL1760).workspaceService.resetContextL12413-12620createMuxMessage(createContextResetBoundaryMessageId(),"assistant","",{contextBoundaryKind:RESET}),advanceContextMutationEpoch,clearUsageState,clearPostCompactionState, sandbox scope discard; rejects while a turn is active.agentSession.sendMessageL3786checkBeforeSend→ L3813shouldCompactBeforeSend→ L3844-3921 builds compaction request; user message is not persisted on that branch (L3896); otherwise persisted at L4051. Provider history loaded later instreamWithHistoryL5504 (getHistoryFromLatestBoundary).sendMessagecontinue.forward("usage-delta")L6369-6461 →checkMidStream→interruptForCompactionL5201 (stopStream({abortReason:"system"}),waitForIdle,sendMessage("Continue",…)).streamManager.createStopWhenConditionL2214-2264; SDK evaluates after all sibling tool results settle;StopConditionmay be async and seessteps[i].usage/toolResults. Existing predicaterequest.hasQueuedMessages?.("tool-end").completeToolCall→flushPartialWriteL2728 →writePartialL1244. Stream end →commitPartial(write-locked).context_exceededstreamManager.categorizeErrorL4920-5008 string/code match;agentSession.handleStreamErrorL6181-6228: normal turns fail terminally (non-retryable).messageQueue.addOnce(message, options, dedupeKey, internal)L482;dispatchModedefault"tool-end";agentSession.hasQueuedMessages(mode)L7200;sendQueuedMessagesL7508 →sendMessage. Heartbeat enqueue precedentworkspaceService.ts:15250-15272(muxMetadata,queueDispatchMode,internal:{synthetic, queueDedupeKey, skipAutoResumeReset, yieldToQueuedMessages}).autoCompactionCheck.ts:37-44,119,124:{shouldShowWarning, shouldForceCompact, usagePercentage, thresholdPercentage}; force = threshold+5%, warn = threshold−10%;getContextTokens = input+cached+cacheCreate.compactionMonitor.getThreshold() < 1gates auto.contextTokens/maxTokensto result.agentSession.ts:4944-4947patternoptions?.experiments?.x ?? aiService.isExperimentEnabled(EXPERIMENT_IDS.X);ExperimentsSection.tsx.TOKEN_BUDGET.message.ts:935-978:synthetic,uiVisible,compactedstrict union,contextBoundaryKind,muxMetadata; orpcmuxMetadata: z.any()(L174).muxMetadataonly.iterateFullHistory(ws, dir, visitor)L1278: 256 KiB chunks, early exit, no giant-line cap (carryover grows unbounded); in-process mutex only for reads;historySequencemonotonic across chat+archive (L2165), legacy rows may lack it.getHistoryBoundaryWindowfallback reads both files fully.src/common/constants/memory.ts: 8 items / 48 KiB / 12k tokens / 16 KiB per item;rankHotSetCandidatesL57 (pinned first; unpinned 0-access filtered L62);selectHotMemoriesL105; appended inturnContextAssembler.ts:819-821; gated bymemory+memory-hot-set.toolDefinitions.ts:2301-2309(ptcExcluded?: string),getAvailableToolsoptions L3577;ToolConfigurationhasworkspaceIdbut nohistoryService; built inturnRequestBuilder.buildToolsForModelL1967-2005;TOOL_REGISTRYgetToolComponent.ts:75;TOOL_NAME_TO_ICONToolPrimitives.tsx:243.modelMessageTransform.injectPostCompactionAttachmentsanchors on compaction boundaries only.docs/adr/0003…,0004…exist → new one is0005.Decisions
D1 — Rollover = reset boundary + separate synthetic user lead-in. Boundary row:
role:"assistant",contextBoundaryKind:"reset",muxMetadata:{type:"context-window-rollover", rolloverId, reason:"on-send"|"mid-stream"|"context-exceeded", previousWindowId, flushOpportunity:boolean, contextTokens, maxTokens}. Immediately after:role:"user",synthetic:true, uiVisible:false, muxMetadata:{type:"context-window-lead-in", rolloverId}with deterministic guidance (notes file if present is preloaded;session_historyif available; prior window id; for mid-stream: "your previous turn was interrupted by a context rollover; continue the task"). Slicing stays ADR-0003 (exclusive at reset). Boundary, lead-in and the continuation (user/Continue) row are written in oneappendManyToHistorycall (D3). Not a compaction-shaped row (compactedunion is strict → downgrade risk; ADR-0003 forbids fake summaries).D2 — Gate:
EXPERIMENT_IDS.TOKEN_BUDGET = "tokenBudget"(global toggle inExperimentsSection). Read via the L4944 pattern. Precedence:continuousCompactionor RLM on → rollover disabled (log.debug), summarization as today.getThreshold() >= 1(auto disabled) → no proactive warning or threshold rollover, but the D4.4 hard-ceiling preflight/block still applies while the experiment is on.session_historyavailability (author-approved revision): with the experiment on,session_historyis registered as a read-only, workspace-scoped tool and follows ordinary inherited agent/caller tool policy. Access requires an explicit tool name or matching wildcard grant; enabling the experiment never widens a narrow allowlist. Built-in Exec and Plan grant it through.*, and Explore inherits that grant. Omission or a later matching deny blocks rollover before existing context is sealed, returning"context_budget_blocked"with guidance (enablesession_history,/compact,/clear --soft). A fitting request in an empty/internal-only window remains allowed. Rejected: a special baseline grant, silently summarizing, or rolling over without retrieval.D2.1 — Request-middleware admission. Before rollover cleanup or publication, reconcile lazy workspace plugin hooks without constructing models/tools and capture an immutable ordered snapshot of applicable request-assembly registrations. Generic tool-mutating middleware is uncertified and blocks rollover before existing context is sealed; it is never overridden by restoring a denied tool. Explicit workspace scopes are enforced in dispatch. Context-only adapters receive no tool references and write back system text alone, so sandboxed context hooks remain supported. Run the admitted snapshot through primary/fallback requests, thinking context, emergency and in-process automatic retries; registration changes affect subsequent admissions. The snapshot is never serialized. Plugin revocation/epoch checks remain live and dropped mounts are reacquired. Ordinary non-rollover requests retain live middleware behavior. Validate blocked/benign uncertified chains, workspace isolation, lazy first-rollover context, registration races, fallback/retry continuity, revocation, and context-only projection.
D3 — One rollover path; the settled-step decision stops the stream directly and only requests the rollover.
stopWhen.StreamRequestConfiggetsonStepSettled?: (step: {usage, outputTokens, toolResultChars, imageParts}) => Promise<"continue"|"warn"|"rollover">;createStopWhenConditionawaits it and returnstrueitself for"warn"/"rollover"— independent ofhasQueuedMessages("tool-end"), so a queue holding only aturn-endentry can never let the stream run past the ceiling.AgentSessionimplements it using the model actually streaming (from the stream context, not the primary model): normalize usage exactly asupdateUsageStateFromModelUsage, computeprojected = contextTokens + outputTokens + ceil(toolResultChars / 4) + IMAGE_TOKEN_ESTIMATE × imageParts, evaluateevaluateStepBudget(Phase 2) againstcheckAutoCompactionthresholds and the hard ceiling (modelContextLimit − OUTPUT_RESERVE_TOKENS):"block"when the measured hard projection reaches the ceiling with automatic handling disabled: stop at the settled boundary without warning, rollover, Continue, retry metadata, or quarantine of already-executed input; preserve all sibling tool results."rollover"(automatic handling enabled andprojected ≥ forceThreshold || projected ≥ hardCeiling) → latchthis.pendingRollover = {rolloverId, reason:"mid-stream", flushOpportunity: projected < hardCeiling}; ifmessageQueue.isEmpty(),addOnce("Continue", {...internal-resume options as built at L5231-5249, queueDispatchMode:"tool-end"}, CONTEXT_CONTINUE_DEDUPE_KEY, {synthetic:true, skipAutoResumeReset:true, …}); otherwise the already-queued real input is dispatched first and receives the rollover (it would have needed it anyway). Stream ends at the step boundary;commitPartialcommits the assistant row with all tool results paired;sendQueuedMessagesdispatches."warn"only whenshouldShowWarning && !warningEmittedInWindow && projected + WARNING_RESERVE_TOKENS < hardCeiling(the warning turn must itself fit; otherwise the evaluator returns"rollover"withflushOpportunity:false). IfmessageQueue.isEmpty(),addOnce(warningText, …, CONTEXT_WARNING_DEDUPE_KEY, …)withmuxMetadata:{type:"context-budget-warning", contextTokens, maxTokens}; if not empty, the warning is emitted on-send as a prefix row (D6) ahead of the queued input. Latch the in-memory claim for this window.usage-deltakeeps updating usage state but no longer makes decisions.interruptForCompactionis untouched (still used when the experiment is off).sendMessage(on-send), as prefix rows of the same append. Branch before L3844:if (tokenBudgetActive && rolloverEligible (D4.1) && (this.pendingRollover || evaluateStepBudget(projectedOnSend (D4.3)) === "rollover"))→ preflight (D4.2) → buildprefixRows = [boundary, leadIn](plus the on-send warning row when applicable) → continue the samesendMessage; at the existing user-message persist point (L4051) first runapplyContextResetSideEffects()(below; idempotent, benign if the append then fails), then callappendManyToHistory([...prefixRows, userMessage])so boundary, lead-in and the user's (or Continue) message land in one write — no lost user input. If either step throws, the send fails visibly before any provider build. After the append: clear the latch, emitchat-events; history is loaded from the new boundary at L5504. Skip turn snapshots for the boundary rows as the compaction branch already does.applyContextResetSideEffects(reason): extracted fromresetContextL12535-12614 and shared with it:advanceContextMutationEpoch,clearUsageState(),clearPostCompactionState(),clearPendingBranchSummary, discard context-scoped PTC/sandbox scope and stale refinement/retry state. Preserved: session history, costs/lifetime usage, MemoryService data, task handles and intentional background jobs, queued real inputs, goal acknowledgment state (rollover does not callrequireUserAcknowledgment; that is user-clear semantics). Asserts: no active stream,turnPhaseadmits, boundary is a reset marker, threehistorySequences strictly increasing.Why not
stopStream+interruptForCompaction(Fable) or a separate journal file (Astra)?stopWhenfinishes the step: tool calls and results are committed together by the normal stream-end →commitPartialpath. An abort mid-step can leave the pairing to recovery. Dispatch reuses the heartbeat mechanism (queue), so ordering with real user input is already solved.pendingRollover, queued Continue) is derivable: after a restart the completed old turn is on disk and the nextsendMessagere-evaluates usage seeded from history (seedUsageStateFromHistory) and rolls over then. A journal would add a second source of truth to reconcile without adding a state it could protect.rolloverIdis stamped on all three rows for auditing/tests.D4 — Loop guard + fresh-request preflight (no chain of empty windows).
Already-fresh guard. A rollover is eligible only if the active window contains ≥1 provider-eligible row that is not token-budget internal (
muxMetadata.type ∉ {context-window-lead-in, context-budget-warning}, notcompaction-request, notrlmPreservedTailCopy). An internal-only window is treated as already fresh: no second boundary, the message is sent normally if it fits (D4.2),log.warnonce. This is also the recovery rule for an incomplete rollover batch (D5).Emergency rollover eligibility excludes all continuation-owned preludes, including synthetic assistant/family rows, so a crash-resumed already-fresh batch cannot create another rollover. Real older context still permits one recovery transition.
Fresh-request preflight (cheap, pre-history). Against the resolved model for this send (
modelForStream, after fallback-route resolution at L3786):await estimateFreshRequestTokensForModel(...) ≥ hardCeiling→ no rollover, no provider call;sendMessagereturns a visible error Result"context_budget_blocked"("This message plus the system context does not fit in a fresh context window for ; shorten it, remove attachments, or use a larger model") — same surface as existing pre-send validation errors. Without measured system/schema overhead, use the existing model-scaledSYSTEM_FLOOR_TOKENS_ESTIMATEfallback. Historical request input includes old user/history content and must not be reused as fixed overhead. The final assembled preflight remains authoritative. Fresh and assembled hard text guards use real resolved encodings with a per-call approximation bypass. Oversized strings use codepoint-safe chunks and boundary slack; encoding failures fail closed. Provider-family encodings and media/framing allowances remain estimates, with provider overflow handling as a backstop.Recheck the complete already-materialized file/skill/MCP/family prelude batch before reset cleanup or publication. Expand dynamic inputs once per send, count their actual payload rather than invocation text, preserve snapshot/file-tracking eligibility on rejected retries, and revalidate cancellation/admission after asynchronous counting. Over-budget batches retain only safe rejection capsules and apply manual goal safety without sealing existing context. Emergency recovery applies the same admission to the complete copied/deduplicated retry prelude against the actual failing model and captured provider configuration before cleanup or reset publication.
On-send projection includes the unsent tail.
projectedOnSend = seededContextTokens + outputTokens(lastAssistant) + estimate(tool results of the last assistant row) + estimate(user message + attachments); the last step's provider usage never counts its own trailing tool results, so this is what makes the post-restart case (D5) and the mid-stream latch produce the same decision from history alone.Per-attempt hard preflight after final assembly (Phase 2/3). In
turnRequestBuilder.build, after system prompt, tools, memory and messages are assembled for the attempt's resolved model, runcheckAssembledRequestBudgetForModelusing the attempt’s resolved model/capability encoding, sanitized wire text/tool schemas, and media/framing allowances and compare with that model'shardCeiling. Over → typed build outcome{kind:"context_budget_exceeded", model, estimate, hardCeiling}returned before any network call, handled inagentSessionahead of generic failure/retry: iftokenBudgetActiveand the window is rollover-eligible (D4.1) → emergency rollover (flushOpportunity:false, continuation = same user message) and rebuild once; otherwise →"context_budget_blocked"visible result. Runs for the initial attempt and every fallback attempt. Phase 3b (providercontext_exceeded) remains the backstop for estimator misses, not the primary mechanism. Omitted JSON punctuation and escape expansion receive a conservative one-token-per-byte allowance; real leaf encoding and media exclusions remain intact. Ordinary strings and JSON count as text even when they resemble data URLs or media objects; only genuine SDK/model media at explicit part boundaries and supported sanitized tool wrappers receive media allowances. With Tool Search, count only the actual advertised schema subset while retaining all tools for execution. Recheck each provider step after thinking/media transforms using the pinned attempt limit and actual model, including newly activated schemas. Per-step violations use terminalContextBudgetBlockedError(including fallback step zero), preserving completed tool results without an emergency-rollover/catalog loop; builder preflight remains the recoverable seam. Before any on-send or emergency rollover clears context state or appends a boundary, use that same builder seam to prepare the complete pinned future request (system/middleware, fresh memory context, advertised schemas, exact candidate rows). Rejecting admission disposes preparation resources while preserving the old window and its context-scoped state. Persist the exact accepted rows and start the same one-shot prepared request; do not repeat tool/system/hook or prelude assembly, register an assistant placeholder/stream before acceptance, or preconstruct fallbacks. Promote the candidate memory cache only after successful append; late-bind durable sequence and operation/thinking callbacks at start. Preview only prospective goal-tool availability without goal writes, preserving queued-user consent. Limit the admission abort link to candidate preparation and discard a candidate already aborted before detachment. After preparation, explicit admission/rollback guards decide cancellation versus retained delivery, including failed-rollback acceptance; accepted-turn interruption/disposal remains live.If a window rolls over after a single assistant turn,
log.warn(limit too small for system prompt + hot set).Auto disabled (
getThreshold() >= 1) undertokenBudget: no proactive warning/rollover, but D4.4 still applies — an over-ceiling request is blocked visibly rather than knowingly sent. With the experiment off, behavior is unchanged.Rejected manual intervention still applies goal safety. When rejection retention reports an actionable manual message, apply the same goal pause/acknowledgment and continuation cleanup as pricing rejection. Synthetic/internal and blank input are not manual interventions. Validate with real goal/history services.
D5 — Crash-safe recovery contract (derived from history, no replayed side effects).
Intended persisted states:
A= old window complete, no rollover rows;B=[boundary, lead-in, continuation]appended in one call (samerolloverId). In-memory only:pendingRolloverlatch, queued Continue/warning. Ordering inside the rollover send: (1)applyContextResetSideEffects()before the append — every step is idempotent and benign if the append then fails (usage re-seeds from history, discarded PTC scope/post-compaction state is context-scoped and would be dropped by the boundary anyway); (2)appendManyToHistory(B); (3) clear latch, emit chat events; (4)streamWithHistory. If (1) or (2) throws,sendMessagefails visibly before any provider build — never stream with stale carryover/PTC state.Crash in
A(incl. afterstopWhenreturnedtrueandcommitPartialran, before the queued Continue was dispatched): the old assistant turn is complete on disk with tool pairs intact (commitPartialis write-locked and atomic; an interrupted stream follows existing partial recovery). The queue is not resurrected; the turn is paused visibly (assistant row complete, no divider yet). The next realsendMessagerecomputes D4.3 from history — including the trailing tool results that caused the stop — and rolls over then. No auto-resume of tasks after restart in v1 — a deliberate choice: nothing the user typed is lost and no side effect is replayed.Crash/partial write during
B(appendFileof several lines may persist a complete prefix, and tolerant parsing drops a truncated trailing line): the window on disk is[boundary]or[boundary, lead-in]→ D4.1 treats it as already fresh; the next user message is appended normally into that window, no second boundary. Recovery is the D4.1 rule itself; no marker needed because the only lost row is the continuation, whose send already failed visibly. Test this exact case (truncatechat.jsonlafter row 1 and after row 2).After
B: normal stopped-turn semantics. A second boundary requires a new non-internal row in the window (D4.1) and the latch is cleared under the samesendMessagethat appendedB, so duplicates are impossible by construction.Supersession: explicit
/clear(either kind),/compact, edit, delete, interrupt, heartbeat compaction, or fork clearspendingRolloverand the warning claim and drops the queued Continue via its dedupe key (hook whereclearUsageState()/advanceContextMutationEpochalready run).resetContextrejects while a turn is active; rollover only runs fromsendMessageunder the existing admission gates, so the two never interleave.Emergency (giant single tool result / model switch / provider
context_exceeded): same path withflushOpportunity:false; the giant result is committed to the old window before the boundary (never deleted, never re-executed); the lead-in names it as retrievable viasession_history.Tests fault-inject
spyOn(historyService,"appendManyToHistory").mockRejectedValueOnce, truncate the last line ofchat.jsonlafter a rollover, and simulate restart (newAgentSessionover the samecreateTestHistoryService()), asserting ≤1 boundary perrolloverId, no orphan tool call, no duplicate Continue, no lost user text in the success path.In-process emergency retries retain only the original canonical tracking baseline for the accepted file snapshot actually copied into the retry. Restore synchronously after a successful, still-current rollover commit; never reread the snapshot, use a newer tracked hash, or restore unrelated old-window files. Rejected/deferred snapshots do not establish a baseline; compaction clears it with normal tracking state.
Preserve trunk’s centralized
PreparationAttempt, scoped/generation-guarded retry, and awaitable cancellation/disposal contracts. Budget-failure callbacks settle through the preparation owner before terminal policy; asynchronous emergency handoffs recheck current-turn admission, and direct/resumed requests retain the admitted snapshot and rollover metadata.Append provenance is bounded fixed-stamp evidence, not content-identity proof. Same-size rewrites with unchanged observed identity/size/timestamps, including benign same-tick filesystem collisions, cannot be detected without stronger filesystem/write isolation or whole-prefix verification. The unknown-write epoch-invalidation fixture must make the stamp change observable while retaining real-write, same-size, changed-content, and epoch-invalidation assertions; do not claim a production fix for the unchanged-stamp limitation.
D6 — Warning: once per window; rollover wins over warning. Text: "Context window ~N% used (X of Y tokens). If you have state worth keeping, write/update
/memories/workspace/context-notes.mdnow (essential state first, ≤ 8 KiB), then continue the current task without commentary." Emitted either mid-stream (D3 queue) or on-send as a pre-turnrole:"user",synthetic:true, uiVisible:truerow before the user's message. Latch = history-derived (acontext-budget-warningrow exists in the active window) plus in-memory claim. Omit the notes sentence whenmemoryis off or read-only; say writes are unavailable and namesession_history.D7 — Additive context notes (author-approved revision). Preserve ordinary hot-memory ranking, eligibility, eight-item selection, and byte/token budgets unchanged. When effective token-budget mode is active, append an existing
/memories/workspace/context-notes.mdafter that normal selection as one optional extra entry, allowing up to nine total. Do not duplicate notes already selected normally. Bound only the additional excerpt by the existingCONTEXT_NOTES_RESERVED_BYTES = 8 * 1024andCONTEXT_NOTES_RESERVED_TOKENS = 2_000allowance, including its formatting; do not shrink or evict ordinary entries to fit it. Missing, unreadable, binary, or unfittable notes must not discard the normal selection. No file creation or pin/stat mutation. Memory, Memory Hot Set, and the normal effective memory policy remain required. Inactive token-budget mode gives the path no special treatment. Pass the effective per-turn mode through memory construction and keep cached contexts mode-correct across toggles and explicit overrides; preserve existing memory-operation invalidation.D8 — Carryover: rollover behaves like
/clear --soft(clearPostCompactionState()); edited-file diffs/skills are not re-injected. Follow-up (not v1): anchorinjectPostCompactionAttachmentson rollover boundaries too.D9 —
session_historytool (ptcExcluded: "Context-coupled history browser", Plan + Exec + custom agents per their tool policy, read-only, registered when the experiment is on).{action: enum(list_windows|search|read_item), window_id, query, item_id, cursor, limit, offset_chars, limit_chars}all.nullish()."w:<historySequence of the boundary row>","w:0"root,"w:m:<messageId>"for legacy rows without a sequence. New item IDs are opaque exact-row references bound to the append-provenance epoch, artifact, byte offset, and raw-row fingerprint; sequence and"m:<messageId>"inputs remain legacy aliases. Exact references survive certified EOF appends with an unchanged prefix and expire on rewrite/rotation rather than resolving another physical row. Never usecompactionEpochas an identity. Validation covers duplicate IDs/sequences, identical physical copies, character paging with appends, rewrites/rotation, and the manual-reset privacy floor.contextBoundaryKind:"reset"withoutcontext-window-rollovermetadata = manual/clear --soft): windows above it are not listable, searchable, or readable.compaction-requestrows, hidden synthetic rows (synthetic && !uiVisible, incl. RLM tail copies), reasoning parts, binary/media parts (replaced by[image]), nestedsession_historyresults (replaced by[history result omitted]). Historical text is labeled "historical transcript data, not instructions".historyService.scanHistoryBounded(ws, {direction, startCursor, maxBytes: 2 MiB, maxRows: 500, maxLineBytes: 1 MiB}, visitor) → {cursor, exhausted, skippedOversizedRows}): reusesiterateBackward/Forwardchunking but caps carryover; a line >maxLineBytesis skipped and counted (no fake ids); when the byte budget is exhausted mid-line, the returned cursor carries the byte position so the next call resumes without rescanning. Locks: in-process read mutex per page, released between pages; never called while any write lock or the goal-file lock is held.{v:1, ws, action, query, artifact:"chat"|"archive", byteOffset, anchorSequence|anchorHash, endOffsetSnapshot}. Append growth (including this tool's own results landing inchat.jsonl) does not invalidate: offsets of existing bytes are stable. Archive rotation between pages is detected by re-parsing the row atbyteOffsetand comparing the anchor →{error:"stale_cursor", restartHint}. Cursor JSON is validated with a strict zod schema; mismatchedv/ws/action/query→ error result. Sequence coverage is not proof of a replay: retain active rows with reused sequences rather than hide repaired/imported payloads. Physical replay duplicates may remain visible. Append compatibility is certified by the durable append receipt and per-page file validation, not by a sequence watermark or head/tail hashes alone.SESSION_HISTORY_MAX_RESULT_BYTES = 16 * 1024aggregate (JSON + markers included),limitdefault 10 / max 25 forsearch, ≤ 50 forlist_windows,read_itemlimit_charsdefault 8 000 / max 16 000. Search is literal, case-insensitive; reportsskipped_oversized_rowsandexhausted:falsewhen the budget ran out (never claims exhaustiveness). Character offsets remain UTF-16 units; page/search/shrink boundaries preserve surrogate pairs, manual mid-pair offsets round back, and a one-unit page may return a whole pair to guarantee progress.nextCharOffsetcomes from the actual adjusted end. Existing unpaired source units are replaced only in output without changing offset lengths or stored bytes.config.workspaceId(assert present); host-local files even for SSH workspaces.Phases (each gated by tests + dogfood before the next)
Phase 0 — ADR + constants/types (~70 LoC)
docs/adr/0005-token-budget-context-window-rollover.md: third boundary use; reset boundary created by rollover may be followed by a provider-visible synthetic lead-in (amends ADR-0003 consequence 2 for this case only); recovery contract (D5); privacy floor (D9).src/common/constants/experiments.tsTOKEN_BUDGET;src/common/constants/contextBudget.ts(notes path, reserved bytes/tokens, dedupe keys,OUTPUT_RESERVE_TOKENS,IMAGE_TOKEN_ESTIMATE,SYSTEM_FLOOR_TOKENS_ESTIMATE, tool caps, scan caps).src/common/types/message.ts:muxMetadatavariantscontext-window-rollover,context-window-lead-in,context-budget-warning(+ type guardsisTokenBudgetInternalMessage,isRolloverBoundary).Phase 1 — Bounded
session_historytool (~380 LoC)Ship the recovery path before any automatic reset (Astra ordering).
historyService.scanHistoryBounded+ cursor codec (src/node/services/historyCursor.ts).src/common/utils/messages/contextWindows.ts: pure bucketing/rendering (bucketWindows(rows),renderItemPreview, privacy-floor predicate, filters; reuseextractMessageText).src/node/services/tools/session_history.ts;toolDefinitions.tsschema +getAvailableTools({enableSessionHistory});tools.tshistoryService?: HistoryServiceonToolConfiguration; wire inturnRequestBuilder.buildToolsForModel;TOOL_NAME_TO_ICON.session_history = History;GenericToolCallfallback.session_history.test.ts,historyService.scanBounded.test.ts, real history on disk): windows across mixed legacy/reset/compaction/rollover boundaries; privacy floor; legacy rows without sequence addressable; search/limit/window filter;read_itempaging; 1 MiB+ row skipped with count; 2 MiB budget exhaustion → resumable cursor without rescanning (assert bytes read); cursor survives appends made by the tool's own result; archive rotation between pages →stale_cursor; aggregate ≤ 16 KiB (assert); hidden/media/nested-result omission; wrong-workspace cursor rejected.list_windows → search → read_item; page a long item; screenshot tool card at 375 px and desktop.Phase 2 — Additive notes + budget evaluator (~180 LoC)
memoryHotSet.selectHotMemories: unchanged ordinary selection followed by the optional bounded notes append (D7). Tests: eight competing pins and normal byte/token budgets stay intact; notes become a ninth entry only while effective token-budget mode is active; inactive mode matches ordinary behavior; zero-access notes, deduplication, malformed/unreadable files, independent excerpt limits, and mode-correct caches/overrides. Memory-off and policy-disabled preloading remain disabled; readonly memory does not authorize writes.src/common/utils/compaction/contextBudget.ts: pureevaluateStepBudget({contextTokens, outputTokens, toolResultChars, imageParts, modelContextLimit, threshold, warningEmitted}) → {decision:"continue"|"warn"|"rollover"|"block", flushOpportunity, projected, hardCeiling},estimateFreshRequestTokens,estimateAssembledRequestTokens(payload)(D4.4),estimateToolResultChars(step). ExtendAutoCompactionCheckResultwithcontextTokens/maxTokens.turnRequestBuilder.build: after assembly, runestimateAssembledRequestTokensfor the attempt's model and return the typedcontext_budget_exceededoutcome (no network) when over the hard ceiling (~40 LoC; wiring of the outcome intoagentSessionlands in Phase 3).turnContextAssembler<memory-tool-guidance>: one sentence about the notes file when the experiment is on (gate is tested, wording is not).flushOpportunity:false, unknown limit → "continue" +log.warn, never zero-as-unlimited);turnRequestBuilderreturns the typed outcome for an over-ceiling assembled payload and makes no provider call.Phase 3 — Rollover + warning + recovery (~430 LoC) — the switch-over gate
streamManager.ts:onStepSettledrequest field;createStopWhenConditionawaits it and returnstrueon"warn"/"rollover"before the existing queue check (~20 LoC).src/node/services/contextWindowRollover.ts:buildLeadInText,buildBudgetWarningText,hasRolloverEligibleMessages,estimateToolResultChars(step).agentSession.ts:onStepSettledimplementation (D3),pendingRolloverlatch + supersession clears,sendMessagebranch (D3/D4.1–4.3, prefix rows in the single append), handling of thecontext_budget_exceededbuild outcome (D4.4: emergency rollover once or blocked result),"context_budget_blocked"Result, on-send warning row (D6). ExtractapplyContextResetSideEffectsfromworkspaceService.resetContextand call it from both.agentTools.ts/toolAssembly.ts: registersession_historyunder the experiment and filter it through ordinary inherited tool policy (D2); blocked result when history access is omitted or disabled and existing context would be sealed.handleStreamErrorforcontext_exceededon a normal turn with the experiment on and no deltas streamed → perform rollover (reason:"context-exceeded",flushOpportunity:false, continuation = the same user message re-appended in the fresh window, original row left in place) and retrystreamWithHistoryonce; D4.1 prevents loops. Must run before the string-matched legacy compaction retry paths (maybeRetryCompactionOnContextExceededonly applies to compaction turns).CompactionBoundaryMessage.tsxlabel "Context window rollover";context-budget-warningrows viaCollapsibleMachineMessage(branch inMessageRenderer.tsx/displayedMessageBuilder.ts); verify at 375 px.contextWindowRollover.test.ts,agentSession.tokenBudget.test.tswithcreateTestHistoryService()+ mock AI router,streamManager.test.ts):sliceMessagesForProviderFromLatestContextBoundary).sendMessagerolls over; queue already holding a tool-end message → no Continue enqueued, rollover still happens on dispatch; hard-ceiling jump from a single giant tool result →flushOpportunity:false, no warning.session_historyomitted or disabled by effective policy → blocked before sealing old context; explicit/wildcard grants → tool included; built-in Exec/Plan/Explore retain access through inherited policy.context_exceededon a normal turn → one rollover + retry; secondcontext_exceededin the fresh window → terminal failure (no loop).stopWhenreturnstrueon"rollover"even when the queue holds only aturn-endentry; that entry is dispatched and receives the rollover.appendManyToHistoryrejects → no rows, latch preserved, next send retries; restart between boundary and continuation → paused, no second boundary, next user message sends in fresh window; manual/clear --softclears latch.replayRequestBuilderreconstructs the post-rollover request identically (lead-in/warning are ordinary rows).<hot_memories>contains them (checkdevtools.jsonl), no compaction request written, older messages still paged in UI; restart the sandbox between boundary and continuation and show paused-not-corrupt.Phase 4 — Settings + docs (~30 LoC + docs)
ExperimentsSection.tsxtoggle (description names the precedence andsession_historyrequirement).ContextUsageBar/Section: label the window as "rolls over at N%" instead of "compacts" when the experiment is on (text only, no new controls).docs.json); ADR index if present.Invariants & defensive checks
rolloverId(reset boundary then lead-in), strictly increasinghistorySequence; assert after write; at most one boundary perrolloverIdon disk (test).sendMessage(or the Phase 3b error handler, after the stream has terminated) with no active stream; never from a stream callback.stopWhenonly stops and enqueues.historySequence ≤ boundary.context-budget-warningper window; assert before append.session_history:assert(config.workspaceId); never returns rows older than the newest manual reset boundary; aggregate ≤ 16 KiB; scan ≤ 2 MiB / 500 rows / 1 MiB per line per call; never under a write lock.applyContextResetSideEffectsruns before the append and is idempotent; a failed append fails the send visibly before any provider build.log.warn), never treated as 0 or ∞.Compatibility
muxMetadata.typepreserved and rendered as generic hidden/synthetic rows; no new top-level fields,compactedunion untouched. Lead-in wording is conditional ("if asession_historytool is available…").Dogfooding (evidence: screenshots + short recordings via
attach_file; artifacts outside the source tree)KEEP_SANDBOX=1 make dev-server-sandbox DEV_SERVER_SANDBOX_ARGS="--clean-projects"as a background task; scratch project; enable API Debug Logs and experimentsmemory,memory-hot-set,tokenBudget; per-model threshold ~10%; cheap model.agent-browser(snapshot -i→fill/click→ re-snapshot); record ≤5-minute clips per checkpoint./compact,/clear --softprivacy floor (windows above it invisible to the tool), experiment off, 375 px and desktop layouts, keyboard access.bun test <touched suites>,make typecheck,make lint,make static-checksequentially after the last edit (re-run typecheck after any lint fix).Net LoC estimate (product code only)
Recommended: ≈ 1 090 LoC (range 950–1 300): P0 70 · P1 380 · P2 180 · P3 430 · P4 30.
Alternatives considered: compaction-shaped rollover row (−150 LoC, rejected: downgrade/ADR risk); separate journal file + generic
context-budgetdurable event + per-attempt estimator seam inaiService(+300–500 LoC, rejected: the single-append transition leaves no intermediate state to journal, warnings/lead-ins are ordinary replayable rows, and Phase 3b covers fallback-model overflow; see D3/D4/D5).Generated with
mux• Model:coder:openai/gpt-6-astra• Thinking:highGenerated with
xum• Model:coder:openai/gpt-6-astra• Thinking:high• Cost:$1488.17