Skip to content

🤖 feat: add token-budget context window rollovers - #4097

Open
ThomasK33 wants to merge 51 commits into
mainfrom
plan-token-budget-combined
Open

🤖 feat: add token-budget context window rollovers#4097
ThomasK33 wants to merge 51 commits into
mainfrom
plan-token-budget-combined

Conversation

@ThomasK33

@ThomasK33 ThomasK33 commented Sep 5, 2026

Copy link
Copy Markdown
Member

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_history tool and carry concise working notes through a reserved memory slot.

Implementation

  • Evaluate budget after settled tool steps and on send, then atomically persist a reset boundary, hidden lead-in, and triggering input. Preserve queue attribution and prevent duplicate rollovers after interruptions or append acknowledgment failures.
  • Warn before the rollover threshold and reserve /memories/workspace/context-notes.md in the memory hot set without changing user pins. Successful memory mutations refresh the cached context.
  • Add bounded list_windows, literal search, and paged read_item recovery 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 untracked changes; automatic rewrites preserve raw reset evidence.
  • Preflight fully assembled requests, including fallback models, against the hard context ceiling; restore validated persisted usage after restart. Rejected inputs and owned snapshots are empty assistant capsules excluded even by preceding provider assembly, with originals retained solely for display/edit/export. Keep manual/idle compaction available; continuous compaction and effective RLM take precedence.
  • Add experiment settings, rollover dividers, warnings/countdowns, mobile stories, ADR-0005, and user/tool documentation.

Validation and dogfooding

  • 1,294 passing targeted tests, including lifecycle, stream settlement, request assembly, real-disk history recovery, policy, and memory regressions.
  • 10 passing Storybook cases and make static-check-full; repeated make static-check immediately before push. Nix-only format checks were skipped because Nix is unavailable locally.
  • Isolated live backend with a controlled Anthropic-compatible provider: warning → notes write → two automatic rollovers → prior-window list/search. This validates the real request/tool/lifecycle paths, not external model reasoning.
  • Live read_item calls 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.
  • Desktop 1900×1080 and phone 375×667 checked through the Storybook manager; keyboard warning expansion and expanded tool details fit without horizontal overflow. Rejected inputs remain visible but are not offered for retry; editing and a smaller draft remain available even when the persisted row is an empty capsule.
  • Recorded real-disk smoke under Node with two backend processes: cooperative append preserves the cursor; same-length interior rewrite plus untracked append expires it; ordinary stream completion, edit/fork, and percentage truncation preserve malformed reset privacy floors; legacy filtering excludes rejected inputs and owned payloads without relying on the new rejection flag.
Live rollover recording
live-validated-rollovers.webm
Live history paging recording
read-item-paging.webm
375px phone verification

Rollover warning, history tool, and context countdown at 375px

Risks and implementation notes

  • Rollover deliberately discards earlier messages from the active provider request, not from stored history. Recovery depends on bounded history access; an explicitly disabled session_history tool blocks threshold rollover instead of silently losing access.
  • Request-size estimation is conservative and is not a provider tokenizer. Unknown model limits cannot receive the same preflight guarantee.
  • The implementation follows existing provenance/admission rules: branch-summary registrations are cleared only after publication, and the initiating send reconciles its own context-mutation epoch. Warnings use a durable prefix row plus a correlated queued continuation.
  • The append receipt assumes cooperative writers honor the history lock and that the receipt is private. It detects untracked changes between transactions/pages, not hostile filesystem writes racing inside a certified append syscall/stat interval (documented in ADR-0005).
  • Older builds intentionally hide rejected capsule contents while retaining originals for upgrade. Truncation markers keep legacy decoded hashes alongside versioned byte hashes for crash recovery across versions.
  • This is opt-in; manual reset and compaction compatibility paths have dedicated regressions.

📋 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:

  1. Hard window rollover — near the limit, start a fresh provider context (reset boundary). Prior transcript stays on disk / UI / export.
  2. session_history tool — list prior windows, search them, page items back in, all bounded.
  3. Cross-window notes — conventional /memories/workspace/context-notes.md, reserved-slot pinned into <hot_memories>.
  4. Proactive budget warning — one durable, in-band message per window before rollover, telling the agent to flush state into notes.

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)

Seam Where Fact that shapes the design
Boundaries docs/adr/0003…, src/common/constants/contextBoundary.ts, compactionBoundary.ts:151-170 Reset boundary (contextBoundaryKind:"reset") → exclusive provider slice; compaction → inclusive. Both found by byte needles (HistoryService.BOUNDARY_NEEDLES L794) and rotate sealed epochs to chat-archive.jsonl (rotateSealedHistoryUnlocked L1760).
Reset writer workspaceService.resetContext L12413-12620 createMuxMessage(createContextResetBoundaryMessageId(),"assistant","",{contextBoundaryKind:RESET}), advanceContextMutationEpoch, clearUsageState, clearPostCompactionState, sandbox scope discard; rejects while a turn is active.
On-send trigger agentSession.sendMessage L3786 checkBeforeSend → L3813 shouldCompactBeforeSend → L3844-3921 builds compaction request; user message is not persisted on that branch (L3896); otherwise persisted at L4051. Provider history loaded later in streamWithHistory L5504 (getHistoryFromLatestBoundary). A pre-send rollover can append rows and then let the same sendMessage continue.
Mid-stream trigger forward("usage-delta") L6369-6461 → checkMidStreaminterruptForCompaction L5201 (stopStream({abortReason:"system"}), waitForIdle, sendMessage("Continue",…)). Listener timing based; we do not reuse it for the decision.
Step-end stop streamManager.createStopWhenCondition L2214-2264; SDK evaluates after all sibling tool results settle; StopCondition may be async and sees steps[i].usage / toolResults. Existing predicate request.hasQueuedMessages?.("tool-end"). Authoritative budget decision lives here.
Partial flush completeToolCallflushPartialWrite L2728 → writePartial L1244. Stream end → commitPartial (write-locked). Tool results are durable before stop condition runs.
context_exceeded streamManager.categorizeError L4920-5008 string/code match; agentSession.handleStreamError L6181-6228: normal turns fail terminally (non-retryable). Emergency rollover hook (Phase 3b).
Queue messageQueue.addOnce(message, options, dedupeKey, internal) L482; dispatchMode default "tool-end"; agentSession.hasQueuedMessages(mode) L7200; sendQueuedMessages L7508 → sendMessage. Heartbeat enqueue precedent workspaceService.ts:15250-15272 (muxMetadata, queueDispatchMode, internal:{synthetic, queueDedupeKey, skipAutoResumeReset, yieldToQueuedMessages}). Reused verbatim for warning / continue dispatch.
Thresholds autoCompactionCheck.ts:37-44,119,124: {shouldShowWarning, shouldForceCompact, usagePercentage, thresholdPercentage}; force = threshold+5%, warn = threshold−10%; getContextTokens = input+cached+cacheCreate. compactionMonitor.getThreshold() < 1 gates auto. Reuse; add contextTokens/maxTokens to result.
Experiments agentSession.ts:4944-4947 pattern options?.experiments?.x ?? aiService.isExperimentEnabled(EXPERIMENT_IDS.X); ExperimentsSection.tsx. Same pattern for TOKEN_BUDGET.
Message schema message.ts:935-978: synthetic, uiVisible, compacted strict union, contextBoundaryKind, muxMetadata; orpc muxMetadata: z.any() (L174). New discriminators go in muxMetadata only.
History reads iterateFullHistory(ws, dir, visitor) L1278: 256 KiB chunks, early exit, no giant-line cap (carryover grows unbounded); in-process mutex only for reads; historySequence monotonic across chat+archive (L2165), legacy rows may lack it. getHistoryBoundaryWindow fallback reads both files fully. Needs a bounded scanner variant.
Hot set src/common/constants/memory.ts: 8 items / 48 KiB / 12k tokens / 16 KiB per item; rankHotSetCandidates L57 (pinned first; unpinned 0-access filtered L62); selectHotMemories L105; appended in turnContextAssembler.ts:819-821; gated by memory + memory-hot-set. Reserved-slot pin for notes.
Tools toolDefinitions.ts:2301-2309 (ptcExcluded?: string), getAvailableTools options L3577; ToolConfiguration has workspaceId but no historyService; built in turnRequestBuilder.buildToolsForModel L1967-2005; TOOL_REGISTRY getToolComponent.ts:75; TOOL_NAME_TO_ICON ToolPrimitives.tsx:243.
Carryover modelMessageTransform.injectPostCompactionAttachments anchors on compaction boundaries only. D8.
ADRs docs/adr/0003…, 0004… exist → new one is 0005.

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_history if 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 one appendManyToHistory call (D3). Not a compaction-shaped row (compacted union is strict → downgrade risk; ADR-0003 forbids fake summaries).

D2 — Gate: EXPERIMENT_IDS.TOKEN_BUDGET = "tokenBudget" (global toggle in ExperimentsSection). Read via the L4944 pattern. Precedence: continuousCompaction or 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_history availability: with the experiment on, session_history is added to the effective toolset as a core read-only, workspace-scoped tool for every agent (built-in and custom, incl. Plan/Explore) unless the agent's tool policy explicitly disables it by name (verify how toolPolicy distinguishes explicit disabled from allowlist omission in agentTools.ts; if only allowlists exist, treat allowlist omission as implicit and still include it). If it is explicitly disabled, rollover is not allowed to make context unrecoverable: at threshold the turn is blocked with "context_budget_blocked" and guidance (enable session_history, /compact, /clear --soft). Rejected: silently summarizing (violates the experiment's contract) and rolling over without retrieval (model-visible context loss).

D3 — One rollover path; the settled-step decision stops the stream directly and only requests the rollover.

  • Authoritative decision in stopWhen. StreamRequestConfig gets onStepSettled?: (step: {usage, outputTokens, toolResultChars, imageParts}) => Promise<"continue"|"warn"|"rollover">; createStopWhenCondition awaits it and returns true itself for "warn"/"rollover" — independent of hasQueuedMessages("tool-end"), so a queue holding only a turn-end entry can never let the stream run past the ceiling. AgentSession implements it using the model actually streaming (from the stream context, not the primary model): normalize usage exactly as updateUsageStateFromModelUsage, compute projected = contextTokens + outputTokens + ceil(toolResultChars / 4) + IMAGE_TOKEN_ESTIMATE × imageParts, evaluate evaluateStepBudget (Phase 2) against checkAutoCompaction thresholds and the hard ceiling (modelContextLimit − OUTPUT_RESERVE_TOKENS):
    • "rollover" (projected ≥ forceThreshold || projected ≥ hardCeiling) → latch this.pendingRollover = {rolloverId, reason:"mid-stream", flushOpportunity: projected < hardCeiling}; if messageQueue.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; commitPartial commits the assistant row with all tool results paired; sendQueuedMessages dispatches.
    • "warn" only when shouldShowWarning && !warningEmittedInWindow && projected + WARNING_RESERVE_TOKENS < hardCeiling (the warning turn must itself fit; otherwise the evaluator returns "rollover" with flushOpportunity:false). If messageQueue.isEmpty(), addOnce(warningText, …, CONTEXT_WARNING_DEDUPE_KEY, …) with muxMetadata:{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-delta keeps updating usage state but no longer makes decisions. interruptForCompaction is untouched (still used when the experiment is off).
  • Rollover executes only in 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) → build prefixRows = [boundary, leadIn] (plus the on-send warning row when applicable) → continue the same sendMessage; at the existing user-message persist point (L4051) first run applyContextResetSideEffects() (below; idempotent, benign if the append then fails), then call appendManyToHistory([...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, emit chat-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 from resetContext L12535-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 call requireUserAcknowledgment; that is user-clear semantics). Asserts: no active stream, turnPhase admits, boundary is a reset marker, three historySequences strictly increasing.
Why not stopStream + interruptForCompaction (Fable) or a separate journal file (Astra)?
  • Graceful stop via stopWhen finishes the step: tool calls and results are committed together by the normal stream-end → commitPartial path. 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.
  • With prefix rows, every transition is one atomic history append with no external side effect: (a) nothing on disk, or (b) boundary + lead-in + continuation together. There is no intermediate persisted state to journal. The only in-memory state (pendingRollover, queued Continue) is derivable: after a restart the completed old turn is on disk and the next sendMessage re-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. rolloverId is stamped on all three rows for auditing/tests.

D4 — Loop guard + fresh-request preflight (no chain of empty windows).

  1. 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}, not compaction-request, not rlmPreservedTailCopy). An internal-only window is treated as already fresh: no second boundary, the message is sent normally if it fits (D4.2), log.warn once. This is also the recovery rule for an incomplete rollover batch (D5).
  2. Fresh-request preflight (cheap, pre-history). Against the resolved model for this send (modelForStream, after fallback-route resolution at L3786): estimateFreshRequestTokens({userText, attachments, leadIn, systemFloorTokens}) ≥ hardCeilingno rollover, no provider call; sendMessage returns 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. systemFloorTokens = inputTokens of the first step after the most recent boundary when known, else SYSTEM_FLOOR_TOKENS_ESTIMATE. Estimates are conservative (chars/3.5).
  3. 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.
  4. 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, compute estimateAssembledRequestTokens(payload) (chars/3.5 + IMAGE_TOKEN_ESTIMATE per media part + tool-schema bytes) and compare with that model's hardCeiling. Over → typed build outcome {kind:"context_budget_exceeded", model, estimate, hardCeiling} returned before any network call, handled in agentSession ahead of generic failure/retry: if tokenBudgetActive and 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 (provider context_exceeded) remains the backstop for estimator misses, not the primary mechanism.
  5. If a window rolls over after a single assistant turn, log.warn (limit too small for system prompt + hot set).
  6. Auto disabled (getThreshold() >= 1) under tokenBudget: 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.

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 (same rolloverId). In-memory only: pendingRollover latch, 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, sendMessage fails visibly before any provider build — never stream with stale carryover/PTC state.

  • Crash in A (incl. after stopWhen returned true and commitPartial ran, before the queued Continue was dispatched): the old assistant turn is complete on disk with tool pairs intact (commitPartial is 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 real sendMessage recomputes 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 (appendFile of 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 (truncate chat.jsonl after 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 same sendMessage that appended B, so duplicates are impossible by construction.
  • Supersession: explicit /clear (either kind), /compact, edit, delete, interrupt, heartbeat compaction, or fork clears pendingRollover and the warning claim and drops the queued Continue via its dedupe key (hook where clearUsageState()/advanceContextMutationEpoch already run). resetContext rejects while a turn is active; rollover only runs from sendMessage under the existing admission gates, so the two never interleave.
  • Emergency (giant single tool result / model switch / provider context_exceeded): same path with flushOpportunity: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 via session_history.
  • Tests fault-inject spyOn(historyService,"appendManyToHistory").mockRejectedValueOnce, truncate the last line of chat.jsonl after a rollover, and simulate restart (new AgentSession over the same createTestHistoryService()), asserting ≤1 boundary per rolloverId, no orphan tool call, no duplicate Continue, no lost user text in the success path.

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.md now (essential state first, ≤ 8 KiB), then continue the current task without commentary." Emitted either mid-stream (D3 queue) or on-send as a pre-turn role:"user", synthetic:true, uiVisible:true row before the user's message. Latch = history-derived (a context-budget-warning row exists in the active window) plus in-memory claim. Omit the notes sentence when memory is off or read-only; say writes are unavailable and name session_history.

D7 — Notes = conventional memory path with a reserved slot. CONTEXT_NOTES_MEMORY_PATH = "/memories/workspace/context-notes.md", CONTEXT_NOTES_RESERVED_BYTES = 8 * 1024, CONTEXT_NOTES_RESERVED_TOKENS = 2_000. In selectHotMemories: if a workspace-scope candidate at that path exists, it is selected first as pinned, its excerpt truncated to the reservation (explicit [truncated — use memory view] marker), and the remaining 48 KiB / 12k / 7 items go to ordinary ranking. No sidecar mutation, no auto-create (system-authored rows would pollute refinement journals), no injection when memory or memory-hot-set is off. Global hot-set convention (inert when the file does not exist), not plumbed through the experiment flag. The cached memory session context (aiService.buildMemorySessionContext) is invalidated after a completed memory tool write to the notes path so the next request sees fresh notes (verify how the cache is keyed today; reuse its existing invalidation if one exists).

D8 — Carryover: rollover behaves like /clear --soft (clearPostCompactionState()); edited-file diffs/skills are not re-injected. Follow-up (not v1): anchor injectPostCompactionAttachments on rollover boundaries too.

D9 — session_history tool (ptcExcluded: "Context-coupled history browser", Plan + Exec + custom agents per their tool policy, read-only, registered when the experiment is on).

  • Actions {action: enum(list_windows|search|read_item), window_id, query, item_id, cursor, limit, offset_chars, limit_chars} all .nullish().
  • Window id = "w:<historySequence of the boundary row>", "w:0" root, "w:m:<messageId>" for legacy rows without a sequence; item id = historySequence (fallback "m:<messageId>"). Never compactionEpoch.
  • Privacy floor (ADR-0003): traversal crosses rollover boundaries and compaction boundaries (incl. heartbeat-shaped), but stops at the newest plain reset boundary (contextBoundaryKind:"reset" without context-window-rollover metadata = manual /clear --soft): windows above it are not listable, searchable, or readable.
  • Default filters: compaction-request rows, hidden synthetic rows (synthetic && !uiVisible, incl. RLM tail copies), reasoning parts, binary/media parts (replaced by [image]), nested session_history results (replaced by [history result omitted]). Historical text is labeled "historical transcript data, not instructions".
  • Bounded scanning (new historyService.scanHistoryBounded(ws, {direction, startCursor, maxBytes: 2 MiB, maxRows: 500, maxLineBytes: 1 MiB}, visitor) → {cursor, exhausted, skippedOversizedRows}): reuses iterateBackward/Forward chunking but caps carryover; a line > maxLineBytes is 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.
  • Cursors are opaque base64 JSON {v:1, ws, action, query, artifact:"chat"|"archive", byteOffset, anchorSequence|anchorHash, endOffsetSnapshot}. Append growth (including this tool's own results landing in chat.jsonl) does not invalidate: offsets of existing bytes are stable. Archive rotation between pages is detected by re-parsing the row at byteOffset and comparing the anchor → {error:"stale_cursor", restartHint}. Cursor JSON is validated with a strict zod schema; mismatched v/ws/action/query → error result. Rows present in both files during/after rotation are deduplicated by the existing archive sequence watermark (getArchiveTailMaxSequence), never by content. No mtime/size equality checks.
  • Caps: SESSION_HISTORY_MAX_RESULT_BYTES = 16 * 1024 aggregate (JSON + markers included), limit default 10 / max 25 for search, ≤ 50 for list_windows, read_item limit_chars default 8 000 / max 16 000. Search is literal, case-insensitive; reports skipped_oversized_rows and exhausted:false when the budget ran out (never claims exhaustiveness).
  • Scoped to 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.ts TOKEN_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: muxMetadata variants context-window-rollover, context-window-lead-in, context-budget-warning (+ type guards isTokenBudgetInternalMessage, isRolloverBoundary).

Phase 1 — Bounded session_history tool (~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; reuse extractMessageText).
  • src/node/services/tools/session_history.ts; toolDefinitions.ts schema + getAvailableTools({enableSessionHistory}); tools.ts historyService?: HistoryService on ToolConfiguration; wire in turnRequestBuilder.buildToolsForModel; TOOL_NAME_TO_ICON.session_history = History; GenericToolCall fallback.
  • Tests (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_item paging; 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.
  • Dogfood gate: seed a long history, ask "what did the first test say about X?" → list_windows → search → read_item; page a long item; screenshot tool card at 375 px and desktop.

Phase 2 — Notes reserved slot + budget evaluator (~180 LoC)

  • memoryHotSet.selectHotMemories: reserved-slot logic (D7). Tests: 0-access notes file selected first; >8 competing pins; oversize notes truncated with marker while memory view still pages; remaining budget honored; other unpinned 0-access files still drop; memory off/read-only → not injected / not instructed.
  • src/common/utils/compaction/contextBudget.ts: pure evaluateStepBudget({contextTokens, outputTokens, toolResultChars, imageParts, modelContextLimit, threshold, warningEmitted}) → {decision:"continue"|"warn"|"rollover", flushOpportunity, projected, hardCeiling}, estimateFreshRequestTokens, estimateAssembledRequestTokens(payload) (D4.4), estimateToolResultChars(step). Extend AutoCompactionCheckResult with contextTokens/maxTokens.
  • turnRequestBuilder.build: after assembly, run estimateAssembledRequestTokens for the attempt's model and return the typed context_budget_exceeded outcome (no network) when over the hard ceiling (~40 LoC; wiring of the outcome into agentSession lands in Phase 3).
  • turnContextAssembler <memory-tool-guidance>: one sentence about the notes file when the experiment is on (gate is tested, wording is not).
  • Tests: evaluator boundaries (warn band, force band, hard ceiling wins, warning that would not fit → rollover with flushOpportunity:false, unknown limit → "continue" + log.warn, never zero-as-unlimited); turnRequestBuilder returns 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: onStepSettled request field; createStopWhenCondition awaits it and returns true on "warn"/"rollover" before the existing queue check (~20 LoC).
  • src/node/services/contextWindowRollover.ts: buildLeadInText, buildBudgetWarningText, hasRolloverEligibleMessages, estimateToolResultChars(step).
  • agentSession.ts: onStepSettled implementation (D3), pendingRollover latch + supersession clears, sendMessage branch (D3/D4.1–4.3, prefix rows in the single append), handling of the context_budget_exceeded build outcome (D4.4: emergency rollover once or blocked result), "context_budget_blocked" Result, on-send warning row (D6). Extract applyContextResetSideEffects from workspaceService.resetContext and call it from both.
  • agentTools.ts/toolAssembly.ts: include session_history as a core tool under the experiment unless explicitly disabled (D2); blocked result when explicitly disabled and at threshold.
  • Phase 3b (required — it is the fallback-model backstop, D4.3): in handleStreamError for context_exceeded on 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 retry streamWithHistory once; D4.1 prevents loops. Must run before the string-matched legacy compaction retry paths (maybeRetryCompactionOnContextExceeded only applies to compaction turns).
  • UI: CompactionBoundaryMessage.tsx label "Context window rollover"; context-budget-warning rows via CollapsibleMachineMessage (branch in MessageRenderer.tsx/displayedMessageBuilder.ts); verify at 375 px.
  • Tests (contextWindowRollover.test.ts, agentSession.tokenBudget.test.ts with createTestHistoryService() + mock AI router, streamManager.test.ts):
    • boundary+lead-in ordering/metadata/sequences; provider slice excludes boundary, includes lead-in; on-send persists user message after lead-in; payload after rollover contains no pre-boundary rows (via sliceMessagesForProviderFromLatestContextBoundary).
    • settled-step: a step whose tool results push projection past force threshold ends the stream at the step boundary with tool pairs intact, enqueues exactly one Continue, next sendMessage rolls 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.
    • warning once per window, not after rollover, suppressed when rollover wins; on-send and mid-stream variants; mid-stream skipped when a tool-end message is already queued.
    • D4: internal-only window → treated as fresh (no second boundary, message sent normally); oversized fresh request → blocked result, no provider call; over-ceiling assembled payload (D4.4) → emergency rollover once, then blocked on a second over-ceiling build; fallback attempt to a smaller model gets its own D4.4 evaluation; experiment off → unchanged path; continuous/RLM precedence; auto disabled → no warning/rollover but D4.4 still blocks; session_history explicitly disabled by policy → blocked at threshold; allowlist omission → tool included.
    • D4.3: after simulated restart, on-send projection counts the trailing tool results of the last assistant row and rolls over.
    • Phase 3b: provider context_exceeded on a normal turn → one rollover + retry; second context_exceeded in the fresh window → terminal failure (no loop).
    • stopWhen returns true on "rollover" even when the queue holds only a turn-end entry; that entry is dispatched and receives the rollover.
    • D5 fault injection: appendManyToHistory rejects → 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 --soft clears latch.
    • replay: replayRequestBuilder reconstructs the post-rollover request identically (lead-in/warning are ordinary rows).
  • Dogfood gate: two real rollovers (one during a multi-tool batch, one from an oversized tool output), warning → agent writes notes → after rollover <hot_memories> contains them (check devtools.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.tsx toggle (description names the precedence and session_history requirement).
  • ContextUsageBar/Section: label the window as "rolls over at N%" instead of "compacts" when the experiment is on (text only, no new controls).
  • User docs page (register in docs.json); ADR index if present.

Invariants & defensive checks

  • Exactly two rows per rolloverId (reset boundary then lead-in), strictly increasing historySequence; assert after write; at most one boundary per rolloverId on disk (test).
  • Rollover only from sendMessage (or the Phase 3b error handler, after the stream has terminated) with no active stream; never from a stream callback. stopWhen only stops and enqueues.
  • Boundary, lead-in and continuation are one append; a boundary is never persisted without its continuation in the success path.
  • Never delete or re-execute a tool result; rollover never splits a tool call from its result (graceful step stop only).
  • Provider payload after rollover has no row with historySequence ≤ boundary.
  • ≤1 context-budget-warning per 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.
  • Rollover never fires when experiment off, continuous/RLM on, or auto disabled; no provider call is made when D4 blocks; a request estimated over the hard ceiling for the attempt's model is never sent while the experiment is on.
  • applyContextResetSideEffects runs before the append and is idempotent; a failed append fails the send visibly before any provider build.
  • Unknown model context limit ⇒ no rollover decision (log.warn), never treated as 0 or ∞.

Compatibility

  • Downgrade: rows are a plain reset boundary + synthetic user rows; unknown muxMetadata.type preserved and rendered as generic hidden/synthetic rows; no new top-level fields, compacted union untouched. Lead-in wording is conditional ("if a session_history tool is available…").
  • Upgrade: no migration; experiment defaults off; existing summary/reset histories unchanged.

Dogfooding (evidence: screenshots + short recordings via attach_file; artifacts outside the source tree)

  1. KEEP_SANDBOX=1 make dev-server-sandbox DEV_SERVER_SANDBOX_ARGS="--clean-projects" as a background task; scratch project; enable API Debug Logs and experiments memory, memory-hot-set, tokenBudget; per-model threshold ~10%; cheap model.
  2. Drive with agent-browser (snapshot -ifill/click → re-snapshot); record ≤5-minute clips per checkpoint.
  3. Per-phase checkpoints as listed in Phases 1–3; Phase 4: legacy /compact, /clear --soft privacy floor (windows above it invisible to the tool), experiment off, 375 px and desktop layouts, keyboard access.
  4. Gates: bun test <touched suites>, make typecheck, make lint, make static-check sequentially 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-budget durable event + per-attempt estimator seam in aiService (+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: high • Cost: $811.95

---
_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`_
@mintlify

mintlify Bot commented Sep 5, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
Mux 🟢 Ready View Preview Sep 5, 2026, 2:03 PM

💡 Tip: Enable Automations to automatically generate PRs for you.

@chatgpt-codex-connector

This comment has been minimized.

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review


Generated with mux • Model: coder:openai/gpt-6-astra • Thinking: high

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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.

Comment thread src/common/utils/tools/toolPolicy.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/node/services/agentSession.ts
Comment thread src/node/services/agentSession.ts Outdated
Comment thread src/node/services/historyScanner.ts Outdated
Comment thread src/node/services/agentSession.ts Outdated
Comment thread src/node/services/agentSession.ts Outdated
Comment thread src/browser/components/CompactionWarning/CompactionWarning.tsx
Comment thread src/node/services/agentSession.ts Outdated
…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_
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

Pushed 3ce1127e246b28e29642405f7e6cddbe8b347395 and replied individually to all eight findings. Seven received behavioral/UI fixes; the cleanup-order finding retains the accepted fail-closed D5 ordering with an earlier cancellation/admission check and an explicit ADR explanation of the tradeoff.

Validation: 1,270 integrated regressions, additional final targeted tests (including emergency skill reuse), eight Storybook cases, full static checks and a final make static-check all passed. Current desktop/phone screenshots confirm tabular countdown numerals; the phone recording also checks keyboard expansion.

Updated 375px countdown verification

Tabular rollover countdown at 375px

review-phone.webm

Generated with mux • Model: coder:openai/gpt-6-astra • Thinking: high

@chatgpt-codex-connector

This comment has been minimized.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/common/utils/compaction/contextBudget.ts Outdated
Comment thread src/node/services/agentSession.ts
Comment thread src/node/services/contextWindowRollover.ts Outdated
Comment thread src/node/services/agentSession.ts Outdated
Comment thread src/node/services/agentSession.ts
Comment thread src/node/services/agentSession.ts
Comment thread src/browser/features/RightSidebar/ThresholdSlider.tsx
…ipts

Track cooperative history append continuity with an O(1) pending/stable
receipt containing a UUID epoch and exact bigint file stamps. Validate each
bounded scan under the history file lock and certify only low-level appends
or byte-preserving atomic batches; invalidate rewrites, recovery, and failures.

Preserve accepted write results when receipt or late publication finalization
fails. Cover cross-process writes/crashes, lifecycle mutations, corrupt receipts,
raw-byte batch preservation, and existing reset detector regressions.
…enance

Add full-app desktop/phone coverage for rejected-tail replay and document the approved bounded append-receipt trust and failure contract.

Validation: 10 Storybook interactions passed; focused ESLint, formatting, and typecheck passed.

---
_Generated with [`mux`](https://github.com/coder/mux) • Model: `coder:openai/gpt-6-astra` • Thinking: `high` • Cost: `$443.11`_
Retain both the goal-continuation type and the upstream queue assertion import. Preserve upstream sub-agent progress/continuation changes alongside token-budget rollover metadata.

Validation: merged queue and token-budget lifecycle suites; full typecheck.

---
_Generated with [`mux`](https://github.com/coder/mux) • Model: `coder:openai/gpt-6-astra` • Thinking: `high` • Cost: `$443.11`_
Open append receipts nonblocking so a FIFO cannot wedge scans and writes before the descriptor regular-file check. Reconcile invalid receipts through the normal locked path.

Validation: reproduced the blocked open with a bounded child process; all 29 receipt tests, ESLint, formatting, and typecheck pass.

---
_Generated with [`mux`](https://github.com/coder/mux) • Model: `coder:openai/gpt-6-astra` • Thinking: `high` • Cost: `$443.11`_
Keep malformed reset evidence byte-for-byte through automatic history rewrites and rotation, and refuse cleanup or updates that would erase hidden reset evidence.

Recognize Unicode-escaped colons across bounded scan chunks/pages and sanitize persisted request-prelude ownership before budget rejection.
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

All six outstanding findings are fixed, individually replied to, and resolved. Please review 96b9245ea443f78661b62de80606fdb3a7d2db6a, including the approved append-provenance extension documented in ADR-0005.

Validation: 1,222 targeted tests, 10 Storybook cases, make static-check-full, and the final make static-check passed (Nix-only formatting skipped because Nix is unavailable). Independent review also caught and fixed a special-file receipt hang.

The Node smoke uses real disk and two backend processes: tracked append stability, mixed rewrite/append rejection, fresh-query privacy, and malformed-reset preservation after ordinary assistant completion. The phone capture shows a rejected tail without Retry and a usable fresh draft; it is a full-app Storybook fixture, not an external model session.

Two-process Node history smoke

cross-process-smoke.webm

Rejected tail and fresh draft at 375px

rejected-tail-phone.webm

Generated with mux • Model: coder:openai/gpt-6-astra • Thinking: high • Cost: $474.29

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

Please review current head 96b9245ea443f78661b62de80606fdb3a7d2db6a; the six findings are addressed. ADR-0005 now states the cooperative-lock/private-receipt trust boundary for append provenance.


Generated with mux • Model: coder:openai/gpt-6-astra • Thinking: high • Cost: $474.29

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 96b9245ea4

ℹ️ 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".

Comment thread src/node/services/historyService.ts
@chatgpt-codex-connector

This comment has been minimized.

Preserve raw reset fragments in active and archived edit/fork cuts and partial percentage truncation without changing full-delete behavior. Hash two-file transaction contents as raw bytes so invalid UTF-8 cannot trigger an incorrect recovery rollback.
Keep an unterminated retained archive row separate from preserved active reset fragments when collapsing the two JSONL files.

Validation: reproduced target-row loss before the fix; 1,237 targeted tests, the expanded two-process Node smoke, and both static-check-full and static-check pass.

---
_Generated with [`mux`](https://github.com/coder/mux) • Model: `coder:openai/gpt-6-astra` • Thinking: `high` • Cost: `$474.29`_
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

Please review 12ea0257c0ae6edcf6e9e0d9ab3a1ceff6392625. The truncation finding is fixed, replied to, and resolved. 1,237 targeted tests, make static-check-full, and final make static-check pass.

Expanded real-disk Node smoke: ordinary update, edit/fork truncation, and percentage truncation all retain malformed reset privacy floors. The recording is accelerated; the assertions use real files and two backend processes.

Truncation privacy smoke

truncation-smoke.webm

Generated with mux • Model: coder:openai/gpt-6-astra • Thinking: high • Cost: $474.29

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

Please review updated head 12ea0257c0ae6edcf6e9e0d9ab3a1ceff6392625, including raw reset preservation during partial truncation.


Generated with mux • Model: coder:openai/gpt-6-astra • Thinking: high • Cost: $474.29

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. What shall we delve into next?

Reviewed commit: 12ea0257c0

ℹ️ 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".

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 12ea0257c0

ℹ️ 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".

Comment thread src/node/services/historyService.ts Outdated
Comment thread src/node/services/historyService.ts Outdated
Comment thread src/node/services/historyService.ts
Comment thread src/node/services/agentSession.ts Outdated
Comment thread src/node/services/turnContextAssembler.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Security Review

Here are some automated security review suggestions for this pull request.

Reviewed commit: 12ea0257c0

ℹ️ 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.

Comment thread src/node/services/agentSession.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Security Review

Here are some automated security review suggestions for this pull request.

Reviewed commit: 12ea0257c0

ℹ️ 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.

Comment thread src/node/services/agentSession.ts
When the restart usage seed is absent, sanitize persisted input/cache counters
before reusing cache-inclusive display accounting. Keep valid in-memory usage,
including zero, authoritative. Finalize emergency continuation ownership only
after deduplicated skill snapshots are copied so fresh retry rejection also
quarantines those payloads before an unrelated next request.

Validation: five red-first failures reproduced; all109 token-budget lifecycle
tests, full typecheck, targeted ESLint, formatting and diff checks pass.
Exclude unreadable parsed rows from typed history operations while retaining their bytes. Preserve legacy UTF-8 truncation digests and add a validated versioned raw digest extension for newer recovery. Terminate rewritten history files so later active and archive appends remain separate JSONL rows.
Reject malformed part arrays from typed history operations while preserving their raw rows. Reuse the existing part schema rather than letting null parts or non-string text reach replay eligibility checks.

Validation: both malformed-part regressions reproduced failure; all 377 history tests, typecheck, targeted ESLint, and formatting pass.

---
_Generated with [`mux`](https://github.com/coder/mux) • Model: `coder:openai/gpt-6-astra` • Thinking: `high` • Cost: `$474.29`_
Persist rejected inputs and owned preludes as empty completed assistant rows with original content held in one inert metadata field. Strip outer request controls, keep rejection idempotent, and stop retry lookup at either capsule or legacy rejection markers.

Restore originals only for transcript display/editing and sanitized sharing; exports remain inert. Treat empty rejection updates as authoritative over richer stale transcript rows. Cover legacy-provider exclusion, restart/retry barriers, hidden snapshots, editing attachments, and export redaction.
Align the copied-skill ownership regression with inert persisted capsules while checking both display metadata and provider exclusion. Document downgrade-safe rejection retention and legacy/raw truncation digests.

Validation: 1,294 targeted tests, 10 Storybook interactions, recorded two-process Node smoke and desktop/phone editing checks, static-check-full, and static-check pass.

---
_Generated with [`mux`](https://github.com/coder/mux) • Model: `coder:openai/gpt-6-astra` • Thinking: `high` • Cost: `$811.95`_
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

Please review a5eb5eff2a8b7eb9e2c36f7471cf95e330218d86. All seven findings are addressed, individually replied to, and resolved.

Validation: 1,294 targeted tests, 10 Storybook interactions, make static-check-full, and final make static-check pass. The current Node smoke proves rejection exclusion even after removing the new flag; the current full-app stories render real empty-assistant capsules, preserve original text/editing, and do not offer Retry. Nix-only formatting remains unavailable locally.

Legacy-safe rejection smoke

quarantine-smoke.webm

Capsule editing and smaller draft at 375px

capsule-phone.webm

Generated with mux • Model: coder:openai/gpt-6-astra • Thinking: high • Cost: $811.95

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

Please review current head a5eb5eff2a8b7eb9e2c36f7471cf95e330218d86, including downgrade-safe rejection capsules, complete copied-snapshot ownership, and compatible byte-preserving history recovery.


Generated with mux • Model: coder:openai/gpt-6-astra • Thinking: high • Cost: $811.95

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a5eb5eff2a

ℹ️ 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".

Comment thread src/node/services/historyScanner.ts
Comment thread src/node/services/historyService.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Security Review

Here are some automated security review suggestions for this pull request.

Reviewed commit: a5eb5eff2a

ℹ️ 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.

Comment on lines +83 to +84
if (options.sessionHistoryEnabled) {
agentPolicy.push({ regex_match: "session_history", action: "enable" });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Security: Require allowlists to grant session history

With Token-budget enabled, a custom/restricted agent whose tools.add omits session_history still receives it: this rule is inserted after deny-all, and no later rule matches the omitted tool. The checked-in policy test confirms that plan/explore/custom agents allowing only file_read receive session_history, whose executor returns pre-rollover transcript text. Thus a prompt-injected lower-privileged agent can read history its allowlist was meant to exclude. Require an explicit add or matching wildcard; otherwise block rollover as for an explicit disable.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is intentional under the approved implementation plan (D2), not an accidental allowlist bypass in the implementation: the plan explicitly grants session history as a baseline capability when the experiment is enabled, treats allowlist omission as implicit, and requires explicit deny rules—including regex denies—to block it. ADR-0005 records the same decision.

The requested explicit-allow policy would change that approved contract. I have asked the author whether to retain baseline recovery or switch to strict allowlist permission. Leaving this finding unresolved pending that decision rather than silently changing permissions or continuing contradictory review churn. The two independent history-correctness findings are being fixed separately.


Generated with mux • Model: coder:openai/gpt-6-astra • Thinking: high • Cost: $811.95

…tity

Use bounded JSON lexical duplicate-key detection before granting rollover exemptions, including escaped and nested keys, and retain those ambiguous rows through automatic rewrites. Match the newest duplicate request identity when rejecting its owned preludes while preserving rejection capsules.
@ThomasK33

Copy link
Copy Markdown
Member Author

Addressed the two independent history findings in c77d0d81eae887fb49c82735a298835fe78980b0:

  • Duplicate-key reset privacy floor: parsed reset candidates now receive bounded lexical duplicate-key detection before any rollover exemption. Escaped-equivalent keys and nested duplicates fail closed, and raw rewrite classification preserves ambiguous rows rather than normalizing them into valid rollovers. Direct/resumed retrieval and migration regressions pass; valid rollovers remain traversable.
  • Newest persisted rejection trigger: rejection now matches the newest id/sequence, consistent with request assembly. The real-disk regression checks the current trigger and its owned preludes are quarantined, repeated rejection remains idempotent, and earlier accepted inputs plus later sends remain provider-eligible.

Validation: 450 tests across seven suites, make static-check-full, and make static-check passed. A separate production-Node smoke run exercised the real history service and session_history, including both new cases. Screenshot and 18.84-second recording below show the run.

The allowlist-policy finding remains unresolved pending author direction; this commit does not change the approved D2 baseline policy. Review retriggers remain paused to avoid contradictory policy churn. CI for this new head is still running; this is not a readiness claim.

Production-Node history smoke checks

duplicates-smoke.webm

Generated with mux • Model: coder:openai/gpt-6-astra • Thinking: high

Generated with xum • Model: coder:openai/gpt-6-astra • Thinking: high • Cost: $847.58

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant