From 46e847425e68bfa156b7d5b32c09a0ccc6b3bdfc Mon Sep 17 00:00:00 2001 From: liruifengv Date: Tue, 21 Jul 2026 11:38:24 +0800 Subject: [PATCH] fix(tui): hide system-trigger messages in resume replay Goal mode stores its per-turn continuation prompt as a user message with a system_trigger origin. Live rendering never shows these model-facing messages, but resume replay only filtered three specific system_trigger names, so the continuation prompt leaked into the transcript as a user message. Skip all system_trigger user messages during replay, matching the live path and the markdown exporter, and drop the two goal-specific filters this makes redundant. --- .changeset/goal-replay-leak.md | 5 +++ .../src/tui/controllers/session-replay.ts | 27 +++------------- .../kimi-code/test/tui/message-replay.test.ts | 32 +++++++++++++++++++ 3 files changed, 41 insertions(+), 23 deletions(-) create mode 100644 .changeset/goal-replay-leak.md diff --git a/.changeset/goal-replay-leak.md b/.changeset/goal-replay-leak.md new file mode 100644 index 0000000000..20bd8f2e7e --- /dev/null +++ b/.changeset/goal-replay-leak.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Fix goal mode continuation prompts leaking into the transcript when resuming a session. diff --git a/apps/kimi-code/src/tui/controllers/session-replay.ts b/apps/kimi-code/src/tui/controllers/session-replay.ts index ad6e82358b..a20e8a513a 100644 --- a/apps/kimi-code/src/tui/controllers/session-replay.ts +++ b/apps/kimi-code/src/tui/controllers/session-replay.ts @@ -301,17 +301,10 @@ export class SessionReplayRenderer { this.renderCronMissed(context, message); return; } - if (isGoalForkClearedSystemReminder(message)) { - return; - } - const goalReminder = goalOutcomeReminderFromSystemMessage(message); - if (goalReminder !== null) { - if (goalReminder !== undefined) { - this.flushAssistant(context); - this.host.appendTranscriptEntry( - replayEntry(context, 'assistant', goalReminder, 'markdown'), - ); - } + // System-trigger messages (goal continuation prompts, goal outcome + // reminders, stop-hook reasons, …) are model-facing only: the live event + // stream never renders them, so replay must not leak them either. + if (message.origin?.kind === 'system_trigger') { return; } @@ -741,18 +734,6 @@ function isModelBlockedGoalLifecycle(change: GoalReplayLifecycleChange): boolean return change.status === 'blocked' && change.actor === 'model'; } -function goalOutcomeReminderFromSystemMessage(message: ContextMessage): string | undefined | null { - if (message.origin?.kind !== 'system_trigger') return null; - if (message.origin.name !== 'goal_completion' && message.origin.name !== 'goal_blocked') { - return null; - } - return undefined; -} - -function isGoalForkClearedSystemReminder(message: ContextMessage): boolean { - return message.origin?.kind === 'system_trigger' && message.origin.name === 'goal_fork_cleared'; -} - function extractCronPrompt(text: string): string { const open = '\n'; const close = '\n'; diff --git a/apps/kimi-code/test/tui/message-replay.test.ts b/apps/kimi-code/test/tui/message-replay.test.ts index f3e5faa253..26d8ddc450 100644 --- a/apps/kimi-code/test/tui/message-replay.test.ts +++ b/apps/kimi-code/test/tui/message-replay.test.ts @@ -501,6 +501,38 @@ describe('KimiTUI resume message replay', () => { expect(content).not.toContain('Write a concise final message for the user'); }); + it('does not replay system-trigger prompts such as goal continuation as user messages', async () => { + const driver = await replayIntoDriver([ + message( + 'user', + [ + { + type: 'text', + text: 'Continue working toward the active goal. Keep the self-audit brief.', + }, + ], + { origin: { kind: 'system_trigger', name: 'goal_continuation' } }, + ), + message( + 'user', + [ + { + type: 'text', + text: '\nThe goal was cancelled.\n', + }, + ], + { origin: { kind: 'system_trigger', name: 'goal_cancelled' } }, + ), + message('assistant', [{ type: 'text', text: 'Working on it.' }]), + ]); + + expect(driver.state.transcriptEntries.filter((entry) => entry.kind === 'user')).toEqual([]); + const transcript = stripAnsi(driver.state.transcriptContainer.render(140).join('\n')); + expect(transcript).not.toContain('Continue working toward the active goal'); + expect(transcript).not.toContain('The goal was cancelled'); + expect(transcript).toContain('Working on it.'); + }); + it('does not replay the model-blocked lifecycle marker when the follow-up is replayed', async () => { const driver = await replayIntoDriver([ goalReplay(