Repository navigation
fix: bound the small unbounded-growth items from the C6 hunt (#1278) - #1417
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d the legacy ledger parse is cached (#1278) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… search prompt cache via a shared LruMap (#1278) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e and is never cached (steering q109) A transient read failure (EACCES, EMFILE) returned an empty manual set, which put every hand-saved legacy bundle in the deletable bucket; the identity cache then kept that empty set after access recovered. Now a failed stat/read is 'unknown': not cached, and every legacy bundle is protected for that prune. Only ENOENT means no ledger. The identity key adds inode and ctime, so a rename-replace or a same-size edit with the mtime set back re-parses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…runing (#1251 row 13) Moved here from #1411 (manager q109: coordinate the overlap). Row 13 and steering q109 both change loadManualLegacyBundlePaths, so they ship together. A JSON-valid non-entry line (null, a number, a non-string bundlePath) threw, which rejected collectArtifacts and stopped every prune pass. Rows are now shape-checked; a non-string reason counts as manual, so the bundle is kept. Adds a test through the real loader and cache: #1411 review (b) found that replacing the loader's parse with an empty set survived the parser-only test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…codex Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ss the read (#1417 review a) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r appends after the evicted one (#1417 review b, c) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lose keeps queued saves (#1417 review b) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…bound Codex rolloutPaths (#1417 review c) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review disposition (round 1: a, b, c codex at
|
| Finding | Verdict | Disposition |
|---|---|---|
a, major: a ledger renamed between stat and read gets cached as empty, so a manual bundle becomes deletable |
valid | Fixed: re-stat after the load; any identity change → 'unknown' (protective, not cached). Fail-first for "still away" and "renamed back". |
a survivor: root + sep |
valid | Pinned with a sibling-root fixture. |
a survivor: ino in the identity |
declined | Not isolatable portably (a rename-replace also moves ctime). Kept as defence in depth. |
| b + c, major: an evicted paste writer with an in-flight append escaped the shutdown drain, and a re-created writer could reorder the file | valid | Fixed: flush() joins the in-flight append, and a re-created writer waits for the evicted flush. Gated-append test; both mutants red. |
| b, major: the tombstone count cap could drop a queued final save during a mass close | valid | Fixed: the bound is now age (10 min), not count. Red under the count cap. |
| c, major: scoped Claude discovery kept summaries of gone files in listed directories | valid | Fixed: summaries are swept per enumerated directory. Own-corpus test. |
c, minor: Codex rolloutPaths unbounded |
valid | Fixed: LruMap(4096). |
| c: plan 3b stale | valid | Corrected. |
b survivors: the promptFolder hook peek vs get; a search-cache cap of 1 |
declined | A test-only inspection hook and an untimed performance contract. The LruMap semantics are pinned in lruMap.test.ts. |
Round 2 (a, b, c codex at 62091493): all FIX-BEFORE-MERGE, the final round
Every round-1 reproduction verified fixed; the control mutants are red. Fixed at 02c7c918:
| Finding | Verdict | Disposition |
|---|---|---|
| a, major: a ledger absent for the whole prune made manual bundles deletable | valid | Fixed: an absent ledger is 'unknown', protecting every legacy bundle, never cached. Cost stated: without a ledger, pre-split legacy bundles never age out. Fail-first. |
| a, major: a removed Claude project directory kept its summaries | valid | Fixed: swept against the scope-independent root listing, plus ENOENT while listing; a failed listing keeps them. Fail-first. |
| b, major; a and c, minor: wall-clock steps broke tombstone aging | valid | Fixed: monotonic performance.now(). Wall-clock-step test; the Date.now mutant is red. |
| c, major; a and b, minor: a failed paste append lost its batch, rejected unhandled, and a joining flush resolved | valid | Fixed: the batch is re-queued in order, bounded at 1000 lines with a journal:dropped-lines report; the timer catches; both joining flushes reject on a dead disk. Red at the previous head; the bound is pinned. |
survivors: the rolloutPaths bound; the promptFolder hook / cache cap |
declined | Shared LruMap semantics, pinned in lruMap.test.ts. Pinning the field would need over 4096 indexed rows or reaching into the private map; a miss costs only the SQLite lookup. |
Both review rounds are spent. 02c7c918 needs a manager-verify (a, b, c) before the member gate.
Loader ownership (manager q118)
This PR owns the manual-ledger loader, and #1388 (W2, owner-held) rebases onto it after this merges. The contract that must survive that integration:
cachedManualLegacyBundlePaths()returnsSet<string> | 'unknown'. Absent (ENOENT) and unreadable are both'unknown', never an empty set.- A parse is trusted only when the ledger's identity (inode, ctime, mtime, size) is unchanged across the read.
'unknown'is never cached.legacyDebugBundleBucketForPath(path, 'unknown')returnsdebug-bundles-manual, so every legacy bundle is protected for that prune.
#1388's current loadManualLegacyBundlePaths still maps ENOENT to an empty set, and it answers unreadable with null (skipping legacy collection). After the rebase it should call this PR's loader and classifier rather than keep its own, so the stricter rule wins. Skipping collection instead of protecting is also fail-closed; only one of the two should remain.
Pinned by debugRetention.test.ts: "protects every legacy bundle while the ledger is absent…", the unreadable-then-readable sequence, the two rename-race cases, and the same-size edit with the mtime set back.
Main merged (0de17781) for a real conflict: #1376's .1-only retention test and this PR's tests were added at the same anchor in debugRetention.test.ts, and the resolution keeps both. debugRetention.ts auto-merged: #1376's .1 run markers sit beside this PR's parent sweep. On the merged tree npx tsc -b prints 0 lines, and every touched suite passes (storage 19, paste 5, window 17, lruMap 2, promptFolder 13, claude 7, codex 7, service 3). No source change beyond the merge; the manager-verify target is otherwise 02c7c918.
Manager verification: two survivors pinned (test-only)
| Survivor | Test | Mutant |
|---|---|---|
| The inode in the ledger cache key | debugRetention.ledgerIdentity.test.ts: stat is controlled for the ledger path only, with equal size, mtime and ctime and a different inode; the content is real. A real rename always moves ctime, so the inode can't be isolated on a real filesystem. |
Removing the inode: red |
| The paste retry order | pasteDebugJournal.test.ts: a held write fails while a line is appended during it; the file must read first, second. |
Re-queueing at the back: red |
npx tsc -b prints 0 lines; storage and paste suites green.
Residuals (stated, not blocking):
- A readable but emptied ledger parses as "no manual bundles". Only a manual edit produces one; an empty file is not unknown.
- The summary cache keeps a directory whose listing fails with EACCES (an EACCES-as-gone mutant survives). Keeping on an unknown error is the intended fail-closed direction, and the cost is memory only.
- The declined survivors: the
rolloutPathsbound, the promptFolder test hook, and the search-cache cap (sharedLruMapsemantics pinned inlruMap.test.ts).
Main merged again (1c0702f6) for a real conflict after #1411 merged: both PRs added tests at the same anchor in codex.system.test.ts, and the resolution keeps both. codex.ts auto-merged (#1411's per-row guard sits beside this PR's heads sweep, and rolloutPaths is an LruMap with the same set/get). On the merged tree npx tsc -b prints 0 lines and every touched suite passes (codex 9, storage 19 + 1, monitor 16, paste 6, window 17, lruMap 2, promptFolder 13, claude 7, service 5). There is no source change beyond the merge; the manager-verify target is otherwise 00a6ccbb.
Manager verification and READY
B6 verified the code and both pins at 1c0702f6 (manager-verify a/b/c, MERGE-READY once the body and one comment were corrected): a1–a4 are fixed and killed; 50/50 pass.
- Body corrected:
- 3a: absent =
'unknown', protected, never aged; - 8: a 10-minute monotonic TTL;
- the Claude summaries sweep;
- the paste retry and the 1000-line cap;
- the full residual list (the a1 cost, the emptied ledger, the EACCES summary survivor, the declined survivors).
- 3a: absent =
- Comment corrected at
cachedManualLegacyBundlePathsin8b3928d7: comment-only; the second stat now guards a ledger replaced or edited mid-read, and the loader already answers ENOENT with'unknown'.
Main merged (d3745374) after batch Q, which carried #1434 and conflicts with this PR in codex.ts:
- Import lines, both kept: fix(conversations): an unreadable conversation file is said, not 'no prompts' (#1306) #1434's
ConversationPromptsUnreadablehelpers from./types.js, and this PR'sLruMap. - Semantic conflict, fixed in the merge: fix(conversations): an unreadable conversation file is said, not 'no prompts' (#1306) #1434's new
promptsUnreadable.system.test.tsread the privaterolloutPathsas aMap(.has/.get), so on the merged tree its two Codex cases failed withpaths.has is not a function. They now useLruMap.peek(), which does not touch recency. No source change. - Result:
npx tsc -bprints 0 lines, and conversations + storage + paste + window + lruMap pass 182/182.
Main merged again (6cd622f4) after batch R, which carried #1388 and conflicts in debugRetention.ts. Both sides are kept:
- Imports: this PR's
rmdir/sepand fix(debug-retention): collect key-log-only proxy run dirs from new runs only; existing key logs untouched #1388'swriteFile/relative. collectArtifacts: this PR'scachedManualLegacyBundlePaths()stays the only ledger read, so the q118 rules win: an absent or unreadable ledger is'unknown', never cached, and every legacy bundle is protected. fix(debug-retention): collect key-log-only proxy run dirs from new runs only; existing key logs untouched #1388's key-log baseline capture follows it. fix(debug-retention): collect key-log-only proxy run dirs from new runs only; existing key logs untouched #1388 already uses this loader and keeps none of its own.- Result:
npx tsc -bprints 0 lines, and storage + conversations + paste + window + lruMap + performance pass 413/413.
merge-gate.sh agent-code 1417 --member --dry: GATE PASS (6cd622f4, 0 behind main, checks green). READY.
…egacy bundle (#1417 review round 2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t are gone (#1417 review round 2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…jects unhandled, never flushes as success (#1417 review round 2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # src/main/storage/debugRetention.test.ts
…e-parsed (#1417 manager verification) Removing the inode from the cache key passed every test: a real rename moves ctime, so no portable filesystem sequence isolates it. stat() is controlled for the ledger path only (size, mtime, ctime equal; the inode differs); the content is real. The no-inode mutant is red. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…while it wrote (#1417 manager verification) Re-queueing a failed batch at the back passed every test. A held write that fails, with a line appended during it, must land first/second in order; the back-requeue mutant is red. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # src/main/conversations/sources/codex.system.test.ts
…loader (#1417 manager verify) Comment-only. The comment still said the loader "rightly" answers ENOENT with "no ledger"; since round 2 an absent ledger is 'unknown', and the second stat now guards a ledger replaced or edited mid-read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Real conflict: batch Q's #1434 and this PR both changed codex.ts's imports (#1434 adds ConversationPromptsUnreadable and friends from ./types.js, this PR adds LruMap). Both kept. Semantic conflict: #1434's promptsUnreadable.system.test.ts reads the private rolloutPaths as a Map (.has/.get), which this PR bounded as an LruMap. The two Codex tests now read it with peek(), which does not touch recency, so the lookup cannot change what the source evicts next. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Real conflict with #1388 in debugRetention.ts, both sides kept: - imports: this PR's rmdir/sep (parent sweep) + #1388's writeFile/relative (key-log baseline); - collectArtifacts: this PR's cachedManualLegacyBundlePaths() stays the one ledger read (q118: absent or unreadable ledger = 'unknown', every legacy bundle protected, never cached), and #1388's keyLogOnlyBaseline capture is added after it. #1388 already calls this PR's loader; it keeps no loader of its own. tsc -b 0 lines; storage, conversations, paste, window, lruMap and performance suites 413/413. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
408eca9
into
integration/batch-2026-09-27-t
…9-27-t Integration batch T (#1417)
Closes #1278 (the C6 hunt's small unbounded-growth items). Also carries #1251 row 13 (a malformed legacy-ledger row stopped all pruning), moved from #1411 because it and steering q109 change the same ledger loader. Per-item evidence and decisions:
docs/plans/2026-09-27-c6-small-unbounded.md.Every item was verified on origin/main
5e22c7b0first. Each fix has a test that fails without it: fail-first, or with the fix removed as a mutation. Final head8b3928d7: the code and both B6 pins were manager-verified at1c0702f6, and the later commit only corrects a comment.Owner rule "do not delete stuff often": these fixes bound memory and repair a cleanup that was already intended. None adds a new deletion policy for user-visible data.
rm(dir, { recursive: false }), which always throws EISDIR (checked on Node 24.14.1), so no parent was ever removed. Nowrmdir, which is atomic against a concurrent new run (ENOTEMPTY) and bounded byroot + sep. A real-filesystem test is red on main.'unknown'. Absent and unreadable are both'unknown'(steering q109 and review round 2): never cached, and every legacy bundle is protected for that prune. This also closes main's windows where a transient read failure, or a ledger moved aside during a prune, made hand-saved bundles deletable. Pinned: the absent, unreadable-then-readable, two rename-race and same-size-edit-with-mtime-set-back cases, and the inode in the key (B6 pin).flushAllawaits evicted flushes (the dictation #1276 shape), and a re-created writer is ordered after the evicted one. Failed appends: a failed batch is re-queued at the front, in order (B6 pin). The queue is bounded at 1000 lines, and overflow writes ajournal:dropped-linesrecord. The timer's drain is caught, so a failed timed append is never an unhandled rejection, and a flush that joins a failing drain rejects instead of reporting success.retiredWebContentsIdstombstones ("cleared with the registry" only happened in tests)performance.now()(monotonic, so a wall-clock step back cannot strand stale entries), and are swept whenever another window closes. Test red on main.heads: swept against the scope-independent walk (exact); mutant red. CodexrolloutPaths:LruMap(4096)(the recorded corpus has 2,023 indexed threads). Claudesummaries: swept per listed project directory, and against the root listing for vanished project directories. SearchpromptCache: LRU 1024 through a new sharedLruMap, which now also backspromptFolder; its private LRU functions are removed, and its 13 tests are unchanged and green.Residual or intentional, not changed (reasons in the plan):
rolloutPathsset and its 4096 bound (the Codex search falls back to SQLite, so no test sees the cache);promptFoldertest hook;LruMapsemantics are pinned inlruMap.test.ts).lastActivityAtis kept on purpose (WHY atsessionManager.ts:1095).cli-update-logs(13 files, 52 KB here) is kept on purpose per its WHY.Verification:
npx tsc -b: 0 lines.Overlap:
codex.system.test.ts).codex.tsimport conflict is resolved with both kept. Its unreadable-rollout tests now read the boundedrolloutPathswithpeek(), and 182/182 conversation, storage, paste, window and lruMap tests pass on the merged tree. fix: bound the small unbounded-growth items from the C6 hunt (#1278) #1417 owns the manual-ledger loader (q118). fix(debug-retention): collect key-log-only proxy run dirs from new runs only; existing key logs untouched #1388 (batch R) merged onto it, and the merge keeps its key-log baseline beside this PR's single cached ledger read.🤖 Generated with Claude Code