Repository navigation
fix(tldr): TLDR and Goal stores evict the oldest record instead of filling up (#1277) - #1328
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… of refusing (#1277) At 10,000 identities every new agent's tldr_update and goal_set failed for good. The store now drops the record written longest ago (completion counts as a write) in the same atomic write, and remembers its revision so a returning identity is not discarded as stale. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…uses (#1328 review) Age alone could evict a running agent's goal, after which goal_complete failed and mounted views kept a stale record. Eviction now skips every identity named by a persisted workspace or a live MCP registration, and refuses (fails closed) when none qualifies or in-use is unknown. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…name (#1328 round 2) The renderer reads a reporting session with no explicit identity under its session id, which eviction did not protect; and in-use was sampled before the write's disk I/O. Every persisted session id is now in use, the commit rechecks before its rename, and the app's stores are built by a factory the tests drive. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review disposition (#1328): 3 reviewers, round 1 and round-2 verification; final head
|
| Finding | Verdict | Disposition |
|---|---|---|
a1 / c2: an active goal (the oldest by age) is evicted; the agent's later goal_complete fails |
valid, Major | Fixed in 57dbe6b4. Eviction never takes an identity named by any persisted workspace or by a live MCP registration; when none qualifies, or in-use is unknown, the store refuses (fails closed). |
| a2 / c1: mounted readers (peek, Agent Activity, Close Completed Agents) keep an evicted record; no removal event | valid consequence of a1 | Fixed at the source by the same policy: views read workspace sessions' identities, and those are never evicted. The renderer removal path was declined: it would add a new wire shape across IPC, remote frames and three readers for a state the policy makes unreachable. |
| a3 / b3: Goal revision continuity untested, and only from revision 1 | valid test gap | Fixed. Both stores are now tested from their highest real revision (TLDR 32, Goal 5). |
| b1: a two-write eviction would pass the tests | valid test gap | Fixed. An injected rename fault test; the two-commit mutant fails it. |
| b2: evictee history retention untested | valid test gap | Fixed; the test seeds real history. |
| c3: new writer's history row untested | valid test gap | Fixed, in the same test. |
| c2 (residual wording): "enforcement re-creates the record" | valid | Corrected. The hooks only prompt or block, and under the final policy a running agent is never evicted. |
| b survivor: revision remembered before the commit | declined | No user-visible effect: after a refused write the in-memory record's own revision wins (b agreed). |
Round 2 (verification)
| Finding | Verdict | Disposition |
|---|---|---|
| a / b / c: a session with Goal/TLDR enabled but no explicit identity is read under its session id, which was not protected | valid, Major | Fixed in 1e17f6c2. Every persisted session id is protected too, a superset of tldrIdentityForSession. The test gives a real recorded session that shape, and it is red on 57dbe6b4. |
| a / b: in-use sampled before the write's I/O can go stale | valid | Fixed. commit() rechecks right before the rename and refuses (storage is busy; try again.) without writing; the retry chooses afresh. The test flips in-use inside the I/O window and is red on 57dbe6b4. The rename syscall itself is the stated residual. |
| a: TLDR in-use protection unpinned | valid test gap | Fixed. The workspace test runs for both stores; the TLDR-only mutant is killed. |
c: index.ts wiring unpinned |
valid test gap | Fixed. createReportingStores(stateDir, inUse) (required parameter), driven by a test; the TLDR-only mutant is also killed here. |
| c suspicion: 400 ms autosave window | accepted as residual | Stated in the body. It needs a pane that spawned and lost its MCP registration within 400 ms, while the store is at the cap. |
…ction write (#1328 q51) registerSession is synchronous and can land during the awaited rename, after the last in-use sample. Once the rename lands the store asks again and restores any evictee now in use, displacing the next free record, inside the same serialized operation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d of compensating (#1328 q52) Evict-then-restore still lost a live goal when the evictee was the only unused record, and had a crash window between two renames. Sessions now pin their reporting identity in each store's write queue before they register; eviction chooses and commits in one queued operation, never takes a pinned or workspace identity, and refuses when nothing is free. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… pins (#1328 q56) A workspace save could name a parked pane's identity while an eviction deleting its record was being written, and a spawn cancelled after pinning left a pin nothing released. Evicting writes and workspace saves now hold one shared lock, and every pre-registration pin has a release on cancellation, registration failure and partial pin failure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…thing registers (#1328 verify2) A recovered session whose domains the provider policy filters away got no token, so its pin was never released. registerSession now takes the release: immediate when nothing is registered, otherwise on revoke. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Disposition addendum (#1328): steering q51, q52, q56 and two verification passes; final head
|
| Finding | Verdict | Disposition |
|---|---|---|
| q51: a registration during the eviction's awaited rename, after the last in-use sample | valid, destructive | First "restore after the rename" attempt (659adf13) withdrawn, see q52. |
| q52: the restore could not give back the sole free evictee; crash window between two renames | valid, destructive | Redesigned (24113672). Live sessions pin their identity through each store's write queue before registering. Eviction chooses and commits in one queued operation. With nothing free, the write is refused and nothing is deleted. Sole-candidate test red on the q51 head. |
| verify a / b / c + q56: a workspace save naming the evictee during the write | valid, Major | Fixed (62d52c98). A shared withReportingPublicationLock is held by WorkspaceFileStore.commit and by every evicting store write. Tests on both sides; either lock removed fails. |
| verify a / b / c + q56: pins leaked on cancellation, registration throw, partial pin failure | valid, Major | Fixed (62d52c98). An idempotent release on each path; mutants killed. |
| verify2 a / b / c: a registration that creates no token (policy-filtered domains) leaked its pin | valid, Major | Fixed (99645fda). registerSession takes the release and owns it: at once when nothing registers, otherwise on revoke. Identity-based unpin removed. Red on 62d52c98. |
| verify2 b: a slow evicting write delays workspace saves | valid observation | Accepted. It only happens on a new identity at the cap, and no cyclic wait exists (b verified). |
Invariant at this head: a record is never deleted while its identity is pinned by a registering or live session, or named by the committed workspace. When no unused record exists, the write is refused.
|
Final verification (verify3) at |
…1328) The cross-layer recovery test's fake MCP host predates pinReportingIdentity, so SessionManager.spawn threw before registering and the CI system project failed. The fake now pins, and the registration assertion includes the release registerSession owns. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
CI fix at |
|
OWNER-APPROVED: the 'No goal yet' after-eviction residual is accepted (the cap is ~1 year out). |
Fixes #1277
Problem
TldrStore.update()refused a new identity once the store held 10,000 records, and nothing ever deletes one. Past that point every NEW agent'stldr_updateandgoal_setwould fail for good. Both the TLDR and Goal stores are instances of this class. The owner's live stores hold ~425 and ~385 records after about two weeks, so the wall is roughly a year out.Change
At the cap, a new identity's write evicts the least recently written record that is not in use, in the same atomic write that adds the new record. If no such record exists, the write is refused (
<label> storage is full.) and nothing is deleted.In use means one of two things:
Named by the persisted workspace of any window, either as a session's explicit
tldrIdentityor as its session id. The session id covers the renderer's fallback identity (tldrIdentityForSession), so parked and hibernated agents are protected. SeetldrIdentitiesInUse.Pinned by a live session. Each store counts pins, and
pin/unpinrun in the store's own write queue. The spawn path awaitsBuiltInMcpHttpHost.pinReportingIdentitybeforeregisterSession, andrevokeSessionreleases the pin. Because the eviction chooses and commits inside one queued operation:Every pin has exactly one owner:
registerSessionthrows, or one store's pin fails;registerSession(scope, releasePin)releases at once when policy leaves nothing to register, and else on revoke.So no pin can protect a record forever.
Workspace saves and evicting writes share one lock (
withReportingPublicationLock).WorkspaceFileStore.commitholds it for its write, rename and document advance; an evicting store write holds it for sample, choose, write and rename. A save that names the evictee waits until that write has landed.A record is therefore never deleted while its identity is pinned or named by the committed workspace, and nothing is ever deleted and then compensated. That was the q51 approach, withdrawn after q52.
Also:
updatedAtandcompletedAt.createReportingStores(stateDir, inUse).Plan:
docs/plans/2026-09-26-tldr-store-evict.md, including every review round and steering note q45, q51 and q52.Tests
TldrStore.system.test.ts(15 cap cases), plus host and spawn-path cases:createReportingStoreswires in-use into both stores; the host pins exactly the registration's identity and releases it on revoke;SessionManager.spawnregisters only after the pin settles, and releases it when cancelled.WorkspaceFileStoredoes not publish while the lock is held.[];Mutations killed: either writer without the lock, no release on cancel, an unawaited pin, ignoring pins (5 failures), a pin that bypasses the queue, TLDR-only in-use, splitting the write in two, deleting evictee history, resuming revisions at 2.
Suites:
src/main/tldr/,src/mcp/runtime/,sessionManager.{wake,recover,codexReplacement},src/main/storage/,src/main/ipc/,src/main/goalLoop/, and session routing: 572/572 pass.npx tsc -bis clean.npx tsc -bis clean. In the full suite, only the known local failures failed, plusProviderEnablementRow(5 s timeout under full load; passes alone; unrelated file, reported to the manager).Residual
Final verification (verify3, a/b/c, at
99645fda): all three MERGE-READY.🤖 Generated with Claude Code