You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
PR #1726 adds a recoverable on-disk backing for the resident string store and releases materialized views at agent_idle; PR #1729 (stacked) spills resident strings to that backing across compaction. Reading both diffs end to end surfaced six defects/risks that must be fixed before the pair can merge. This issue tracks completing the design to its ideal state and landing both PRs.
Actual (at #1726 head 8359873f, #1729 head 32618a6c)
A. spillResident() never releases memory.session-resident-store.ts:66-77 deletes from strings but not from the idsByText reverse index; the Map keys are the spilled strings themselves, so every spilled string stays pinned. changes.md:5973 claims the index is "deleted on eviction/spill/clear" — false for spill. feat(coding-agent): spill resident strings to the blob backing across compaction #1729's stated goal (empty the map, keep the backing) is therefore not achieved.
B. BigInt path now crashes with the wrong error. The explicit TypeError("Do not know how to serialize a BigInt") was removed from transformJsonValue, but a bigint then reaches seen.add(value) on a WeakSet → TypeError: WeakSet values must be objects (verified with Bun 1.4). The PR body's "serializes BigInt values instead of throwing" is not what the code does.
C. Runtime tokens escape to consumers. Between agent_idle and the next turn, agent.state.messages holds \0senpi-resident-string:v1:<id> tokens (agent-session.ts:1998-2002). The public getter get messages() (agent-session.ts:3445) returns that array unchanged, so any idle-time reader (TUI message selector, interactive-mode.ts:9400 JSON export, extensions, RPC) sees tokens. test/suite/harness.ts had to be changed to materialize through the store — proof that the consumer contract changed despite the PR body's "every existing consumer contract unchanged". This is the same failure class as _getCompactEntries() leaks evicted resident-string refs into resumed image data #1631, introduced into runtime state.
D. Blob directory growth is unbounded. Ids are a per-process counter; a string hydrated from a blob and re-externalized at the next idle gets a NEW id → a NEW .blob file, and old files are never deleted. The dir is only removed on newSession/branch inside the same process — never on process exit or on reopening the session. feat(coding-agent): spill resident strings to the blob backing across compaction #1729's "bounded by the resident budget" does not hold.
E. Cross-process id collision. Two processes on one session id share <sessionDir>/resident-blobs/<sessionId>/; both start ids at 0, so N.blob is overwritten with a different text and the other process hydrates the wrong content silently (the envelope is valid JSON).
Blob files are content-addressed (id = hash of the text): re-externalizing hydrated text maps to the same blob, no per-turn churn, and two processes writing the same session dir can only ever write identical bytes under a given name. The reverse index becomes unnecessary, which fixes A structurally; spillResident() empties the map and releases the strings.
session.messages and every out-of-turn reader of runtime messages (compaction admission estimates, log-token estimates) always observe real strings; test/suite/harness.ts returns to its pre-PR form.
The idle release (view drop + in-place tokenization) runs only when the store has evicted something.
The blob directory is released when the session's writer is disposed and cleared when a persisted session is reopened; newSession/branch removal stays.
Corrupt blobs are deleted on read so a later eviction rewrites them.
Acceptance criteria
test/session-resident-store.test.ts: (a) re-externalizing a spilled string makes it resident again and yields the pre-spill token; (b) two stores over one blobsDir with different texts each hydrate their own text; (c) evict→hydrate→re-externalize adds no .blob file; (d) a corrupt blob is removed on read; (e) externalize({ n: 1n }) throws TypeError matching /BigInt/.
Harness tests: after agent_idle on a persisted over-budget session, session.messages text equals the original; an under-budget session keeps getEntries() identity across idle, an over-budget one does not.
SessionManager tests: blob dir removed on writer disposal; stale dir cleared on open(); existing newSession/branch removal tests stay green.
Focused suites + root bun run check green; PR CI green at the final head; changes.md and CHANGELOG.md entries updated.
Summary
PR #1726 adds a recoverable on-disk backing for the resident string store and releases materialized views at
agent_idle; PR #1729 (stacked) spills resident strings to that backing across compaction. Reading both diffs end to end surfaced six defects/risks that must be fixed before the pair can merge. This issue tracks completing the design to its ideal state and landing both PRs.Actual (at #1726 head
8359873f, #1729 head32618a6c)spillResident()never releases memory.session-resident-store.ts:66-77deletes fromstringsbut not from theidsByTextreverse index; the Map keys are the spilled strings themselves, so every spilled string stays pinned.changes.md:5973claims the index is "deleted on eviction/spill/clear" — false for spill. feat(coding-agent): spill resident strings to the blob backing across compaction #1729's stated goal (empty the map, keep the backing) is therefore not achieved.TypeError("Do not know how to serialize a BigInt")was removed fromtransformJsonValue, but a bigint then reachesseen.add(value)on aWeakSet→TypeError: WeakSet values must be objects(verified with Bun 1.4). The PR body's "serializes BigInt values instead of throwing" is not what the code does.agent_idleand the next turn,agent.state.messagesholds\0senpi-resident-string:v1:<id>tokens (agent-session.ts:1998-2002). The public getterget messages()(agent-session.ts:3445) returns that array unchanged, so any idle-time reader (TUI message selector,interactive-mode.ts:9400JSON export, extensions, RPC) sees tokens.test/suite/harness.tshad to be changed to materialize through the store — proof that the consumer contract changed despite the PR body's "every existing consumer contract unchanged". This is the same failure class as _getCompactEntries() leaks evicted resident-string refs into resumed image data #1631, introduced into runtime state..blobfile, and old files are never deleted. The dir is only removed onnewSession/branch inside the same process — never on process exit or on reopening the session. feat(coding-agent): spill resident strings to the blob backing across compaction #1729's "bounded by the resident budget" does not hold.<sessionDir>/resident-blobs/<sessionId>/; both start ids at 0, soN.blobis overwritten with a different text and the other process hydrates the wrong content silently (the envelope is valid JSON).getEntries()consumer re-materializes the full history each turn — the cost bug(coding-agent): post-compaction entry counting re-materializes the full JSONL history on the settled path #1699 (57a7b5441f) just removed — even though releasing the views frees nothing unless the store has evicted something (resident strings are shared objects).Expected (ideal state)
spillResident()empties the map and releases the strings.externalize/materializethrowTypeErrormatching/BigInt/.session.messagesand every out-of-turn reader of runtime messages (compaction admission estimates, log-token estimates) always observe real strings;test/suite/harness.tsreturns to its pre-PR form.newSession/branch removal stays.Acceptance criteria
test/session-resident-store.test.ts: (a) re-externalizing a spilled string makes it resident again and yields the pre-spill token; (b) two stores over oneblobsDirwith different texts each hydrate their own text; (c) evict→hydrate→re-externalize adds no.blobfile; (d) a corrupt blob is removed on read; (e)externalize({ n: 1n })throwsTypeErrormatching/BigInt/.agent_idleon a persisted over-budget session,session.messagestext equals the original; an under-budget session keepsgetEntries()identity across idle, an over-budget one does not.open(); existingnewSession/branch removal tests stay green.bun run checkgreen; PR CI green at the final head;changes.mdandCHANGELOG.mdentries updated.Related
/compactmemory growth), bug(coding-agent): post-compaction entry counting re-materializes the full JSONL history on the settled path #1699 (settled-path re-materialization), Resuming image-heavy sessions stalls the TUI; evicted strings cause repeated full-history reads #1407 / perf(coding-agent): batch evicted resume hydration #1595 (batched evicted recovery)