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(