From 58b74cfeab157483eef8a9e4ed8f4b683eecb34d Mon Sep 17 00:00:00 2001 From: Luyu Cheng <2239547+chengluyu@users.noreply.github.com> Date: Mon, 31 Aug 2026 06:41:56 +0800 Subject: [PATCH] fix(transcript): preserve turn prompt identity (#3377) * fix(transcript): preserve turn prompt identity * fix(transcript): backfill active prompt identity * fix(transcript): seed late-bound turn identity * fix(transcript): persist turn prompt identity * fix(transcript): skip undone prompt identities * fix(transcript): hide undone continuation turns * docs(agent-core): update wire manifest * fix(transcript): match cold turns by prompt identity * fix(transcript): match internal prompt turns by origin * fix(transcript): align undo anchors with context * fix(transcript): prioritize cold turn matches * fix(transcript): retire unmatched turn boundaries * style(transcript): align cold matcher --- .changeset/fix-turn-prompt-identity.md | 5 + .../agent-core-v2/docs/wire-manifest.d.ts | 1 + .../src/agent/loop/loopService.ts | 8 +- .../src/agent/loop/turnEvents.ts | 1 + .../agent-core-v2/src/agent/loop/turnOps.ts | 8 +- .../test/agent/loop/loop.test.ts | 12 +- .../test/app/config/config.test.ts | 8 +- .../test/features/plan/plan.test.ts | 8 +- packages/agent-core-v2/test/tool/tool.test.ts | 8 +- .../src/services/transcript/coreBinding.ts | 7 + .../src/services/transcript/coreEventMap.ts | 19 ++ .../services/transcript/transcriptService.ts | 4 + .../test/services/transcript.test.ts | 81 ++++++- packages/transcript/src/contract/schema.ts | 1 + packages/transcript/src/history/foldFacts.ts | 184 +++++++++++++- packages/transcript/src/history/groupTurns.ts | 30 ++- packages/transcript/src/model/turn.ts | 1 + packages/transcript/src/ops/apply.ts | 1 + packages/transcript/test/layers.test.ts | 224 +++++++++++++++++- 19 files changed, 566 insertions(+), 45 deletions(-) create mode 100644 .changeset/fix-turn-prompt-identity.md diff --git a/.changeset/fix-turn-prompt-identity.md b/.changeset/fix-turn-prompt-identity.md new file mode 100644 index 000000000..6eaaa3bb2 --- /dev/null +++ b/.changeset/fix-turn-prompt-identity.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Fix duplicate user messages in transcript clients. diff --git a/packages/agent-core-v2/docs/wire-manifest.d.ts b/packages/agent-core-v2/docs/wire-manifest.d.ts index ff1de49f7..ba37d2d2b 100644 --- a/packages/agent-core-v2/docs/wire-manifest.d.ts +++ b/packages/agent-core-v2/docs/wire-manifest.d.ts @@ -791,6 +791,7 @@ interface TurnPromptPayload { input: readonly ContentPart[]; /** PromptOrigin */ origin: 'user' | 'skill_activation' | 'plugin_command' | 'injection' | 'shell_command' | 'compaction_summary' | 'system_trigger' | 'task' | 'cron_job' | 'cron_missed' | 'hook_result' | 'retry'; + promptId?: string; } /** diff --git a/packages/agent-core-v2/src/agent/loop/loopService.ts b/packages/agent-core-v2/src/agent/loop/loopService.ts index 1f33dd26b..3c7ad8907 100644 --- a/packages/agent-core-v2/src/agent/loop/loopService.ts +++ b/packages/agent-core-v2/src/agent/loop/loopService.ts @@ -465,7 +465,12 @@ export class AgentLoopService extends Disposable implements IAgentLoopService { private startTurn(job: TurnJob): void { const origin = job.seed.origin; void this.dispatcher.dispatch( - new TurnPrompt({ agentId: this.scopeContext.agentId, input: job.seed.input, origin }), + new TurnPrompt({ + agentId: this.scopeContext.agentId, + input: job.seed.input, + origin, + promptId: job.seed.promptId, + }), ); job.turn.state = 'running'; this.activeTurnJob = job; @@ -473,6 +478,7 @@ export class AgentLoopService extends Disposable implements IAgentLoopService { new TurnStarted({ agentId: this.scopeContext.agentId, turnId: job.turn.id, + promptId: job.seed.promptId, origin, prompt: isDisplayablePromptOrigin(origin) ? turnPromptText(job.seed.input, origin) : undefined, promptAttachments: turnPromptAttachments(job.seed.input, origin), diff --git a/packages/agent-core-v2/src/agent/loop/turnEvents.ts b/packages/agent-core-v2/src/agent/loop/turnEvents.ts index 3dbdd396b..1199c3ebc 100644 --- a/packages/agent-core-v2/src/agent/loop/turnEvents.ts +++ b/packages/agent-core-v2/src/agent/loop/turnEvents.ts @@ -31,6 +31,7 @@ export type TurnPromptAttachment = export interface TurnStartedPayload { readonly agentId: string; readonly turnId: number; + readonly promptId?: string; readonly origin: PromptOrigin; readonly prompt?: string; readonly promptAttachments?: readonly TurnPromptAttachment[]; diff --git a/packages/agent-core-v2/src/agent/loop/turnOps.ts b/packages/agent-core-v2/src/agent/loop/turnOps.ts index 710003dcc..95029d642 100644 --- a/packages/agent-core-v2/src/agent/loop/turnOps.ts +++ b/packages/agent-core-v2/src/agent/loop/turnOps.ts @@ -33,7 +33,12 @@ const turnInputShape = { origin: z.custom(), }; -const turnPromptSchema = z.object(turnInputShape); +const turnPromptSchema = z.object({ + agentId: z.string(), + input: z.custom(), + origin: z.custom(), + promptId: z.string().optional(), +}); export class TurnPrompt extends AgentEvent2> { static override readonly type = 'turn.prompt'; @@ -44,6 +49,7 @@ export interface TurnPrompt { readonly agentId: string; readonly input: readonly ContentPart[]; readonly origin: PromptOrigin; + readonly promptId?: string; } const turnSteerSchema = z.object(turnInputShape); diff --git a/packages/agent-core-v2/test/agent/loop/loop.test.ts b/packages/agent-core-v2/test/agent/loop/loop.test.ts index ec7138704..7c19d9663 100644 --- a/packages/agent-core-v2/test/agent/loop/loop.test.ts +++ b/packages/agent-core-v2/test/agent/loop/loop.test.ts @@ -72,8 +72,8 @@ describe('Agent loop', () => { [wire] prompt.accepted { "agentId": "main", "promptId": "", "content": [ { "type": "text", "text": "Hello" } ], "time": "