feat: tolerate sealed session-history shapes in all readers (phase 2a) - #359
feat: tolerate sealed session-history shapes in all readers (phase 2a)#359zxch3n wants to merge 2 commits into
Conversation
…nybench 2.9 result API The bench read task.result.latency/throughput (tinybench v3 shape) while the lockfile pins tinybench 2.9, whose TaskResult exposes mean/p99/hz/samples directly. pnpm check failed on this before this branch's changes. Model: kimi-code/k3
Reader-side preparation for the upcoming sealed-turn storage: sealed turns
will keep tool_call items as skeletons (kind/status/title/locations/ref)
with the execution payload on the origin machine, and turns will gain a
derived summary plus a live streaming container. Nothing here changes what
any current writer persists.
- @lody/shared types: tool_call gains ref?: ToolCallRef; new ToolCallRef,
ToolCallPayload and TurnSummary types.
- Session doc schema: history entries declare summary (Any) and live
(LoroMap { kind, text }), both optional; the item validator accepts a
tool_call with a valid ref and no toolCallId/content. The external-history
cursor and ExternalAcpHistorySyncMeta gain hashVersion. Old-shape docs
round-trip unchanged (tests/session-history-shapes.test.ts).
- @lody/components: message-content-guards accept skeletons;
tool-call-skeleton.ts owns the runtime guards; ToolCallCard renders
skeletons title-only with an "Execution details are stored on <machine>"
line driven by the new useToolCallPayload hook (stubbed 'unavailable'
until the Machine RPC lands); activity counting works from kind/status
alone.
- history-apply: tool_call_update merging keeps live-turn semantics and no
longer crashes or false-matches on sealed skeleton items.
- @lody/history-import: HASH_VERSION=2 canonical hashing (full and skeleton
tool_calls hash equal), v1 kept for stored cursors; refresh/conflict
decisions compare in the stored cursor's version so an upgrade never
produces a false sync_conflict; the CLI sync service records hashVersion
in meta and the session doc cursor.
- CLI export/session readers render skeletons with title + a note instead
of crashing on missing toolCallId; old-shape export output is
byte-identical (locked by tests).
Model: kimi-code/k3
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e4b8e0b444
ℹ️ 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".
| await sessionDoc.setExternalHistoryCursor({ | ||
| importedTurnHashes: [...turnHashes], | ||
| hashVersion, | ||
| }); |
There was a problem hiding this comment.
Keep cursor hashes paired with their version
If the cursor write succeeds but the following doc-meta update fails or the process exits, the session is left with v2 importedTurnHashes in the cursor and a v1 externalHistory.hashVersion. On the next source update, readSessionImportedTurnHashes returns those v2 hashes while decideHistoryRefresh interprets them using the meta's v1 version, so the valid imported prefix is reported as prefix_mismatch and the durable session is incorrectly marked sync_conflict. Read and compare the cursor's hashVersion together with its hashes, or otherwise make the version transition recoverable across the two non-atomic writes.
Useful? React with 👍 / 👎.
What
Reader-side preparation for the upcoming sealed-turn session-history storage. Sealed turns will store tool_call items as skeletons
{ type: 'tool_call', kind, status, title?, locations?, ref }with the execution payload (content/rawInput/rawOutput) kept in the origin machine's local store, and turns will gain an optional derivedsummaryand an optionallivestreaming container. Nothing here changes what any current writer persists — this PR only makes every reader accept both the old and the new shape.Readers that now accept the skeleton / new shape
packages/shared/src/schema.ts):historyMessageItemSchema.validateaccepts atool_callwith a validrefand notoolCallId/content;sessionHistorySchemadeclares optionalsummary(schema.Any) andlive(LoroMap { kind: 'text'|'thought', text: LoroText });sessionExternalHistoryCursorDocSchemagainshashVersion.packages/shared/src/ai.ts):ref?: ToolCallRefon tool_call; newToolCallRef,ToolCallPayload,TurnSummary;ExternalAcpHistorySyncMeta.hashVersion.packages/components/.../message-content-guards.ts+ newtool-call-skeleton.ts):isMessageContentaccepts skeletons; runtime guardsisToolCallRef/isToolCallSkeleton/getToolCallStableId— no casts around the type-requiredtoolCallId.ToolCallCardrenders skeletons title-only (no output/diff/terminal section) plus an "Execution details are stored on " placeholder (i18n'd, machine name viagetMachineMetaByIdAtomFamily) driven by the newuseToolCallPayload(ref)hook — signature{ state: 'idle'|'loading'|'ready'|'unavailable', value?: ToolCallPayload }, stubbedunavailableuntil the Machine RPC task lands. Activity counting inassistant-turn-render-blocks.tsclassifies fromkind/statusalone.message-copy.ts(tool calls were already non-copyable → title-only) andassistant-edited-files.tsx(derives from the turn-levelfileDiffevidence store, never fromtool_call.content) needed no changes — covered by tests.packages/shared/src/acp/history-apply.ts):tool_call_updatemerging bytoolCallIdunchanged for live turns; merge-target lookups and the batch index skip id-less skeleton items so they can neither crash nor false-match (undefined === undefined).apps/cli/src/lib/acp/*audited — all history reads already null-safe, no changes needed.packages/history-import):HASH_VERSION = 2— tool_call canonicalizes to exactly{ type, kind?, title?, status, locations? }(dropscontent/rawInput/rawOutput/toolCallId/ref+ runtime annotations; full dropped-key list documented aboveVOLATILE_ITEM_KEYS_V2), text/thought →{ type, text }, other items minus volatile keys. v1 kept for recomputing old cursors.decideHistoryRefresh/decideHistoryConflictResolutioncompare in the stored cursor's version (absenthashVersion= v1), recomputing the replay's hashes when versions differ, so an upgrade never produces a falsesync_conflict.materializeReplayemits v2 and records the version;buildExternalHistoryMetacopies it into the sync meta; the CLI sync service (local-project-history-sync-service.ts) writeshashVersioninto the session doc cursor and hashes local turns in the stored version.apps/cli):session history/session showand MCPlody_session_historyskip tool_call items entirely (skeletons included — covered by new tests);session exportmarkdown/JSON render skeletons with title + "execution details stored on the origin machine" note andtoolCallId: null+refinstead of crashing onundefined.replace.view.tsx files/hunks touched
Only
packages/components/src/components/ai-gui/view.tsx, confined to tool-call rendering: imports; the expanded activity-detail row key (getToolCallStableIdinstead of rawtoolCallId);ToolCallCard(payload overlay viauseToolCallPayload,effectiveContent/effectiveRawOutputlocals, skeleton placeholder branch); newToolCallPayloadStoredRowcomponent. List/virtualization code untouched.Verification
pnpm checkgreen (incl. public-boundary guard);pnpm formatrun.packages/shared/tests/session-history-shapes.test.ts(old shape round-trips unchanged, nosummary/livecontainers materialize for old-shape data = no writer byte change; new shape validates; malformed skeletons still rejected), 5 skeleton tests inacp-history-apply.test.ts, 12 tests inpackages/history-import/tests/hash-versions.test.ts(full vs skeleton → equal v2 hashes; v1 cursor matches after upgrade; mixed-version prefix append), 10 tests inpackages/components/tests/tool-call-skeleton.test.ts, CLI export byte-lock + skeleton tests, and mixed-version refresh tests inapps/cli/tests/local-project-history-sync-service.test.ts.benchmarks/open-conversation.bench.tsused the tinybench v3 result API while the lockfile pins 2.9, sopnpm checkwas already red onsession/8336142a(separate first commit).Known gaps (by design)
useToolCallPayloadalways returnsunavailableuntil the Machine RPC task lands; skeletons render the placeholder line.toolName/schedulingTimeZone, which skeletons omit) until payload fetch exists — no crash, behavior consequence of the skeleton shape itself.