fix(daemon): preserve user message source metadata (#6385)

This commit is contained in:
callmeYe 2026-07-07 15:08:28 +08:00 committed by GitHub
parent 5d2bfbd21b
commit 7e0e79b6bc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 95 additions and 3 deletions

View file

@ -113,6 +113,7 @@ export class HistoryReplayer {
await this.messageEmitter.emitUserMessage(
displayText,
record.timestamp,
record.subtype === 'cron' ? { source: 'cron' } : undefined,
);
}
break;

View file

@ -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', () => {

View file

@ -90,12 +90,17 @@ export class MessageEmitter extends BaseEmitter {
async emitUserMessage(
text: string,
timestamp?: string | number,
options: { source?: string } = {},
): Promise<void> {
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 } : {}),
});
}

View file

@ -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']);

View file

@ -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({

View file

@ -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 {

View file

@ -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(

View file

@ -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) {