Skip to content

feat: tolerate sealed session-history shapes in all readers (phase 2a) - #359

Open
zxch3n wants to merge 2 commits into
session/8336142afrom
feat/reader-side-session-history-shapes
Open

feat: tolerate sealed session-history shapes in all readers (phase 2a)#359
zxch3n wants to merge 2 commits into
session/8336142afrom
feat/reader-side-session-history-shapes

Conversation

@zxch3n

@zxch3n zxch3n commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

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 derived summary and an optional live streaming 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

  • Loro schema validation (packages/shared/src/schema.ts): historyMessageItemSchema.validate accepts a tool_call with a valid ref and no toolCallId/content; sessionHistorySchema declares optional summary (schema.Any) and live (LoroMap { kind: 'text'|'thought', text: LoroText }); sessionExternalHistoryCursorDocSchema gains hashVersion.
  • Shared types (packages/shared/src/ai.ts): ref?: ToolCallRef on tool_call; new ToolCallRef, ToolCallPayload, TurnSummary; ExternalAcpHistorySyncMeta.hashVersion.
  • Renderer guards (packages/components/.../message-content-guards.ts + new tool-call-skeleton.ts): isMessageContent accepts skeletons; runtime guards isToolCallRef/isToolCallSkeleton/getToolCallStableId — no casts around the type-required toolCallId.
  • Tool-call renderers: ToolCallCard renders skeletons title-only (no output/diff/terminal section) plus an "Execution details are stored on " placeholder (i18n'd, machine name via getMachineMetaByIdAtomFamily) driven by the new useToolCallPayload(ref) hook — signature { state: 'idle'|'loading'|'ready'|'unavailable', value?: ToolCallPayload }, stubbed unavailable until the Machine RPC task lands. Activity counting in assistant-turn-render-blocks.ts classifies from kind/status alone. message-copy.ts (tool calls were already non-copyable → title-only) and assistant-edited-files.tsx (derives from the turn-level fileDiff evidence store, never from tool_call.content) needed no changes — covered by tests.
  • history-apply (packages/shared/src/acp/history-apply.ts): tool_call_update merging by toolCallId unchanged 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.
  • Canonical turn hash (packages/history-import): HASH_VERSION = 2 — tool_call canonicalizes to exactly { type, kind?, title?, status, locations? } (drops content/rawInput/rawOutput/toolCallId/ref + runtime annotations; full dropped-key list documented above VOLATILE_ITEM_KEYS_V2), text/thought → { type, text }, other items minus volatile keys. v1 kept for recomputing old cursors. decideHistoryRefresh/decideHistoryConflictResolution compare in the stored cursor's version (absent hashVersion = v1), recomputing the replay's hashes when versions differ, so an upgrade never produces a false sync_conflict. materializeReplay emits v2 and records the version; buildExternalHistoryMeta copies it into the sync meta; the CLI sync service (local-project-history-sync-service.ts) writes hashVersion into the session doc cursor and hashes local turns in the stored version.
  • CLI readers (apps/cli): session history/session show and MCP lody_session_history skip tool_call items entirely (skeletons included — covered by new tests); session export markdown/JSON render skeletons with title + "execution details stored on the origin machine" note and toolCallId: null + ref instead of crashing on undefined.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 (getToolCallStableId instead of raw toolCallId); ToolCallCard (payload overlay via useToolCallPayload, effectiveContent/effectiveRawOutput locals, skeleton placeholder branch); new ToolCallPayloadStoredRow component. List/virtualization code untouched.

Verification

  • pnpm check green (incl. public-boundary guard); pnpm format run.
  • New tests: packages/shared/tests/session-history-shapes.test.ts (old shape round-trips unchanged, no summary/live containers materialize for old-shape data = no writer byte change; new shape validates; malformed skeletons still rejected), 5 skeleton tests in acp-history-apply.test.ts, 12 tests in packages/history-import/tests/hash-versions.test.ts (full vs skeleton → equal v2 hashes; v1 cursor matches after upgrade; mixed-version prefix append), 10 tests in packages/components/tests/tool-call-skeleton.test.ts, CLI export byte-lock + skeleton tests, and mixed-version refresh tests in apps/cli/tests/local-project-history-sync-service.test.ts.
  • Full suites: @lody/shared 1020, @lody/components 3047, @lody/history-import 45, lody CLI 2444 — all green.
  • Also fixes a pre-existing base-branch failure: benchmarks/open-conversation.bench.ts used the tinybench v3 result API while the lockfile pins 2.9, so pnpm check was already red on session/8336142a (separate first commit).

Known gaps (by design)

  • useToolCallPayload always returns unavailable until the Machine RPC task lands; skeletons render the placeholder line.
  • A sealed Cron/ScheduleWakeup skeleton is invisible to the scheduled-tasks panel (it reads toolName/schedulingTimeZone, which skeletons omit) until payload fetch exists — no crash, behavior consequence of the skeleton shape itself.
  • Storybook has no skeleton story yet (worth adding when the writer lands).

…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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines 133 to 136
await sessionDoc.setExternalHistoryCursor({
importedTurnHashes: [...turnHashes],
hashVersion,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant