Просмотр исходного кода

fix(ui-conversation): skip uncorrelated legacy events

imccyu 1 месяц назад
Родитель
Сommit
81822f4076

+ 5 - 2
packages/client/ui-conversation/src/client/conversation-nodes/retry.ts

@@ -42,10 +42,13 @@ export const retryDefinition: ConversationNodeDefinition<RetryState> = {
   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
   },

+ 4 - 1
packages/client/ui-conversation/src/client/conversation-nodes/tool.ts

@@ -241,7 +241,10 @@ export const toolDefinition: ConversationNodeDefinition<ToolState> = {
       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
   },

+ 44 - 0
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', {