fix(transcript): fold mid-turn task notifications into the current turn on cold rebuild

This commit is contained in:
qer 2026-08-19 23:27:06 +08:00
parent 30e7f62d2c
commit d9459eaf79
2 changed files with 64 additions and 0 deletions

View file

@ -151,9 +151,12 @@ export function groupMessagesIntoSnapshot(
items.push(item);
};
let prevRole: string | undefined;
for (const message of messages) {
if (message.role === 'system') continue;
const originKind = message.origin?.kind;
const prevRoleAtEntry = prevRole;
prevRole = message.role;
if (message.role === 'user') {
if (originKind !== undefined && HIDDEN_USER_ORIGINS.has(originKind)) {
@ -171,6 +174,30 @@ export function groupMessagesIntoSnapshot(
}
continue;
}
if (originKind === 'task' || originKind === 'background_task' || originKind === 'task_notification') {
if (prevRoleAtEntry !== 'assistant' && prevRoleAtEntry !== 'tool') {
const opening = foldTurnOpeningInput(message);
startTurn(mapOrigin(message), opening.text, opening.attachmentIds);
continue;
}
const origin = message.origin as { taskId?: unknown } | undefined;
const taskId = typeof origin?.taskId === 'string' ? origin.taskId : undefined;
const current = ensureTurn(mapOrigin(message));
let step = current.steps.at(-1);
if (step === undefined) {
step = { stepId: `${current.turnId}.1`, ordinal: 1, frames: [] };
current.steps.push(step);
}
step.frames.push({
kind: 'text',
frameId: `${step.stepId}.f${step.frames.length + 1}`,
role: 'user',
text: textOf(message),
...(taskId !== undefined ? { taskId } : {}),
});
syncTurnItem(items, current);
continue;
}
const bundled = bundledSkillActivations(message);
if (bundled.length > 0) {
const parts = message.content ?? [];

View file

@ -443,6 +443,43 @@ describe('groupMessagesIntoSnapshot (cold path)', () => {
expect(marker?.kind === 'marker' && marker.marker).toBe('compaction');
});
it('folds task-notification user messages into the current turn instead of opening their own', () => {
const snapshot = groupMessagesIntoSnapshot([
{ role: 'user', content: [{ type: 'text', text: 'run it' }], toolCalls: [], origin: { kind: 'user' } },
{
role: 'assistant',
content: [{ type: 'text', text: 'starting' }],
toolCalls: [{ id: 'c1', name: 'Bash', arguments: '{"command":"ls"}' }],
},
{
role: 'user',
content: [{ type: 'text', text: '<notification id="task:task-9:completed"></notification>' }],
toolCalls: [],
origin: { kind: 'task', taskId: 'task-9' } as { kind: string },
},
{
role: 'assistant',
content: [{ type: 'text', text: 'continuing' }],
toolCalls: [],
},
]);
const turns = snapshot.items.filter((i) => i.kind === 'turn');
expect(turns).toHaveLength(1);
const turn = turns[0];
if (turn?.kind !== 'turn') throw new Error('expected turn');
const userFrames = turn.steps
.flatMap((step) => step.frames)
.filter((f) => f.kind === 'text' && f.role === 'user');
expect(userFrames).toHaveLength(1);
expect(userFrames[0]).toMatchObject({ taskId: 'task-9' });
const assistantTexts = turn.steps
.flatMap((step) => step.frames)
.filter((f) => f.kind === 'text' && f.role === 'assistant')
.map((f) => f.kind === 'text' && f.text);
expect(assistantTexts).toEqual(['starting', 'continuing']);
});
it('expands a bundled prompt into per-skill markers and a caller-text turn', () => {
const snapshot = groupMessagesIntoSnapshot([
{