v0.3.0 — content-addressed storage (205MB → 30MB, 6.8x) - #18
Merged
Conversation
Splits the dedupable parts of every captured request — system block,
tools array, each messages[]/input[] item — out of log.jsonl and into
sha256-keyed blob files under <projectId>/blobs/. The on-disk
interaction record becomes a small manifest that references those blobs
by hash; hot blobs (tools, system, conversation-prefix items) are
referenced by every iteration in a session but written exactly once.
Layout change: projects move from
~/.agentmind/projects/<id>.jsonl
to
~/.agentmind/projects/<id>/
log.jsonl
blobs/<aa>/<sha256>.json
Auto-migrated on Storage init (idempotent). Legacy v1 lines coexist
with v2 manifests in the same log.jsonl; loadProject reads both. The
first time a project with v1 lines is loaded (and its file hasn't been
touched in REPACK_QUIET_MS), the log is atomically rewritten into v2
manifests + blobs — best-effort, never blocks the in-memory result.
Measured on the captured ~/.agentmind/projects/ corpus:
ac90562a (claude, 1112 iters) 126.2MB -> 7.1MB 18.6x
b22fd262 (claude, 392 iters) 28.1MB -> 2.1MB 13.4x
475fc34f (codex, 120 iters) 20.8MB -> 5.8MB 3.6x
5df6e381 (codex, 46 iters) 10.6MB -> 5.8MB 1.8x
4eb642b5 (claude, 36 iters) 5.4MB -> 1.2MB 4.5x
(small projects 1-22 iters) ~13MB -> ~5MB ~2.6x avg
----------------
TOTAL 205.5MB -> 30.0MB 6.8x
Codex shrinks less than Claude because input[] items (each function_call,
each function_call_output) are unique per turn, so message-level dedup
has a low hit rate; the win comes from tools+instructions instead.
Verified byte-equivalence across all 143 unique interactions in the
corpus (deep-equal of request/response/sseEvents/headers between
backup and unpacked output).
New files:
src/server/blobs.ts content-addressed read/write + per-load cache + GC
src/server/interaction-pack.ts CapturedInteraction <-> v2 manifest
scripts/smoke-storage.mjs roundtrip + dedup + migration + quiet-window guard
Tests added to `pnpm smoke`:
A. 8-iter Claude roundtrip: 80 inline message writes -> 26 unique blobs (3.3x)
B. legacy flat-file migration: file moved + repacked (19KB -> 6KB, 3.0x)
C. quiet-window guard: freshly-modified file is NOT repacked
Concurrency model: single-writer per project (the proxy). Multiple
processes are tolerated for blob writes (rename is atomic, contents
identical) but explicit GC (--gc, future) must be exclusive.
Crash model: blobs land before the referencing manifest is appended.
A crash leaves harmless orphan blobs at worst; never a manifest with
dangling refs.
Doesn't change: capture path latency (writeBlob short-circuits on hot
blobs via existsSync), JSONL append-only crash safety, or the on-the-
wire format consumed by the dashboard.
Co-authored-by: Cursor <cursoragent@cursor.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Cut on-disk capture size by ~7x by content-addressing the repeated
parts of every request.
Heavy hitters in a typical capture (system prompt, tools array, each
messages[]/input[] item) are now written once to
<projectId>/blobs/<aa>/<sha256>.jsonand referenced by hash from asmall v2 manifest line in
log.jsonl. Hot blobs (tools, system,prefix-history items) get referenced by every iteration but stored
exactly once.
Measured on real captured data
11 projects in
~/.agentmind/projects/, repacked in place:Codex shrinks less than Claude because each
function_call/function_call_outputitem ininput[]is unique per turn — the wincomes mostly from dedup'ing
tools+instructions. Long Claudesessions are where this lands hardest because
tools[](124KB per callin dogfood data) and
system(27KB) are byte-identical across 1000+iterations.
Format
Per-project folder (was: flat
<id>.jsonl):v2 interaction manifest (one line in
log.jsonl):{ "type": "interaction", "v": 2, "interactionId": "...", "messageId": "...", "index": 3, "agentType": "claude-code", "request": { "meta": { "model": "claude-sonnet-4-6", "max_tokens": 32000, "stream": true, ... }, "systemRef": "sha256:abc...", "toolsRef": "sha256:def...", "historyKey": "messages", "historyRefs": ["sha256:...", "sha256:...", ...] }, "requestHeaders": {...}, "response": {...}, // inline (unique per request) "sseEvents": [...] // inline (unique per request) }v1 lines (no
vfield) still read inline — coexistence is fullysupported, so a partially-migrated file works.
Migration
Three layers, all automatic + idempotent on Storage init:
<id>.jsonlfiles renamed to<id>/log.jsonl. The old smoke-codex.mjs mixed-agent migration testwas updated to assert the new layout.
loadProjectnotices any v1interaction lines, hashes their inline content into blobs, and
atomically rewrites
log.jsonlwith v2 manifests + the originalproject/message rows. Skipped if the file was modified in the last
5 seconds (quiet-window guard against clobbering an active capture).
sessions/rename,cwd-only-id split, mixed-agent split) all preserved.
Verification
pnpm typecheckclean.pnpm smoke(now includesscripts/smoke-storage.mjswith 3scenarios: roundtrip+dedup, legacy migration, quiet-window guard) —
all green.
request/response/sseEvents/headers/ metadata wascompared between
/tmp/agentmind-backup/(pre-migration) and thev2-unpacked output of
Storage.loadProject. Result: 143interactions across 11 projects, 0 mismatches (modulo JSON key
order, which is normalised away).
Risks evaluated, in priority order
REPACK_QUIET_MS = 5_000skips repack whenlog.jsonlwas just touchedBlobCache: same hot blob is read at most once per project loadrenameis atomic; identical contents make the race no-op-safe<id>.jsonlexternallyFuture work, intentionally out of scope
agentmind-cli --gcsubcommand: walk all manifests, sweep orphanblobs.
gcBlobs(root, used)is already exported, just needs a CLIflag + a "no live captures running" assertion.
duplicated Claude tool definitions across repos, but per-project
isolation makes
rm -rf <id>/cleaner. Defer.Test plan
pnpm typecheckpnpm smoke(incl. new storage smoke)