From 7e0e79b6bc00db5b14263ae1255fc426322aee03 Mon Sep 17 00:00:00 2001 From: callmeYe <512217680@qq.com> Date: Tue, 7 Jul 2026 15:08:28 +0800 Subject: [PATCH] fix(daemon): preserve user message source metadata (#6385) --- .../session/HistoryReplayer.ts | 1 + .../session/emitters/MessageEmitter.test.ts | 15 +++++++++ .../session/emitters/MessageEmitter.ts | 7 +++- .../src/daemon/ui/normalizer.ts | 21 ++++++++++-- .../sdk-typescript/test/unit/daemonUi.test.ts | 33 +++++++++++++++++++ .../web-shell/client/adapters/messageTypes.ts | 1 + .../adapters/transcriptToMessages.test.ts | 15 +++++++++ .../client/adapters/transcriptToMessages.ts | 5 +++ 8 files changed, 95 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/acp-integration/session/HistoryReplayer.ts b/packages/cli/src/acp-integration/session/HistoryReplayer.ts index 189d6425eb..57f68bddae 100644 --- a/packages/cli/src/acp-integration/session/HistoryReplayer.ts +++ b/packages/cli/src/acp-integration/session/HistoryReplayer.ts @@ -113,6 +113,7 @@ export class HistoryReplayer { await this.messageEmitter.emitUserMessage( displayText, record.timestamp, + record.subtype === 'cron' ? { source: 'cron' } : undefined, ); } break; diff --git a/packages/cli/src/acp-integration/session/emitters/MessageEmitter.test.ts b/packages/cli/src/acp-integration/session/emitters/MessageEmitter.test.ts index 941c131efa..3cad5acafb 100644 --- a/packages/cli/src/acp-integration/session/emitters/MessageEmitter.test.ts +++ b/packages/cli/src/acp-integration/session/emitters/MessageEmitter.test.ts @@ -53,6 +53,21 @@ describe('MessageEmitter', () => { content: { type: 'text', text: multilineText }, }); }); + + it('should include source metadata when provided', async () => { + await emitter.emitUserMessage('scheduled prompt', 1_700_000_000_000, { + source: 'cron', + }); + + expect(sendUpdateSpy).toHaveBeenCalledWith({ + sessionUpdate: 'user_message_chunk', + content: { type: 'text', text: 'scheduled prompt' }, + _meta: { + timestamp: 1_700_000_000_000, + source: 'cron', + }, + }); + }); }); describe('emitAgentMessage', () => { diff --git a/packages/cli/src/acp-integration/session/emitters/MessageEmitter.ts b/packages/cli/src/acp-integration/session/emitters/MessageEmitter.ts index 0b6149f571..43da4b4061 100644 --- a/packages/cli/src/acp-integration/session/emitters/MessageEmitter.ts +++ b/packages/cli/src/acp-integration/session/emitters/MessageEmitter.ts @@ -90,12 +90,17 @@ export class MessageEmitter extends BaseEmitter { async emitUserMessage( text: string, timestamp?: string | number, + options: { source?: string } = {}, ): Promise { const epochMs = BaseEmitter.toEpochMs(timestamp); + const _meta = { + ...(epochMs != null ? { timestamp: epochMs } : {}), + ...(options.source ? { source: options.source } : {}), + }; await this.sendUpdate({ sessionUpdate: 'user_message_chunk', content: { type: 'text', text }, - ...(epochMs != null && { _meta: { timestamp: epochMs } }), + ...(Object.keys(_meta).length > 0 ? { _meta } : {}), }); } diff --git a/packages/sdk-typescript/src/daemon/ui/normalizer.ts b/packages/sdk-typescript/src/daemon/ui/normalizer.ts index 7d73961686..d27d2b6c2c 100644 --- a/packages/sdk-typescript/src/daemon/ui/normalizer.ts +++ b/packages/sdk-typescript/src/daemon/ui/normalizer.ts @@ -534,6 +534,7 @@ function normalizeSessionUpdate( ) { return []; } + const meta = extractUpdateMeta(update); const content = update['content']; const part = extractContentPart(content); if (part) { @@ -558,13 +559,29 @@ function normalizeSessionUpdate( } if (part.kind === 'text') { return part.text - ? [{ ...base, type: 'user.text.delta', text: part.text }] + ? [ + { + ...base, + type: 'user.text.delta', + text: part.text, + ...(meta ? { meta } : {}), + }, + ] : []; } return []; } const text = getTextContent(content); - return text ? [{ ...base, type: 'user.text.delta', text }] : []; + return text + ? [ + { + ...base, + type: 'user.text.delta', + text, + ...(meta ? { meta } : {}), + }, + ] + : []; } case 'agent_message_chunk': { const text = getTextContent(update['content']); diff --git a/packages/sdk-typescript/test/unit/daemonUi.test.ts b/packages/sdk-typescript/test/unit/daemonUi.test.ts index ec8c775b6b..10c90b9fa6 100644 --- a/packages/sdk-typescript/test/unit/daemonUi.test.ts +++ b/packages/sdk-typescript/test/unit/daemonUi.test.ts @@ -522,6 +522,39 @@ describe('daemon UI normalizer and transcript reducer', () => { ).toMatchObject([{ type: 'user.text.delta', text: 'hello' }]); }); + it('preserves user message metadata on transcript blocks', () => { + const events = normalizeDaemonEvent({ + id: 23, + v: 1, + type: 'session_update', + data: { + update: { + sessionUpdate: 'user_message_chunk', + content: { type: 'text', text: 'scheduled prompt' }, + _meta: { source: 'cron' }, + }, + }, + } as const); + + expect(events).toMatchObject([ + { + type: 'user.text.delta', + text: 'scheduled prompt', + meta: { source: 'cron' }, + }, + ]); + + const state = reduceDaemonTranscriptEvents( + createDaemonTranscriptState({ now: 1 }), + events, + ); + expect(state.blocks[0]).toMatchObject({ + kind: 'user', + text: 'scheduled prompt', + meta: { source: 'cron' }, + }); + }); + it('carries user shell command metadata into user shell transcript blocks', () => { let state = createDaemonTranscriptState({ now: 1 }); const commandEvents = normalizeDaemonEvent({ diff --git a/packages/web-shell/client/adapters/messageTypes.ts b/packages/web-shell/client/adapters/messageTypes.ts index 6a494e7895..4e7482802f 100644 --- a/packages/web-shell/client/adapters/messageTypes.ts +++ b/packages/web-shell/client/adapters/messageTypes.ts @@ -79,6 +79,7 @@ export interface DaemonUserMessage extends DaemonMessageMeta { role: 'user'; content: string; images?: Array<{ data: string; mimeType: string }>; + source?: string; } export interface DaemonAssistantMessage extends DaemonMessageMeta { diff --git a/packages/web-shell/client/adapters/transcriptToMessages.test.ts b/packages/web-shell/client/adapters/transcriptToMessages.test.ts index 5ff3c12007..bf935630ef 100644 --- a/packages/web-shell/client/adapters/transcriptToMessages.test.ts +++ b/packages/web-shell/client/adapters/transcriptToMessages.test.ts @@ -125,6 +125,21 @@ function toolBlock( } describe('transcriptBlocksToDaemonMessages', () => { + it('preserves user source metadata', () => { + const messages = transcriptBlocksToDaemonMessages([ + textBlock('user-1', 'user', 'scheduled prompt', 1, false, { + meta: { source: 'cron' }, + }), + ]); + + expect(messages[0]).toMatchObject({ + id: 'user-1', + role: 'user', + content: 'scheduled prompt', + source: 'cron', + }); + }); + it('hides background task notifications by metadata', () => { const messages = transcriptBlocksToDaemonMessages([ textBlock( diff --git a/packages/web-shell/client/adapters/transcriptToMessages.ts b/packages/web-shell/client/adapters/transcriptToMessages.ts index 2c6f52fd5f..3b09fedfca 100644 --- a/packages/web-shell/client/adapters/transcriptToMessages.ts +++ b/packages/web-shell/client/adapters/transcriptToMessages.ts @@ -195,11 +195,16 @@ export function transcriptBlocksToDaemonMessages( currentThinkingIdx = null; needsNewContentMessage = false; const textBlock = block as DaemonTextTranscriptBlock; + const meta = getRecord( + (textBlock as ExtendedDaemonTextTranscriptBlock).meta, + ); + const source = getString(meta, 'source'); const msg: DaemonUserMessage = { id: block.id, role: 'user', content: textBlock.text, timestamp: blockTime, + ...(source ? { source } : {}), }; // Attach images if present if (textBlock.images && textBlock.images.length > 0) {