From 81822f40762806c354f2abd489e04841653e507a Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Sun, 9 Aug 2026 20:23:58 +0800 Subject: [PATCH] fix(ui-conversation): skip uncorrelated legacy events --- .../src/client/conversation-nodes/retry.ts | 7 ++- .../src/client/conversation-nodes/tool.ts | 5 ++- .../conversation-node-definitions.spec.ts | 44 +++++++++++++++++++ 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/packages/client/ui-conversation/src/client/conversation-nodes/retry.ts b/packages/client/ui-conversation/src/client/conversation-nodes/retry.ts index fa7c2ee5cf..d504f32928 100644 --- a/packages/client/ui-conversation/src/client/conversation-nodes/retry.ts +++ b/packages/client/ui-conversation/src/client/conversation-nodes/retry.ts @@ -42,10 +42,13 @@ export const retryDefinition: ConversationNodeDefinition = { kind: 'model-retry', match: (event) => { if (event.type === 'llm/retry') { - return { id: String(event.data.retryId), role: event.data.retry === 1 ? 'start' : 'update' } + const retryId: unknown = event.data.retryId + if (typeof retryId !== 'string' || retryId === '') return null + return { id: retryId, role: event.data.retry === 1 ? 'start' : 'update' } } if (event.type === 'llm/retry-started') { - return { id: String(event.data.retryId), role: 'update' } + const retryId: unknown = event.data.retryId + return typeof retryId === 'string' && retryId !== '' ? { id: retryId, role: 'update' } : null } return null }, diff --git a/packages/client/ui-conversation/src/client/conversation-nodes/tool.ts b/packages/client/ui-conversation/src/client/conversation-nodes/tool.ts index dc9e996dcc..c1b8022e41 100644 --- a/packages/client/ui-conversation/src/client/conversation-nodes/tool.ts +++ b/packages/client/ui-conversation/src/client/conversation-nodes/tool.ts @@ -241,7 +241,10 @@ export const toolDefinition: ConversationNodeDefinition = { return { id: String(event.data.message.source.callId), role: 'update' } } if (event.type === 'tool/code-dispatch-start' || event.type === 'tool/code-dispatch') { - return { id: String(event.data.rootCallId), role: 'update' } + const rootCallId: unknown = event.data.rootCallId + return typeof rootCallId === 'string' && rootCallId !== '' + ? { id: rootCallId, role: 'update' } + : null } return null }, diff --git a/packages/client/ui-conversation/tests/conversation-node-definitions.spec.ts b/packages/client/ui-conversation/tests/conversation-node-definitions.spec.ts index 016c7176db..db9c0ccdd4 100644 --- a/packages/client/ui-conversation/tests/conversation-node-definitions.spec.ts +++ b/packages/client/ui-conversation/tests/conversation-node-definitions.spec.ts @@ -718,6 +718,50 @@ describe('built-in conversation node Definitions', () => { expect(node(snapshot(value), 'compaction')).toBeUndefined() }) + it('ignores legacy retry and code-dispatch events without correlation ids', () => { + const value = assembler([ + at(10, 'llm/retry', { + turn: 1, + step: 1, + provider: 'fake', + mode: 'normal', + policyKey: 'fake-normal', + retry: 1, + maxRetries: 2, + delayMs: 10, + failure: { code: 'TRANSPORT', message: 'first legacy retry' }, + }), + at(11, 'llm/retry-started', { turn: 1, step: 1, retry: 1 }), + at(20, 'llm/retry', { + turn: 2, + step: 1, + provider: 'fake', + mode: 'normal', + policyKey: 'fake-normal', + retry: 1, + maxRetries: 2, + delayMs: 10, + failure: { code: 'TRANSPORT', message: 'second legacy retry' }, + }), + at(30, 'tool/code-dispatch-start', { + parentCallId: 'root', + subCallId: 'child', + name: 'legacy-subcall', + arguments: {}, + }), + at(31, 'tool/code-dispatch', { + parentCallId: 'root', + subCallId: 'child', + name: 'legacy-subcall', + arguments: {}, + content: [], + }), + ], true) + + expect(node(snapshot(value), 'model-retry')).toBeUndefined() + expect(node(snapshot(value), 'tool-call')).toBeUndefined() + }) + it('suppresses a turn error when the loaded tail contains only a later retry attempt', () => { const value = assembler([ at(5, 'llm/retry', {