Skip to content

fix(transcript): fill cold snapshot turn/step times and prompts from wire records - #4060

Open
liruifengv wants to merge 8 commits into
mainfrom
fix/cold-transcript-facts
Open

liruifengv wants to merge 8 commits into
mainfrom
fix/cold-transcript-facts

Conversation

@liruifengv

@liruifengv liruifengv commented Sep 28, 2026 •

Copy link
Copy Markdown
Collaborator

Requirement or Bug

Sessions read from disk — reopened after a server restart, or freshly forked — lose turn startedAt, step times, and the entire prompts list in their transcripts, so message times render wrong (the first turn falls back to the session's createdAt, e.g. the fork moment) and prompt metadata is missing.

Bug Reproduction Steps

  1. Pick any session that is not currently live (after a server restart, or a freshly forked session).
  2. GET /api/v1/sessions/{id}/transcript?agent_id=main returns turns with startedAt = null and prompts = [] (only endedAt is present).
  3. In the desktop/web client, user messages show no time, and the first turn shows the session's createdAt — for a fork, the fork moment — instead of the real message time.

Root Cause

Cold snapshots are built from a lossy projection: readColdSnapshot reduces wire records to context messages and discards record times; groupMessagesIntoSnapshot has no time fields and always returns prompts: []; and foldWireRecordFacts only backfilled endedAt. Live sessions mask the gap because times and prompts arrive via live ops. This is a fundamental fix, not a workaround: the missing facts are folded from durable wire records at the layer already designated for record-level facts (foldWireRecordFacts, which already backfills endedAt, interrupted steps, tasks, interactions, todos, and markers the same way).

Code Changes

All behavioral changes are inside foldWireRecordFacts (packages/transcript/src/history/foldFacts.ts); groupTurns.ts only exports two existing helpers for reuse.

 foldWireRecordFacts(records, base)
   ...existing endedAt / interrupted-step facts
+  turn startedAt: turn.prompt record time (fallback: agent.turn.started), first-wins;
+    start-only turns recorded via agent.turn.started are mapped too
+  step times: step.begin/step.end loop events (first-wins begin, last-wins end),
+    realigned to visible steps using the loop fold's visibility rules (vacuous
+    failed attempts drop out of the cold tree while wire step numbers advance),
+    applied before turn.step.interrupted so the interrupted endedAt still wins;
+    synthesized interrupted steps also receive their step.begin time
+  prompts: drafts from user-origin turn.prompt / turn.steer (content projected
+    to the public message shape, bundled skill/hook blocks stripped, steer input
+    assigned only for single-child steers), then prompt.completed / aborted /
+    steered applied (buffered — prompt.steered can precede turn.steer in the
+    wire); children missing a turn.steer are synthesized from prompt.steered,
+    terminal prompts without a turn are synthesized from completions, prompts
+    of undone turns dropped (legacy turn.steer without turnId binds to the
+    active turn)
  • The items-patch activation condition now includes the startedAt/step-time maps, so turns with only a start record (e.g. crashed mid-turn) also get patched.
  • Prompt status defaults: prompt.completed/aborted win; otherwise the owning turn's turn.ended reason maps to the prompt status (completed → completed, cancelled → aborted, failed/blocked → same), else running.
  • Non-user tracked turns (subagent, skill-activation, plugin-command, task) no longer grow content-bearing prompts in the cold snapshot, matching the live projector which publishes prompt content events only for user-origin prompts; their terminal facts still appear via completions.
  • Prompt createdAt is the original submission time: turn.prompt persists createdAt, prompt.steered persists per-child createdAts, and prompt.completed/prompt.aborted persist createdAt (all additive optional wire fields, regenerated wire-manifest.d.ts), so queued prompts — including ones rejected or cancelled before launch — no longer report the turn launch or rejection time after a restart; records written by older versions fall back to the record time.
  • Wire changes in agent-core-v2 are limited to the additive optional fields above; the session event broadcaster strips these durable-only timestamps from prompt.steered/prompt.completed/prompt.aborted so the public websocket contract is unchanged, and loop snapshots asserting wire logs were updated accordingly.
  • Completion-only prompts are associated with their owning raw turn via turnPromptIds, so undoing a user-slash skill/plugin turn also drops its terminal prompt entry.
  • No changes to the snapshot contract, kap-server, or any client.

Impact Scope

  • Every cold-snapshot consumer: session reopen after restart, forked sessions, undo rebuild, post-turn heal, kimi-inspect transcript views.
  • Live sessions: unchanged (live ops already provide these fields; turn heal prefers live values).
  • Tests: layers.test.ts +10 cases; transcript.test.ts cold-snapshot e2e case. transcript 135/135, kap-server suite green, root lint + typecheck green. Also verified against real session data: a forked session's turns recover their original times (not the fork moment), and legacy sessions from the agent.turn.started era map correctly.

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked a related issue (external PRs: issue must have a maintainer's /approve).
  • I have added tests that prove my feature works.
  • Ran gen-changesets skill, or this PR needs no changeset.
  • Ran gen-docs skill, or this PR needs no doc update.

…wire records

readColdSnapshot dropped turn startedAt, step times, and the prompts
list for sessions read from the wire, so reopened or forked sessions
showed wrong message times and lost prompt metadata.
foldWireRecordFacts now folds them from durable records: turn startedAt
from turn.prompt (falling back to agent.turn.started), step times from
step.begin/step.end loop events, and prompt entries from
turn.prompt/turn.steer updated by prompt.completed/aborted/steered,
keeping only prompts whose turn survives in the snapshot.
Copilot AI balanced review requested due to automatic review settings September 28, 2026 07:08
@changeset-bot

changeset-bot Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: bbc9661

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@moonshot-ai/kimi-code Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@liruifengv
liruifengv marked this pull request as ready for review October 8, 2026 07:17
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-15T12:11:55.638603Z bbc9661 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@liruifengv

Copy link
Copy Markdown
Collaborator Author

@codex

@pkg-pr-new

pkg-pr-new Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
pnpm dlx https://pkg.pr.new/@moonshot-ai/kimi-code@bbc9661
npx https://pkg.pr.new/@moonshot-ai/kimi-code@bbc9661

commit: bbc9661

@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: dd97086e20

ℹ️ 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 thread packages/transcript/src/history/foldFacts.ts Outdated
Comment thread packages/transcript/src/history/foldFacts.ts Outdated
Comment thread packages/transcript/src/history/foldFacts.ts
Comment thread packages/transcript/src/history/foldFacts.ts
Comment thread packages/transcript/src/history/foldFacts.ts
Comment thread packages/transcript/src/history/foldFacts.ts
Comment thread packages/transcript/src/history/foldFacts.ts
Comment thread packages/transcript/src/history/foldFacts.ts Outdated
Comment thread packages/transcript/src/history/foldFacts.ts
Comment thread packages/transcript/src/history/foldFacts.ts Outdated
…the live projector

Address review on the cold-snapshot facts fold:

- project prompt content to the public message shape and strip
  bundled skill/hook blocks, matching projectPromptContentParts
- restrict turn.prompt drafts to user-origin prompts; non-user
  tracked prompts still get terminal facts from completions
- assign steer input only for single-child steers; synthesize
  steered children from prompt.steered alone and terminal prompts
  from completions without a turn draft
- derive the fallback prompt status from the turn.ended reason
- map start-only turns recorded via agent.turn.started
- fill begin times on synthesized interrupted steps
- associate legacy steer records without a turnId with the active
  turn so context undo hides their prompt facts
- realign step times with visible steps after vacuous failed
  attempts compact the cold tree
@liruifengv

Copy link
Copy Markdown
Collaborator Author

@codex review

@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: 2cf8a5ce09

ℹ️ 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 thread packages/transcript/src/history/foldFacts.ts Outdated
Comment thread packages/transcript/src/history/foldFacts.ts Outdated
Comment thread packages/transcript/src/history/foldFacts.ts
…s in cold facts

- persist the submission createdAt on turn.prompt and per-child
  createdAts on prompt.steered (additive wire fields), and prefer
  them over record times when reconstructing prompts so queued
  prompts no longer report the turn launch time after a restart
- remap turn.step.interrupted through the kept wire-step mapping so
  an interrupted retry lands on the visible step instead of
  synthesizing a stray empty step
- match the context fold's vacuity predicate: a hidden empty
  thinking part is vacuous
@liruifengv

Copy link
Copy Markdown
Collaborator Author

@codex review

@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: f08afbce26

ℹ️ 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 thread packages/transcript/src/history/foldFacts.ts
Comment thread packages/agent-core-v2/src/agent/loop/loopService.ts
Comment thread packages/transcript/src/history/foldFacts.ts
…and strip them at broadcast

- add an optional createdAt to prompt.completed/prompt.aborted so
  queued prompts rejected or cancelled before launch keep their
  submission time in cold snapshots
- strip the durable-only createdAts/createdAt fields from
  prompt.steered/prompt.completed/prompt.aborted in the session
  event broadcaster so the public websocket contract stays unchanged
- associate completion-only prompts with their owning raw turn via
  turnPromptIds so a context undo hides terminal non-user prompts
  together with the command turn
@liruifengv

Copy link
Copy Markdown
Collaborator Author

@codex review

@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: 2f7892cf56

ℹ️ 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 thread packages/transcript/src/history/foldFacts.ts Outdated
Comment thread packages/transcript/src/history/foldFacts.ts
Comment thread packages/transcript/src/history/foldFacts.ts
…and identity

- restrict turn.steer draft content and client metadata to
  user-origin steers, matching the live projector which only has
  submitted content for user prompts; other origins keep only the
  terminal steer fact
- stop copying the synthetic merged message id into every steered
  child: single-child steers keep the record messageId while merged
  steers fall back to each child's own prompt id, like the live
  projector which preserves each submitted userMessageId
@liruifengv

Copy link
Copy Markdown
Collaborator Author

@codex review

@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: 521724e374

ℹ️ 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 thread packages/transcript/src/history/foldFacts.ts Outdated
Comment thread packages/transcript/src/history/foldFacts.ts
Comment thread packages/transcript/src/history/foldFacts.ts
A tracked non-user active prompt (skill/plugin command) creates no
turn.prompt draft, so a restart during the turn lost the prompt
entirely and a later completion rebuilt it without steeredAt.
Upsert the active prompt when applying prompt.steered, the same way
the live projector does: steered content as the content fallback,
steeredAt as the createdAt fallback, running status until a
completion lands, and the owning raw turn resolved through
turnPromptIds so undo visibility still applies. Children are applied
before the active upsert so the list order matches the live store,
where child submissions precede the steer events.
@liruifengv

Copy link
Copy Markdown
Collaborator Author

@codex

@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: bbc96617a9

ℹ️ 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".

) {
promptDrafts.set(payload.promptId, {
promptId: payload.promptId,
content: projectPromptContent(payload.input, payload.origin),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Persist the pre-gate prompt content

For user prompts containing a malformed, unsupported, or non-canonical image, this reconstructs content from turn.prompt.input, but that durable input has already passed through gateImageFormatParts in runPromptGate, which can replace the submitted image with an internal text notice or rewrite its data URL. The live prompt.submitted event is emitted before that gate, so desktop/web clients see the original payload while live and a different payload after restart. Fresh evidence beyond the earlier projection fix is this irreversible pre-persistence gate rewrite; persist the pre-gate content or reconstruct it from a durable pre-gate record.

AGENTS.md reference: AGENTS.md:L118-L120

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Acknowledged — the durable turn.prompt.input is indeed post-gate content while prompt.submitted is pre-gate, so gate-rewritten images diverge after a restart. After a scope review we are keeping this PR to its core fix (restoring missing turn/step times and the prompts list on cold snapshots) and stopping the Codex-driven expansion here, since this and the remaining findings either require more durable-log format changes or address narrow edge cases. Leaving this as a known follow-up (pre-gate content needs to be persisted, e.g. via a durable submission record) rather than growing this PR further.

Comment on lines +1287 to +1290
content:
steer.promptIds.length === 1
? projectPromptContent(steer.content, undefined)
: undefined,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep steer-only non-user children content-free

If a skill/plugin-origin or untracked delivery is accepted for steering and the process exits after durable prompt.steered but before the nudge emits turn.steer, this fallback has no origin and unconditionally exposes the single child's expanded/internal content. The live projector creates such a child with no content because it never received prompt.submitted. Fresh evidence after the earlier origin-gating fix is that the steer-only crash path bypasses that gate entirely; persist enough origin information to keep non-user children content-free.

AGENTS.md reference: AGENTS.md:L118-L120

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Acknowledged — the steer-only crash path can surface content for non-user children because prompt.steered carries no per-child origin. Valid, but fixing it requires persisting per-child origin on the durable steer record, and after a scope review we are keeping this PR to its core fix and deferring this (along with the other durable-format follow-ups) rather than expanding further. Noted as a known limitation of the steer-only synthesis.

interruptedSteps.has(turnId)),
interruptedSteps.has(turnId) ||
stepTimeTurns.has(turnId) ||
agentStartedTurns.has(turnId)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Exclude undone start-only turns from fallback mapping

In a legacy history containing agent.turn.started but no turn.prompt, these newly admitted candidates never advance nextTurnId; consequently, context.append_message anchors the turn at zero and a later context.undo hides no raw IDs because its loop ends at that unchanged clock. If the user then submits a replacement turn, raw turn 0 remains the first fallback candidate and its old start record is assigned to the replacement base turn. Fresh evidence beyond the earlier start-only mapping fix is that those records were added to candidate mapping without being added to undo/turn-clock accounting.

AGENTS.md reference: AGENTS.md:L118-L120

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Acknowledged — start-only agent.turn.started records were admitted to candidate mapping without joining the undo/turn-clock accounting, so a phantom turn can survive an undo and lend its start record to a replacement turn. This is a genuine edge in legacy (pre-turn.prompt) histories, but the correct fix touches the undo anchor/clock machinery, which carries more regression risk than we want to take in this PR. Deferring as a follow-up; the PR stays scoped to its core fix.

content: projectPromptContent(steer.content, undefined),
createdAt: steer.steeredAt,
rawTurnId: rawTurnIdByPromptId.get(steer.activePromptId),
status: 'running',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Derive synthesized active prompt status from its ended turn

For a tracked user-slash skill or plugin turn whose turn.prompt intentionally created no draft, steering synthesizes the active prompt here. If turn.ended is persisted and the process exits before the separately dispatched prompt.completed, the explicit running status bypasses the turn-outcome fallback below, so reopening reports a running prompt even though its turn completed, failed, or was cancelled. Fresh evidence beyond the earlier fallback-status fix is that the newly added active-prompt synthesis branch pre-populates the status instead of allowing that fallback to run.

AGENTS.md reference: AGENTS.md:L118-L120

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Acknowledged — the synthesized active prompt pre-populates running instead of letting the turn-outcome fallback apply when turn.ended landed but prompt.completed did not. Small and valid, but we are freezing this PR at its core scope after a review of the Codex-driven expansion; bundled with the other follow-ups for a subsequent PR.

@liruifengv

Copy link
Copy Markdown
Collaborator Author

@codex Thanks for the thorough review rounds — the findings so far have all been addressed and their threads resolved.

A note on scope: this PR is staying scoped to its core fix — restoring missing turn/step times and the prompts list in cold snapshots (reopened or forked sessions). We are not expanding it further.

The remaining open threads are acknowledged as valid but deferred to follow-up work, since they either require more durable-log format evolution (e.g. making prompt.submitted durable, which would carry submission-time content/metadata/message identity for every prompt at once) or touch the legacy undo/turn-clock machinery — both deserve their own PR and discussion rather than being bolted onto this one:

  • per-child client metadata on prompt.steered
  • userMessageId/content/metadata for prompts terminated before launch
  • pre-gate prompt content in turn.prompt.input
  • per-child origin for steer-only crash reconstruction
  • start-only turns and the undo turn clock
  • synthesized active prompt status pre-empting the turn-outcome fallback

The deferred items are replied to individually in their threads with the rationale.

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@liruifengv

Copy link
Copy Markdown
Collaborator Author

@codex

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🎉

Reviewed commit: bbc96617a9

ℹ️ 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".

This branch has not been deployed

No deployments
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