Repository navigation
Conversation
…adapter failure (review F5)
…g ticket (review F7)
…ek owner or the send releases them (review F1)
…esponses route (review F15)
…alResolver (review F8)
…eplay (review F12)
…d of a joined prompt string (review F9)
…tries and rebuilds (review F15)
…onses route (review F2)
…t page ordinals (review F11)
…n records (review F6) Add replace_begin/replace_commit/replace_abort records and one shared fold rule (ReplaceTransactionFold) used by every journal reader: an unfinished transaction folds to the prior history, a committed one to its rows, a malformed one throws. Publication still writes the old shape; it moves to transactions in the next commit.
… the surviving prefix (review F16)
…n records (review F6) A general streamed replacement now publishes begin, tagged rows and commit instead of a full rewind plus rows, so a crash or write failure before the commit leaves the previous history intact. Compensation writes replace_abort, or a restoring transaction once the replacement was durable, and derived state follows the durable fold when compensation fails.
# Conflicts: # packages/core/src/recording/journalResolver.ts
…s; F7 test fallout (review F6)
…nt enforcement (review F13) Warm-up now runs past the per-content measurement FIFO cap (550+ turns) and until the settled heap stops moving, then measures. Sampling is uniform every 5 turns; checkpoints are the median of 9 samples. Traps retain only after the baseline. The 1 MiB allowance, noise bound, fitted gate and per-checkpoint enforcement are unchanged. Normal runs (settled delta vs baseline at +100/+200/+300/+400 turns), 3 of 3 pass: run1 74289, 114071, 149368, 179036 (fitted 173260), 73.6 s run2 53615, 135669, 130086, 188390 (fitted 181300), 75.6 s file total run3 45090, 98595, 147708, 172987 (fitted 179437), 73.9 s Traps fail as designed: trap 6.1M/13.8M/21.4M/29.0M; release-trap 6.2M/13.7M/21.4M then 0.2M.
… resources (review F5)
… source families (review F9)
…r refresh (review F11)
…rows (review F16)
… instead of capturing the journal a second time
…e read back The provider normalization stage log kept its 64 KiB append buffer for the life of the request even after every row was written and the log had moved on to reading. Reading now flushes and drops the buffer; a later append allocates a new one. The logging boundary memory test pays the one-time decoder, writer and JIT costs of the same 64-row path before its baseline, and its trap tests still fail the same gates.
…o jest/expect-expect; shell processor fs mock stubs the scratch-root sweep
…ready runs instead of folding the whole journal again Every history mutation (four per turn in the R3 proof) captured row origins through a cursor, which re-folds every byte of the journal from offset zero with no checkpoint. Cost grew with journal length: R3 workload CPU per turn rose from 112 ms (turns 1-50) to 257 ms (turns 451-500), 205 ms of a 302 ms turn at turn 400 was this fold. At f035f19 it stayed flat at about 95 ms. The durable fold now numbers origins itself, using a dictionary owned by the checkpoint so restored rows keep their ids, and the pending fold exposes originAt. HistoryRowOrigins.capture reads them from the rows the fold holds. Row contents, request bytes and token counts are unchanged. R3 workload, CPU ms/turn at turns 451-500: 257 -> 105 (f035f19: 95).
…r has written it A publication scope hands its ops to the per-origin recorder mirrors when it closes, which is after the mutation already awaited journal durability. The mirrors only enqueue, so waitForCommit, restoreTurns and the other detached mutations resolved while a mirror's rewind or replacement was still in memory. Session restore then read a recording without the rewind (session-owner-restore-checkpoint failed on CI and reproduces locally). The fan-out now flushes every mirror when the feed settles, and both the detached mutation path and HistoryServiceCore settle the fan-out after their scope closes. Mirror flush failures are collected and rethrown together. historyRecorderFanout.durability.test.ts fails 3 of 3 without this.
… a trap that must trip it JavaScriptCore scans the machine stack conservatively, so one row of an old batch can stay reachable until its stale slot is reused, which made the gate fail intermittently on CI with a count of 1. A row now counts as retained only when the same row is still reachable at two consecutive checks; a real holder always is, a stale slot is not. The bound stays 0. A new trap holds batch-0 rows and must be reported. 400 local runs: 0 fail.
…dir instead of a recursive walk A Linux CI run failed with 'EFAULT: bad address in system call argument, rm .../llxprt-resolver-*'. Bun 1.3.14 reports failures of its recursive delete walk as EFAULT, which hides the real errno. The resolver index directories hold exactly one file the index created, so close() and the constructor failure path now unlink that file and rmdir the directory. A missing path is accepted (the root may already be gone); any other error surfaces with its real code. A cross-process test pins that the dead-root sweep keeps the root of a running process and reclaims it after the process is killed.
…keep traps that must report a held row JavaScriptCore's conservative stack scan can keep one dead row alive briefly, which made exactly-one-survivor failures appear on Linux CI. Local repro was 0 of 25 at HEAD and 0 of 25 with the fan-out flush reverted, so no holder was found. retainedAfterSettling counts an object only if it survives several collections. Bounds stay 0, and each converted test has a trap holding a row that must be reported.
Eight *-test-helpers.ts files lived under packages/agents/src/core and shipped in the npm package; one imported bun:test. They now live under src/core/__tests__/support, which the package excludes, and their importers point there. The no-bun-test-in-production-src guard now also fails on any *test-helpers* file outside __tests__ in a published source tree; it fails with a helper copied back under src/core. npm pack --dry-run lists no test helpers.
…timer for remaining writes and shutdown
turn-request-artifact.ts and turn-request-json.ts had no production importer (checked src, agents index, package exports, plugins, packages/cli). Only tests imported stageTurnRequestArtifact. Their memory tests read as if they covered live request logging; production stages request artifacts in storage (stageRequestArtifact via ConversationFileWriter), which they never touched. Both modules are deleted. Test files: (a) delete, tests only the dead module: - turn-request-artifact.test.ts - source-logging-boundary.test.ts (boundary identity parity, rejection of identity graphs, release traps; the UTF-16 cap section duplicates the split-surrogate cap cases in telemetry-stream-cap.test.ts) (c) memory test that only exercised the dead writer, deleted: - source-logging-boundary-memory.test.ts. Live-path memory coverage stays in telemetry-stream-memory.test.ts (exporter and reader residency with row and chunk traps) and the chunk-sink release test in telemetry-request-artifact-sink.test.ts. (b) live telemetry path, switched to __tests__/support/request-artifact-fixture.ts (a minimal row-local safe-JSON stager used only to produce the descriptor): - telemetry-stream-ack.test.ts - telemetry-stream-reader.test.ts - telemetry-stream-cap.test.ts - telemetry-stream-consumer.test.ts - source-preflight-abort.test.ts - source-preflight-http.test.ts - telemetry-request-artifact-sink.test.ts (renamed from turn-request-artifact-sink.test.ts; the "request-wide identity protocol" case asserted the dead writer's behaviour and is removed) - source-logging-identity-required.test.ts (live BoundarySnapshotDisk identity checks kept; the boundary-identity case that only the dead writer could satisfy is removed) - __tests__/support/source-strict-logger-fixture.ts - __tests__/support/runtime-request-lifecycle-test-helpers.ts - __tests__/support/telemetry-stream-memory-worker.ts
…on, array cache-anchor) out of the shipped tree
…the CompressionStrategy.compress member
Production summarizes only through OneShotStrategy.compressDisk. Behavior tests now run the disk route through a support adapter. The byte-equivalence helpers and the scripts summary-body test keep an independent array oracle under __tests__/support.
… as an async iterable
…ast row The send path scanned the whole journal every turn to find the last row for IDE context, before checking whether IDE mode was on. It now returns when IDE mode is off and otherwise uses the journal's last-content read.
…yMaxBytes resumeHistoryWindow built its ledger with the default 400 item / 4 MiB limits, so a resume trimmed to 400 rows whatever ui.historyMaxItems said. The callers now pass the limits the settings select; unset settings keep main's defaults.
…not a materialized array The owner-mode commands called session.getHistory() to read a length. They now read the history count from the journal-backed session stats.
…ream, not an array AgentSessionControl.restoreTurns collected the whole remaining history into an array for its only caller, which replays it into the UI one row at a time. The result is now an AsyncIterable over the journal; the type change is inside AgentSessionControl and adds no root export.
… configured historyMaxItems replayUiHistory used the default 400 item window like the other resume paths.
…the agents lib target lacks
hydrateRecordedMedia called store.admitObjects([]) for every replayed row, even when the row had no media. Each call takes the exclusive media store lock, ensures the store directories and fsyncs, so resuming N text rows cost N lock cycles. It now returns before admission when nothing needs copying. resumeHistoryLimits "applies the configured limit to an owner-session resume" (load average 35-65), resume step only: 500 rows: 13.4 s -> 0.55 s 1000 rows: 28.6 s -> 0.76 s 2000 rows: ~70 s -> 1.1 s (whole test 2.5 s) resume-text-rows-media-store.test.ts counts admitObjects calls on a store with a recording archive for 25 and 100 text rows; it saw 25 before the fix and 0 after.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TLDR
Conversation history now lives in an on-disk journal instead of in memory.
HistoryServiceis a facade over that journal, and subagents get their own journal files. Long sessions no longer keep a context-sized copy of history in the heap between turns.OpenAI chat, OpenAI Responses (both the
openaiprovider route and theopenai-responsesprovider) and Anthropic build each request body from journal rows, one row at a time, into a request-scoped spool on disk. The spool is uploaded withContent-Lengthequal to its sealed size. No whole-history array or whole request object is built on the send, estimate, dump or retry paths for these providers. Vercel and the Gemini plugin still build an array per send; they are declared exceptions (#3869).Request bytes and token counts match main for the same history.
Part of #854. The remaining #854 work is tracked under #3868.
Dive Deeper
What changed
requestRows. Stored media references are resolved one row at a time and released at call cleanup.fill()body andLazyBoundedJsonBodyare deleted (Responses progressive input materializes the whole history when a tool row is present #3858).replace_begin/ rows /replace_committransaction. Every journal reader, including the CLI scrollback pager, treats an incomplete transaction as absent, so a crash during a replacement resumes with the previous history. Single-record shapes (a suffix cut, a clear, a compression) are one record.Fixes to regressions found in review
/restoreleft project files changed when the history commit failed. They are rolled back.getStatistics,getContextRangeand the last-content reads rescanned the journal from byte 0. They now read incrementally: about 1 s and 43 MB per call to 0.04 ms and 4 KB at 8,192 rows.Defects found while getting CI green
restoreTurnscould return while the rewind was still in memory. Mutations now settle the fan-out after their publication scope closes (674e522). The new durability test fails 3 of 3 without the fix.EFAULTfrom Bun's recursivermof a resolver scratch directory. The cause was not reproduced. Resolver directories, which hold exactly one file, are now removed withunlinkandrmdir, so a recurrence reports the real errno (3353ad8). Other scratch owners still use recursiverm.Memory proof
packages/providers/src/__tests__/sendPeak.*.test.tssends a 1 MiB and an 8 MiB body (1.4 and 11.6 MiB tool-heavy) on every route. It requires that:Content-Lengthequals the bytes sent;Trap variants hold the whole history (array, estimate, fill) or keep it alive across sends (retain), and must break the bound. In the final run every route was inside the bound, with peak growth between -0.92 and +0.50 MB. Every trap broke its bound, by 12.5x to 76.6x.
packages/agents/src/core/__tests__/journal-retained-growth*.test.tsruns a realChatSessionwith compression over about 850 requests. It requires settled heap to stay under 1 MiB at six checkpoints, with fitted growth over the late checkpoints under 262,144 B. A retaining trap and a release trap must fail. Locally the normal run takes 67 to 80 s and the traps 81 and 114 s.Several WeakRef checks require that no released row survives. JavaScriptCore scans the stack conservatively, so a stale slot can keep one dead row alive for a collection or two, and those checks failed now and then on Linux CI with a count of 1. They now count a row only if it survives several collections, each after an event-loop turn (
retainedAfterSettling, and a two-consecutive-checks rule in the fan-out pump bound). The bounds stay at 0, and each converted test has a trap that holds a row and must report it.Speed
Warm send under the context limit, real runtime wiring, local HTTP provider, median of 5, measured at 0d1051f. The numbers are CPU ms, branch against main, because the machine was heavily loaded and CPU time is the fairer comparison:
At 1,000 rows the branch still adds about 150 to 250 ms per send, because each send normalizes every surviving row. #3849 tracks making normalization incremental across sends. Prompt estimation at 167k tokens takes about 250 ms (main 154 ms) with identical counts.
Behaviour changes reviewers should know about
Content-Lengthagain. They briefly went out chunked earlier on this branch.AbortErrorinstead of the provider's "terminated"./set retries 0on Responses providers sent nothing and returned an empty stream, on main as well. It now makes one attempt. A 200 response with no body is an error.hook_calltelemetry from there, with stdout/stderr capped at 4000 characters.MissingSnapshotHookCallbackErroris raised forHookSystemsubclass overrides, which are not a supported extension point.density_mutationrecords of at most 64 rows or 64 KiB each.SessionRecordingServiceuses backpressure instead of throwing at the queue limit.ui.scrollbackPagerEnabled) ships defaulted off. With it off, the transcript uses the fallback ledger, which keeps main's item and byte limits. Unified scrollback to the start of history: one disk-paged surface with the live turn, on by default #3856 tracks the unified scrollback.Decisions for the owner
retries. Main treatsretries=Nas N total attempts, not N retries, and the branch keeps that, clamping 0 to one attempt. Keep it, or change it to mean "N retries after the first attempt".FileLogExporteris synchronous, so the progress-based liveness check does not cover it./restoreside effects. Each restore makes one shadow commit, and ignored files are not rolled back.LoadBalancingProvider.timeout.test.ts, the "linked listener removal throws" fault now fires only after the first attempt starts, because request staging also removes listeners before any attempt.*test-helpers*undersrcstill importbun:test. A new guard blocks any other production file from doing so.Review
One full independent review of this branch found 1 blocker and 2 major defects. Each was reproduced, fixed and covered by a test that failed first. A follow-up check confirmed the hook fix and found the other two fixes incomplete on sibling paths. Those paths are also fixed:
replace_abortfirst (1761dca). Clone and checkpoint fork, which start a recorder on a copied journal, do the same (605ff99)./clear, pop, batch clear, row transform, validation repair or rollback checkpoint restored rows without their origin. A recorder for another origin attached later then received them. Every rollback now restores each row under its own origin (838ab2e, ce0686c).Follow-ups (under #3868)
Reviewer Test Plan
npm run build:types,npm run lint,npm run typecheck,npm run build,npm run test,npm run test:scripts,npm run check:runtime-boundary.cd plugins/google-gemini && npx tsc --noEmit && bun test.cd packages/providers && bun test src/__tests__/wireBodyGolden.test.ts. That is 315 cases over 7 routes and 22 histories, recorded before this work.packages/providers, run eachsrc/__tests__/sendPeak.*.test.tsfile on its own. Frompackages/agents, run eachsrc/core/__tests__/journal-retained-growth*.test.tsfile. Trap files pass by observing their trap break the bound.bun scripts/start.ts --profile-load lunahigh "write me a haiku and nothing else", plus a tool round trip./chat restoreto an earlier point. Quit,--continue, and confirm the history and the next request are right.Testing Matrix
Linux and Docker are covered by CI (unit shards, E2E with sandbox none and docker, tmux UI). Windows is covered only by the test-infra gate.
Gates at c5d349e. Every CI check passes. Locally:
build:types,lint,format(no diff),typecheck,lint:affected-shards,lint:agents-api-surfaceandcheck:runtime-boundarypass, and the branch is 0 commits behind main. The haiku and tool round-trip smoke tests passed on the merged tree.On earlier runs of this branch the Windows test-infra gate failed twice. Both failures were in launcher and runner meta-tests this branch does not touch: a child that sleeps 50 ms ran past its 4 s budget, and an
EBUSYcame from removing a temp directory. It passed on the next commit.Linked issues / bugs
Part of #854
Fixes #3847
Fixes #3857
Fixes #3858
Fixes #3865
Fixes #3866
Fixes #3867