Skip to content

Stop the persisted state growing by megabytes per run - #17

Merged
christopherjnelson merged 1 commit into
mainfrom
claude/optimistic-shannon-4dh7gs
Sep 28, 2026
Merged

christopherjnelson merged 1 commit into
mainfrom
claude/optimistic-shannon-4dh7gs

Conversation

@christopherjnelson

Copy link
Copy Markdown
Member

The state file grew by megabytes per run for three reasons:

  • Every Worker result stored the complete repository tree (every file's bytes, base64) in workerEvidence.entries.
  • Streamed progress events kept the raw provider event.
  • Each 5 s poll of a running assignment appended an event and rewrote the whole state file.

On a 2.5 MB fixture repository the state grew 3.5 MB per run: 121 MB after 35 runs, with each state write taking about 950 ms and each run about 95 s.

Changes

  • Compact Worker evidence.
    • New evidence stores only its exact changes, the complete snapshot's entry count, and a sha256 over the canonical tree (snapshotFormat: 'base_plus_changes').
    • fullSnapshotEntries() rebuilds the tree from the pinned Git base plus the changes. Every "before" must match the base, and a count or digest mismatch is rejected.
    • Validation and promotion use the rebuilt tree. Promotion's copy is transient and never written back.
  • Legacy records are unchanged.
    • They keep their stored entries and are read exactly as before, so their approval and promotion digests do not change.
    • A recorded legacy state fixture (tests/fixtures/legacy-evidence-state.json) covers approve, promote, and tamper detection on old records.
  • The approval digest still binds the whole result. It hashes the stored record, which now includes the snapshot digest.
  • Bounded progress events.
    • They store the response id, model, status, assignment id, and activity kind and summary instead of the raw provider event. That is everything the Live Response panel reads.
    • assignment.progress and assignment.reconciled are capped at 500 in persisted state, and sequence numbers stay monotonic.
  • No-op polls. A poll that finds a running assignment still running no longer appends an event or rewrites state.

Measured (same benchmark, before → after)

before after
State growth per run 3.5 MB 42–60 KB
State after 35 runs 121 MB 1.8 MB
State write (mutate) at run 20 ~950 ms ~23 ms
Full run at run 35 ~95 s ~7 s
200 polls of a running assignment 200 events, 200 rewrites 0 events, 0 rewrites

Verification

  • Typecheck is clean.
  • vitest: 559/559 passing. New tests: snapshot-evidence, event-volume and live-response, plus a compact-evidence approve → promote assertion in recorded-integration.
  • pnpm test:bridge: 62/62 passing.
  • Build succeeds.

The work was started by a sub-agent that was stopped partway through. I reviewed it, merged it onto current main, and updated its legacy test to send the evidence digest that approvals now require.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DXCvrGWmmRNDLmy7nYS2QP


Generated by Claude Code

Every Worker result stored the complete repository tree (every file's
bytes, base64) in workerEvidence.entries, streamed progress events kept
the raw provider event, and each 5 s poll of a running assignment
appended an event and rewrote the whole state file. On a 2.5 MB fixture
repository the state grew 3.5 MB per run: 121 MB after 35 runs, with a
state write taking ~950 ms and a run ~95 s.

- New Worker evidence stores only its exact changes plus the complete
  snapshot's entry count and a sha256 over the canonical tree
  (snapshotFormat 'base_plus_changes'). fullSnapshotEntries() rebuilds
  the tree from the pinned Git base plus the changes, requires every
  "before" to match the base, and rejects any count or digest mismatch.
  Validation and promotion use the rebuilt tree; promotion's copy is
  transient and never written back.
- Legacy records keep their stored entries and are read exactly as
  before, so their approval and promotion digests are unchanged. A
  recorded legacy state fixture covers approving, promoting and tamper
  detection.
- The approval digest still binds the whole result: it hashes the stored
  record, which now carries the snapshot digest.
- Progress events store a bounded record (response id, model, status,
  assignment id, activity kind and summary) instead of the raw provider
  event. That is everything the Live Response panel reads.
- assignment.progress and assignment.reconciled events are capped at 500
  in persisted state. Sequence numbers stay monotonic.
- A poll that finds a running assignment still running no longer appends
  an event or rewrites the state.

Measured with the same benchmark: 42-60 KB per run, 1.8 MB after 35
runs, state writes ~23 ms, runs ~7 s, and 200 polls of a running
assignment cause no writes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DXCvrGWmmRNDLmy7nYS2QP
@christopherjnelson
christopherjnelson merged commit 397a5db into main Sep 28, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants