Skip to content

fix(tldr): TLDR and Goal stores evict the oldest record instead of filling up (#1277) - #1328

Merged
Juliusolsson05 merged 13 commits into
mainfrom
fix/tldr-store-evict
Sep 27, 2026
Merged

Juliusolsson05 merged 13 commits into
mainfrom
fix/tldr-store-evict

Conversation

@Juliusolsson05

@Juliusolsson05 Juliusolsson05 commented Sep 27, 2026 •

Copy link
Copy Markdown
Owner

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's tldr_update and goal_set would 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 tldrIdentity or as its session id. The session id covers the renderer's fallback identity (tldrIdentityForSession), so parked and hibernated agents are protected. See tldrIdentitiesInUse.

  • Pinned by a live session. Each store counts pins, and pin/unpin run in the store's own write queue. The spawn path awaits BuiltInMcpHttpHost.pinReportingIdentity before registerSession, and revokeSession releases the pin. Because the eviction chooses and commits inside one queued operation:

    • a pin queued first protects the record;
    • a pin queued behind an eviction in flight waits for that write to land.

    Every pin has exactly one owner:

    • the spawn path, if the spawn is cancelled, registerSession throws, or one store's pin fails;
    • otherwise the registration: 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.commit holds 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:

  • In-use unknown (before the workspace file opens) evicts nothing, so a store at the cap refuses (q40).
  • "Written" is the later of updatedAt and completedAt.
  • An evicted identity's revision is remembered in memory, so if it reports again in the same run it continues above that revision.
  • History files are kept, and the new writer's history row is written.
  • The app builds both stores through 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:

  • A new agent evicts the oldest record, and only it; a just-completed goal is not the oldest.
  • Records the recorded workspace names are never evicted (TLDR and Goal), nor a pinned live one. A parked goal read under its session id is never evicted.
  • q52: when the only unused record belongs to a live (pinned) session, the write is refused, the file is byte-identical, and the live goal can still be completed. Once unpinned, it is the one displaced.
  • q51/q52: a pin requested during an eviction's rename resolves only after that write has landed.
  • Pins are counted; everything in use, or in-use unknown, refuses without writing.
  • Revision continuity (both stores, highest real revision); one atomic write under an injected rename fault; history kept for the evictee and written for the new agent.
  • createReportingStores wires in-use into both stores; the host pins exactly the registration's identity and releases it on revoke; SessionManager.spawn registers only after the pin settles, and releases it when cancelled.
  • q56: a workspace save requested during the eviction's rename sees the completed write, and WorkspaceFileStore does not publish while the lock is held.
  • Through the real host and a capped store, the sole candidate is free again after each of these:
    • an unregistered pin's release;
    • a registration the provider policy filters to [];
    • a partial pin failure, which releases the other store.

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 -b is clean. npx tsc -b is clean. In the full suite, only the known local failures failed, plus ProviderEnablementRow (5 s timeout under full load; passes alone; unrelated file, reported to the manager).

Residual

  • With every one of 10,000 records in use, new identities are refused. That needs 10,000 agents across open workspaces and live sessions.
  • Before the workspace file opens at startup, a store already at the cap refuses new identities.
  • A session or pane that names an identity only AFTER an eviction chose it (then unused and unnamed) starts with "No goal yet", like any long-evicted identity.
  • A disk write that stalls inside the shared publication lock (an evicting store write, or a workspace save) also delays the other writer and the shutdown drain until it settles. There is no lock-order cycle (verified by all three reviewers), but a permanently stalled filesystem was not simulated.

Final verification (verify3, a/b/c, at 99645fda): all three MERGE-READY.

🤖 Generated with Claude Code

Juliusolsson05 and others added 2 commits September 26, 2026 17:21
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>
@Juliusolsson05 Juliusolsson05 added provider:claude Affects Claude Code integration provider:codex Affects Codex integration provider:opencode Affects OpenCode integration provider:pi Affects Pi integration type:bug Something works wrong class:C6-unbounded Unbounded growth sev:P2 Real bug with a workaround labels Sep 27, 2026
Juliusolsson05 and others added 2 commits September 26, 2026 17:38
…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>
@Juliusolsson05

Copy link
Copy Markdown
Owner Author

Review disposition (#1328): 3 reviewers, round 1 and round-2 verification; final head 1e17f6c2

Round 1

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.

Juliusolsson05 and others added 4 commits September 26, 2026 17:52
…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>
@Juliusolsson05

Copy link
Copy Markdown
Owner Author

Disposition addendum (#1328): steering q51, q52, q56 and two verification passes; final head 99645fda

This supersedes the round-2 row "refuses (storage is busy)" in the earlier disposition. That path was removed with the q52 redesign.

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.

@Juliusolsson05

Copy link
Copy Markdown
Owner Author

Final verification (verify3) at 99645fda: reviewers a, b and c all MERGE-READY, no findings. One unmeasured suspicion (a permanently stalled disk write inside the shared publication lock would delay workspace saves and the shutdown drain; no lock cycle) is stated in the body's residuals. The residual "a pane that names an identity only after an eviction chose it starts with 'No goal yet'" is with the owner per q56.

…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>
@Juliusolsson05

Copy link
Copy Markdown
Owner Author

CI fix at 38204e83 (test-only): the cross-layer recovery integration test's fake MCP host predated pinReportingIdentity, so its spawn threw and the system project failed. The fake now pins, and its registerSession assertion includes the release the registration owns. No production change since the MERGE-READY verify3 head 99645fda.

@Juliusolsson05

Copy link
Copy Markdown
Owner Author

OWNER-APPROVED: the 'No goal yet' after-eviction residual is accepted (the cap is ~1 year out).
Owner, 2026-09-27T07:33:05Z: "just make these for me, just keep the fixtures in, they are innocent enough. do not delete stuff often" (manager applied it).

@Juliusolsson05
Juliusolsson05 merged commit a1c1a26 into main Sep 27, 2026
2 checks passed
Juliusolsson05 added a commit that referenced this pull request Sep 27, 2026
@Juliusolsson05
Juliusolsson05 deleted the fix/tldr-store-evict branch September 27, 2026 08:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

class:C6-unbounded Unbounded growth provider:claude Affects Claude Code integration provider:codex Affects Codex integration provider:opencode Affects OpenCode integration provider:pi Affects Pi integration sev:P2 Real bug with a workaround type:bug Something works wrong

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TLDR/Goal stores never evict; at 10,000 identities every new agent's tldr_update and goal_set fails

1 participant