Przeglądaj źródła

修复普通聊天幻影排队:上游立即接受时出队本地镜像队列

command.submitted 在 state.running 看似为真时会把 prompt 压入本地
state.queue,但这个队列只是镜像——上游并不知道它的存在。当上游实际
空闲并立即接受(command.confirmed 且 status 非 queued)时,该条目
原本永远不会被清理,除非客户端重进会话触发 session.resume,导致普通
聊天第二轮一直显示"排队中"。

现在 confirmed 到达时立即出队该 delivery 并接管其 turn,让随后的
run 事件挂到正确的用户消息上;上游真实排队(status: queued)的
行为保持不变。
Samien 1 miesiąc temu
rodzic
commit
3bfda561ba

+ 18 - 0
src/server/chatTranscriptStore.ts

@@ -734,6 +734,24 @@ export class ChatTranscriptStore {
           state.queue = state.queue.filter((q) => q.delivery !== p.delivery_id)
           if (state.turn === pending.turn && !state.current) state.terminal = true
           state.running = !state.terminal || state.queue.length > 0
+        } else if (p.status !== 'queued') {
+          // The upstream accepted this prompt for immediate execution. A local
+          // queue entry only exists because `state.running` looked busy at
+          // submit time (e.g. a stale background flag or a missed terminal);
+          // the upstream knows nothing about that entry, so nothing would ever
+          // drain it except a later `session.resume`. Drop it now and adopt
+          // its turn so the imminent run events attach to this user row.
+          const queued = state.queue.filter((q) => q.delivery !== p.delivery_id)
+          if (queued.length !== state.queue.length) {
+            state.queue = queued
+            if (state.terminal) {
+              state.turn = pending.turn
+              state.current = undefined
+              state.preview = false
+              state.terminal = false
+              state.tools = {}
+            }
+          }
         }
         delete state.pending[String(p.delivery_id)]
         state.queued = state.queue.length > 0

+ 26 - 0
tests/server/ordinaryTranscriptV2.test.ts

@@ -87,6 +87,32 @@ describe('ordinary v2 canonical writer', () => {
       reopened.close()
     }
   })
+  it('drops the local queue entry once the upstream accepts the prompt immediately', () => {
+    const { store, send } = setup()
+    store.recordCommand(...scope, 'prompt.submit', { text: '第一问', _delivery_id: 'web:prompt:one' })
+    send('command.confirmed', { delivery_id: 'web:prompt:one' })
+    send('message.start')
+    send('message.delta', { text: '第一答' })
+    // A lingering background job keeps the transcript head "running" after
+    // the visible turn finished.
+    send('message.complete', { text: '第一答', background_pending: 1 })
+    store.recordCommand(...scope, 'prompt.submit', { text: '第二问', _delivery_id: 'web:prompt:two' })
+    expect(store.transcripts.head(...scope).queued).toBe(true)
+    // The upstream was actually idle and accepted the second prompt for
+    // immediate execution; the local queue entry was only a stale mirror.
+    send('command.confirmed', { delivery_id: 'web:prompt:two' })
+    const head = store.transcripts.head(...scope)
+    expect(head.queued).toBe(false)
+    expect(head.queue).toHaveLength(0)
+    send('message.start')
+    send('message.delta', { text: '第二答' })
+    send('message.complete', { text: '第二答' })
+    const all = messages(store)
+    expect(all.filter((m) => m.role === 'user')).toHaveLength(2)
+    expect(all.filter((m) => m.final_result)).toHaveLength(2)
+    expect(new Set(all.filter((m) => m.role === 'assistant').map((m) => m.turn_id)).size).toBe(2)
+    expect(store.transcripts.head(...scope).running).toBe(false)
+  })
   it('keeps steer in the current turn without moving the reply behind a future user', () => {
     const { store, send } = setup()
     store.recordCommand(...scope, 'prompt.submit', { text: '开始', _delivery_id: 'web:prompt:one' })