feat(shared): primitive history metadata storage and versioned import hashes - #694
Conversation
New history writes insert ordinary metadata strings as primitives and create LoroText only for fields that genuinely stream, removing the thousands of unnecessary text containers a long tool-heavy session used to accumulate. - historyItemAnySchema and the nested tool/worktree payload catchalls default to primitive storage; streaming fields are declared explicitly (outer text/markdown, tool content text/output and nested content.text, worktree step output) via storageSchema hints. - Nested tool/worktree metadata (command, args, cwd, path, terminalId, input scalars, steps[].command) and diff oldText/newText stay primitive. - Insertion-only: opening old storage performs no writes, a legacy Text keeps its container id on a same-kind edit, a legacy primitive stays primitive, and malformed/unknown stored shapes survive unrelated edits. - The materializer comment no longer describes storageSchema hints as waiting on a loro-mirror patch; HistoryWriter owns every history write. Measured on unmodified main (375c4c7): toolCallId, status, title, kind, locations[].path, command, args[], cwd, terminalId and step command/output were all Text containers before this change. Tests: packages/shared/tests/history-storage-policy.test.ts (new) drives the real HistoryWriter/Mirror composition; shared suite 1211 passed. Model: kimi-k3
…e versions
Canonical import turn hashes now carry an explicit version, so a stored
v1 cursor is never compared against v2 hashes after an upgrade.
- HASH_VERSION_V1/V2 in the shared session-data import owner. v1 keeps
the frozen verbatim {role, items, plan} form (pinned by a fixed
fixture); v2 hashes a canonical item form so a sealed tool_call
skeleton and the full call it was sealed from hash identically.
Documented tradeoff: v2 excludes tool payload (content/rawInput/
rawOutput) from identity, so tool-output-only source drift does not
by itself trigger a refresh.
- The session doc cursor versions its own importedTurnHashes
(hashVersion) independently of ExternalAcpHistorySyncMeta.hashVersion,
because a conflict marker may advance only the metadata. The stored
baseline records its own version; a baseline with no version field is
v1 and matches a v1 cursor, while a genuine v1/v2 mismatch in either
direction is rejected.
- HistoryImportCursorSchema declares hashVersion so Zod does not strip
it at the port boundary. decideHistoryRefresh /
decideHistoryConflictResolution / planHistoryImport recompute replay
hashes in the stored version when they differ; unknown versions or a
missing replay history refuse before any write instead of guessing.
- applyHistoryImport computes the projected suffix in the stored
cursor's version and writes the next cursor with the replay's version
inside the existing no-await commit block; a cursor failure after the
history write remains indeterminate.
- The CLI sync service materializes new imports at v2 and passes the
version through its conflict pre-checks.
Tests: version matrix at the decision level (v1/v1, v2/v2, cursor
v1/meta v2, cursor v2/meta v1, skeleton parity, unknown version), the
port level through applyHistoryImport (genuine unversioned v1
cursor/baseline upgrade, untracked suffix, already-resolved, cursor
failure stays indeterminate), and the real SessionDocument writer level
(genuine pre-version client shape with no hashVersion field anywhere).
Shared suite 1211 passed; CLI suite green except the pre-existing
environment-dependent worktree-gc failure that reproduces on unmodified
main.
Model: kimi-k3
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9c77395502
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| const turnHashes = args.replayHistory.map((entry) => | ||
| hashHistoryEntryForVersion(entry, args.storedHashVersion) | ||
| ); |
There was a problem hiding this comment.
Reject unknown replay hash versions before recomputing
When a refresh supplies an unknown replay version (for example, hashVersion: 3) against an existing v1/v2 cursor, this branch ignores the unsupported source version and successfully recomputes hashes using the stored version. The writer can then append history before createImportCursor eventually throws while hashing with version 3, so applyHistoryImport returns indeterminate with history and cursor out of sync instead of refusing before any write. Validate replayHashVersion as supported before recomputation.
AGENTS.md reference: packages/shared/src/session-data/AGENTS.md:L33-L38
Useful? React with 👍 / 👎.
Related issue
Same-repository branch: no intake Issue required. Supersedes PR #584 (closed unmerged).
Problem / pressure
Two storage-layer defects in session history:
schema.Any({ defaultLoroText: true }), so every new metadata string (toolCallId,status,title,kind,locations[].path, nestedcommand/args/cwd, stepcommand) became aLoroTextcontainer — thousands of never-streamed containers per long session. Measured on unmodified main 375c4c7: all of those were Text containers.packages/shared/src/session-data, a stored cursor written by an old client (v1 hashes) would be compared against new hashes, manufacturingprefix_mismatchon an unchanged transcript.This re-implements the necessary parts of #584 on current main; #584 was closed unmerged and its CLI-side ownership is obsolete after #376. #586 is untouched.
Summary
Commit 1 — storage policy (
e8c11be2):schema.ts: item + nested catchalls default to primitive; streaming fields declared explicitly viastorageSchemahints (text/thought text, plan markdown, tool contenttext/output+ nestedcontent.text, worktree stepoutput). DiffoldText/newText,command,args,cwd,path,terminalId,inputscalars,steps[].commandstay primitive.Commit 2 — versioned import in the current shared owner (
9c773955):packages/shared/src/session-data/history-import.ts:HASH_VERSION_V1/V2; frozen v1 canonicalization pinned by a fixed byte fixture; v2 canonical item form so a sealed tool_call skeleton and the full call it came from hash identically. Documented tradeoff: v2 excludes tool payload (content/rawInput/rawOutput) from identity, so tool-output-only source drift does not by itself trigger a refresh.cursor.hashVersionversionsimportedTurnHashes;ExternalAcpHistorySyncMeta.hashVersionversionsreplayDigest; the stored baseline records its own version; absent means v1 (tested with a genuine pre-version shape — nohashVersionfield anywhere — not a fabricated one).HistoryImportCursorSchemaretainshashVersionacross the Zod strip boundary; the planner andapplyHistoryImportrecompute replay hashes in the stored version when they differ; an unknown version or missing replay history refuses before any write; a cursor failure after the history write remains indeterminate; a metadata-only conflict marker never upgrades the cursor by implication.Visual explanation
Storage:
Any(defaultLoroText: false)+ explicitstorageSchemahints is a writer-side layout. RawMirror.setStateis not a history writer, so hints cannot diverge from it; readers are unaffected.Before / after
LoroTextcontainerTest plan
cc7ec54e218e6c9570af864fa57d76ee5677fcd0897c1b4919564e969f585935).packages/shared/tests/history-storage-policy.test.ts(10 tests): full field matrix incl. empty values/arrays and unknown new fields, CID preservation on streaming edits, legacy Text + primitive layouts, malformed stored siblings, two-peer convergence, read-only open.applyHistoryImport(genuine unversioned v1 cursor+baseline upgrade, untracked suffix conflict, already-resolved refusal, cursor failure stays indeterminate); realSessionDocumentwriter level (pre-version client shape accepted after upgrade).tests/worktree-gc.test.ts, reproduces identically on unmodified main in this worktree environment); components 3616 passed; electron 112 passed;test:scriptspassed.check:code-collab-imports/check:platform-boundaries/check:public-boundarypass; docs check exit 0 (packages/shared/AGENTS.md8040/8192 bytes).NODE_OPTIONS=--max-old-space-size=2048 pnpm --filter lody buildOK.Limits
refpayload fetch, hooks, UI) is not implemented; v2 is verified against synthetic fixtures only.decideHistoryRefresh(absent = already in the stored version) is reachable only by legacy test doubles comparing opaque same-version hashes; production always carries the requiredHistoryImportReplay.hashVersionthroughplanHistoryImport.Review focus
hashVersionfield anywhere and baselines computed from actual stored content.storageSchemahint diverging from rawMirror.setState— HistoryWriter is the only history writer and hints are inert for readers.