@tea-agent/loop-agent 0.43.0-next.17 → 0.43.0-next.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/CHANGELOG.md +9 -140
  2. package/dist/application/task-lifecycle/advance.js +4 -17
  3. package/dist/application/task-lifecycle/observe.js +29 -43
  4. package/dist/application/task-lifecycle/plan-transitions.js +4 -5
  5. package/dist/build-stamp.json +3 -3
  6. package/dist/commands/client-recovery.js +55 -20
  7. package/dist/commands/new-task.js +5 -0
  8. package/dist/commands/status.js +57 -0
  9. package/dist/commands/task-contract.js +269 -0
  10. package/dist/executors/dag-pi-executor.js +230 -269
  11. package/dist/executors/pi-executor.js +0 -2
  12. package/dist/executors/pi-sdk-executor.js +39 -77
  13. package/dist/executors/shell-executor.js +2 -3
  14. package/dist/executors/shell-write-guard.js +32 -27
  15. package/dist/shared/pi-context-pressure/extension.js +16 -26
  16. package/dist/shared/pi-retry-settings.js +0 -40
  17. package/dist/shared/prompts.js +26 -0
  18. package/dist/task/config.js +2 -0
  19. package/dist/task/contract/validate-draft.js +106 -0
  20. package/dist/task/goal.js +3 -0
  21. package/dist/task/index.js +12 -0
  22. package/dist/task/lifecycle.js +1 -0
  23. package/dist/task/operator/capabilities.js +6 -0
  24. package/dist/task/operator/envelope.js +2 -0
  25. package/dist/task/operator/index.js +5 -0
  26. package/dist/task/operator/registry.js +2 -0
  27. package/dist/task/operator/types.js +1 -0
  28. package/dist/task/paths.js +1 -0
  29. package/dist/task/source-prepare/source-fidelity-pi.js +2 -2
  30. package/dist/task/source-references.js +23 -48
  31. package/dist/task/source-state.js +1 -0
  32. package/dist/task/subagent-guidance.js +1 -0
  33. package/dist/worker/console/chat/assistant-content.js +2 -23
  34. package/dist/worker/console/chat/explore-tools.js +0 -13
  35. package/dist/worker/console/chat/pi-runtime.js +1182 -221
  36. package/dist/worker/console/chat/routes.js +71 -213
  37. package/dist/worker/console/chat/sdd-data-alignment.js +9 -65
  38. package/dist/worker/console/chat/session-catalog.js +0 -32
  39. package/dist/worker/console/chat/session-store.js +6 -22
  40. package/dist/worker/console/chat/shortcuts.js +0 -6
  41. package/dist/worker/console/chat/sift-bridge.js +1 -0
  42. package/dist/worker/console/chat/tool-preview.js +0 -11
  43. package/dist/worker/console/chat/tools.js +0 -6
  44. package/dist/worker/console/console-update-and-init.js +19 -1
  45. package/dist/worker/console/console-update-runtime.js +20 -5
  46. package/dist/worker/console/index.js +20 -0
  47. package/dist/worker/console/operator-actions.js +1 -13
  48. package/dist/worker/console/routes.js +0 -8
  49. package/dist/worker/console/server.js +29 -11
  50. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4NXyedy.js → abnfDiagram-N423BO3Z-hUmM_GRf.js} +1 -1
  51. package/dist/worker/console/static/assets/{arc-NZ3qXvBI.js → arc-Bt-xuieW.js} +1 -1
  52. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-COZSRvml.js → architectureDiagram-T3A2C74G-DTqkL4DJ.js} +1 -1
  53. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-Ck0Gxi9C.js → blockDiagram-VBNYF7ZC-Cy0MNnzV.js} +1 -1
  54. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BX5HQ4Ox.js → c4Diagram-5PPSVZJV-DDdlT0Pd.js} +1 -1
  55. package/dist/worker/console/static/assets/channel-BpzRnZmU.js +1 -0
  56. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-CoviemSL.js → chunk-2GRJ4B5K-3JawwNZ6.js} +1 -1
  57. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DbOdOkbZ.js → chunk-2Q5K7J3B-DoTp7Cri.js} +1 -1
  58. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BsG2pmdQ.js → chunk-5RXB4S5H-CcirhZaS.js} +1 -1
  59. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-DGdvrNm8.js → chunk-5VM5RSS4-VrG21gnP.js} +1 -1
  60. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BAy7Yd6y.js → chunk-6Q2QTUOP-DDOgIS70.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-D9UA7hUb.js → chunk-GF5L2VYU-CBjmgvJ_.js} +1 -1
  62. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-DPz7SZ8V.js → chunk-JWPE2WC7-CCQSht8M.js} +1 -1
  63. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BmuEOIh7.js → chunk-KBJHAD2P-BpGMkUUV.js} +1 -1
  64. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CIhBWnd8.js → chunk-RYQCIY6F-DNDT41QL.js} +1 -1
  65. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CScR6mDD.js → chunk-XXDRQBXY-euXnDFbn.js} +1 -1
  66. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BhlMVqCc.js +1 -0
  67. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BhlMVqCc.js +1 -0
  68. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BjpfWaUr.js → cose-bilkent-JH36ORCC-BJ6NAeql.js} +1 -1
  69. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BFQohJgT.js → cynefin-VYW2F7L2-F5JKIvIq.js} +1 -1
  70. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-D3gzRJnS.js → cynefinDiagram-MW4NZA55-Cwmkl_Gi.js} +1 -1
  71. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-5eojkhoA.js → dagre-VZM6K2ZE-BmCZ8815.js} +1 -1
  72. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CpD8LWu_.js → diagram-7IWD3JNH-zWA3hUnX.js} +1 -1
  73. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B8_4qJ2a.js → diagram-B4RE2ZJO-BzA-888F.js} +1 -1
  74. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-HOsXf0eu.js → diagram-LBJQPF4R-tPNheoch.js} +1 -1
  75. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BqwRwiWP.js → diagram-Q27KOJAE-Cdi7Fyqp.js} +1 -1
  76. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BRVCNZe1.js → diagram-UB23O5K3-DFK9VBf1.js} +1 -1
  77. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Cvb1TgGh.js → ebnfDiagram-BXEA7PRR-xCUr1kpg.js} +1 -1
  78. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DS0O0qG9.js → erDiagram-JOGREHBK-BANQHY_A.js} +1 -1
  79. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-BHIRvRr3.js → flowDiagram-UKHOOZJN-VHJYEMkP.js} +1 -1
  80. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DcCJ1JWx.js → ganttDiagram-PKOTCBZU-DiyZQ0C6.js} +1 -1
  81. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-B9ia5ony.js → gitGraphDiagram-DS77QQ5N-CZtIeBBa.js} +1 -1
  82. package/dist/worker/console/static/assets/index-BlN6ytLr.css +1 -0
  83. package/dist/worker/console/static/assets/index-CA-jfWlE.js +449 -0
  84. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-3ogpgegn.js → infoDiagram-6WML65LV-BuSspC0X.js} +1 -1
  85. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-4tgpQ5SL.js → ishikawaDiagram-WSZJBQD7-DRJZyPFy.js} +1 -1
  86. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DNfgHqFx.js → journeyDiagram-NVQOT4AX-DIfXIVuE.js} +1 -1
  87. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Dz7ngC8z.js → kanban-definition-27J2QSJJ-CT10_7Lt.js} +1 -1
  88. package/dist/worker/console/static/assets/{linear-DI7Jv62E.js → linear-ufemj74H.js} +1 -1
  89. package/dist/worker/console/static/assets/{mermaid.core-CQA4Nzmo.js → mermaid.core-DEQe8-_m.js} +5 -5
  90. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-CtsMc8Mf.js → mindmap-definition-FAOFIHXS-BRLUrbrC.js} +1 -1
  91. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-ByppE9yH.js → pegDiagram-VL7TDLO6-BgibKtiF.js} +1 -1
  92. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-B5PtgFgt.js → pieDiagram-7S7Q4E2Y-D3eyfdV9.js} +1 -1
  93. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Cq4p0k2K.js → quadrantDiagram-CIZ2JOQS-DXeTk5Px.js} +1 -1
  94. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-DjfkyljN.js → railroadDiagram-AXF67PYL-BgXdFEe7.js} +1 -1
  95. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DzynNRSl.js → requirementDiagram-LRYGKXZP-CseC2A7R.js} +1 -1
  96. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Cj7TCpTJ.js → sankeyDiagram-W5VNT64P-D80PFzfJ.js} +1 -1
  97. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-C73C6ykA.js → sequenceDiagram-SI44F4Z6-BWzHW7LS.js} +1 -1
  98. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-BiNLgN2x.js → sizeCapture-X5ZJPWSS-xWQYTLAn.js} +1 -1
  99. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-UwvjS64-.js → stateDiagram-OKZ733FA-CLxtpZeL.js} +1 -1
  100. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-CiVoq4G-.js +1 -0
  101. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-CC7DH98r.js → swimlanes-SLNWSIFB-DwugtmWV.js} +2 -2
  102. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BkdgdpfR.js +8 -0
  103. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-LSBXTR70.js → timeline-definition-Z64GVDOM-DHEOwHIe.js} +1 -1
  104. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BAuxWwGP.js → vennDiagram-T6HMQDX7-pmbZKlBK.js} +1 -1
  105. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y--E5_gjZx.js → wardleyDiagram-T6FBY63Y-CivHoM5q.js} +1 -1
  106. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-dJWNmyzF.js → xychartDiagram-ELKLHX3M-BfHIE7o6.js} +1 -1
  107. package/dist/worker/console/static/index.html +2 -2
  108. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +10 -122
  109. package/dist/worker/console/static-src/operator-chat/compaction-message.js +0 -7
  110. package/dist/worker/console/static-src/operator-chat/pending-user-message.js +15 -35
  111. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +62 -136
  112. package/dist/worker/console/static-src/operator-chat/useChatStream.js +68 -159
  113. package/dist/worker/console/static-src/operator-chat/useChatThread.js +56 -75
  114. package/dist/worker/console/static-src/operator-chat/useComposer.js +78 -206
  115. package/dist/worker/console/workspace-context.js +1 -21
  116. package/dist/worker/observe/static/router.js +1 -1
  117. package/dist/worker/observe/static/styles.css +4 -7
  118. package/dist/worker/observe/static/views/dag-inspector.js +19 -12
  119. package/dist/worker/observe/static/views/dag.js +2 -2
  120. package/dist/worker/scheduler/index.js +25 -0
  121. package/dist/workflows/dag/facts.js +4 -0
  122. package/dist/workflows/dag/frontend-closeout.js +1 -1
  123. package/dist/workflows/dag/frontend-design-policy.js +1 -2
  124. package/dist/workflows/dag/frontend-implementation-contract.js +4 -8
  125. package/dist/workflows/dag/frontend-prewrite-gate.js +3 -3
  126. package/dist/workflows/dag/frontend-repair.js +76 -55
  127. package/dist/workflows/dag/frontend-review-context.js +1 -2
  128. package/dist/workflows/dag/frontend-review-scopes.js +12 -34
  129. package/dist/workflows/dag/frontend-shape-facts.js +2 -12
  130. package/dist/workflows/dag/frontend-verification-trace.js +42 -8
  131. package/dist/workflows/dag/index.js +6 -0
  132. package/dist/workflows/dag/init-hybrid.js +160 -97
  133. package/dist/workflows/dynamic/index.js +7 -0
  134. package/docs/architecture/runtime-boundaries.md +3 -3
  135. package/docs/templates/frontend-implementation-dag.json +8 -9
  136. package/package.json +3 -7
  137. package/skills/frontend-bounded-implement/SKILL.md +3 -4
  138. package/skills/frontend-bounded-implement/references/code-standards.md +3 -3
  139. package/skills/frontend-contract/references/contract-protocol.md +1 -3
  140. package/skills/frontend-design-review/SKILL.md +7 -12
  141. package/skills/frontend-design-review/references/review-checklist.md +7 -7
  142. package/skills/frontend-plan/SKILL.md +0 -19
  143. package/skills/frontend-plan/references/decision-contract.md +19 -26
  144. package/skills/frontend-review/SKILL.md +18 -12
  145. package/skills/frontend-review/references/review-findings.md +43 -24
  146. package/dist/shared/pi-provider-recovery.js +0 -182
  147. package/dist/worker/console/chat/browser-automation.js +0 -363
  148. package/dist/worker/console/chat/browser-policy.js +0 -113
  149. package/dist/worker/console/chat/browser-routes.js +0 -162
  150. package/dist/worker/console/chat/pi-runtime/custom-tools/ask-user-question.js +0 -44
  151. package/dist/worker/console/chat/pi-runtime/custom-tools/browser-tools.js +0 -89
  152. package/dist/worker/console/chat/pi-runtime/custom-tools/explore-tools.js +0 -202
  153. package/dist/worker/console/chat/pi-runtime/custom-tools/goal-tools.js +0 -79
  154. package/dist/worker/console/chat/pi-runtime/custom-tools/operator-tools.js +0 -86
  155. package/dist/worker/console/chat/pi-runtime/custom-tools/todo-write.js +0 -38
  156. package/dist/worker/console/chat/pi-runtime/inspection-types.js +0 -28
  157. package/dist/worker/console/chat/pi-runtime/inspection.js +0 -439
  158. package/dist/worker/console/chat/pi-runtime/sdk-bindings.js +0 -153
  159. package/dist/worker/console/chat/pi-runtime/sdk-session.js +0 -5
  160. package/dist/worker/console/chat/scm-routes.js +0 -217
  161. package/dist/worker/console/chat/scm-service.js +0 -283
  162. package/dist/worker/console/chat/scm-tools.js +0 -111
  163. package/dist/worker/console/chat/session-mode-view.js +0 -49
  164. package/dist/worker/console/chat/session-mode.js +0 -220
  165. package/dist/worker/console/chat/terminal-routes.js +0 -216
  166. package/dist/worker/console/chat/terminal-sessions.js +0 -348
  167. package/dist/worker/console/chat/terminal-tools.js +0 -211
  168. package/dist/worker/console/static/assets/channel-BxdTlb_5.js +0 -1
  169. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-_ByA1Nf8.js +0 -1
  170. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-_ByA1Nf8.js +0 -1
  171. package/dist/worker/console/static/assets/index-B9goN3S5.css +0 -1
  172. package/dist/worker/console/static/assets/index-wXlQohNs.js +0 -469
  173. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-nLgMZ0D3.js +0 -1
  174. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BcYqTady.js +0 -8
  175. package/dist/worker/console/static-src/operator-chat/active-turn-tail.js +0 -10
  176. package/dist/worker/console/static-src/operator-chat/chat-link.js +0 -94
  177. package/dist/worker/console/static-src/operator-chat/chat-scroll-position.js +0 -24
  178. package/dist/worker/console/static-src/operator-chat/composer-session-ledger.js +0 -192
  179. package/dist/worker/console/static-src/operator-chat/process-label.js +0 -41
  180. package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +0 -13
  181. package/dist/worker/console/static-src/operator-chat/turn-process-disclosure.js +0 -10
  182. package/dist/worker/console/static-src/operator-chat/use-searchable-hidden.js +0 -51
  183. package/dist/worker/console/static-src/operator-chat/user-turn-anchor-equality.js +0 -21
  184. package/dist/worker/console/workspace-initialization.js +0 -209
  185. package/dist/workflows/dag/frontend-capacity.js +0 -9
  186. package/dist/workflows/dag/frontend-plan-completeness.js +0 -186
  187. package/dist/workflows/dag/frontend-plan-recovery-policy.js +0 -18
@@ -53,18 +53,14 @@ function clearEmptyAssistant(setMessages, assistantId) {
53
53
  return;
54
54
  setMessages((messages) => messages.filter((message) => !(message.id === assistantId &&
55
55
  message.role === "assistant" &&
56
- !message.text.trim() &&
57
- // A row carrying only thinking/tool blocks is real content. Deleting
58
- // it here dropped the turn's process nodes while the settled path in
59
- // chat-sse-events deliberately keeps them (2026-09-08).
60
- !(message.content && message.content.length > 0))));
56
+ !message.text.trim())));
61
57
  }
62
58
  /** One GET /events stream owns all Chat events. Sending a prompt creates a
63
59
  * detached Turn resource only; it never opens a second inline SSE consumer. */
64
60
  export function useChatStream(params) {
65
61
  const { origin, refs, session, thread, composer, setError, setAutoScroll, setNotice, recoveryVersion = 0, watchSessionTitle, } = params;
66
62
  const { applySseBlock, setMessages, setStreaming, setStreamingAssistantId, streaming, setTurnPhase, setDeferredTurn, setDeferredTurns, } = thread;
67
- const { settleSubmission, settleSubmissionByRequest, replaceSessionDraft, } = composer;
63
+ const { setInput, setPendingImages, recordHistory } = composer;
68
64
  const showNotice = useCallback((message) => {
69
65
  if (!setNotice)
70
66
  return;
@@ -144,10 +140,7 @@ export function useChatStream(params) {
144
140
  current.generation === generation)
145
141
  current.ready = true;
146
142
  replay = false;
147
- // Fall through: the applier owns the buffered-replay flush and
148
- // returns immediately for this event. `continue` here silently
149
- // dropped that flush, so a reconnect that landed after the
150
- // turn settled never restored the answer (2026-09-08).
143
+ continue;
151
144
  }
152
145
  const assistantId = refs.assistantIdRef.current ?? fallbackAssistantId;
153
146
  const eventId = applySseBlock(block, assistantId, {
@@ -223,22 +216,15 @@ export function useChatStream(params) {
223
216
  }, [origin]);
224
217
  const postCreateTurn = useCallback(async (input) => {
225
218
  try {
226
- // Resolve against the configured Console origin and fail closed if a
227
- // future URL helper ever escapes that trusted same-origin boundary.
228
- const consoleOrigin = new URL(origin, window.location.href);
229
- const turnsUrl = new URL(operatorChatTurnsUrl(origin, input.sessionId), consoleOrigin);
230
- if (turnsUrl.origin !== consoleOrigin.origin)
231
- throw new Error("create-turn URL escaped the Console origin");
232
219
  // Real create-turn path remains `${origin}/api/operator/v1/chat/sessions/:id/turns`
233
220
  // (via operatorChatTurnsUrl); source contract anchors on /turns`.
234
- const res = await fetch(turnsUrl, {
221
+ const res = await fetch(operatorChatTurnsUrl(origin, input.sessionId), {
235
222
  method: "POST",
236
223
  headers: {
237
224
  "Content-Type": "application/json",
238
225
  "x-loop-console-confirmation": confirmationToken(),
239
226
  },
240
227
  credentials: "include",
241
- signal: input.signal,
242
228
  body: JSON.stringify({
243
229
  clientRequestId: input.clientRequestId,
244
230
  text: input.text.trim() || "请查看附件图片。",
@@ -357,6 +343,9 @@ export function useChatStream(params) {
357
343
  bag.sessionId = refs.sessionRef.current?.sessionId ?? null;
358
344
  bag.sessionGeneration = refs.sessionGenerationRef.current;
359
345
  bag.submitGeneration = refs.submitGenerationRef.current;
346
+ const setPendingImagesUi = (images) => {
347
+ setPendingImages(images);
348
+ };
360
349
  settleCreateTurnOutcome(bag, outcome, {
361
350
  assistantId: ctx.assistantId,
362
351
  text: ctx.text,
@@ -367,13 +356,13 @@ export function useChatStream(params) {
367
356
  acceptedUi: {
368
357
  setStreaming,
369
358
  setStreamingAssistantId,
370
- setPendingImages: () => undefined,
359
+ setPendingImages: setPendingImagesUi,
371
360
  watchSessionTitle,
372
361
  },
373
362
  adoptUi: {
374
363
  clearEmptyAssistant: (assistantId) => clearEmptyAssistant(setMessages, assistantId),
375
- setInput: () => undefined,
376
- setPendingImages: () => undefined,
364
+ setInput,
365
+ setPendingImages: setPendingImagesUi,
377
366
  setStreaming,
378
367
  setStreamingAssistantId,
379
368
  removePendingMessage: (requestId) => setMessages((messages) => removePendingSubmission(messages, requestId)),
@@ -383,8 +372,8 @@ export function useChatStream(params) {
383
372
  setStreaming,
384
373
  setStreamingAssistantId,
385
374
  clearEmptyAssistant: (assistantId) => clearEmptyAssistant(setMessages, assistantId),
386
- setInput: () => undefined,
387
- setPendingImages: () => undefined,
375
+ setInput,
376
+ setPendingImages: setPendingImagesUi,
388
377
  formatError: formatOperatorUserError,
389
378
  removePendingMessage: (requestId) => setMessages((messages) => removePendingSubmission(messages, requestId)),
390
379
  },
@@ -393,14 +382,6 @@ export function useChatStream(params) {
393
382
  void postAbortForTurn(ctx.sessionId, ctx.generation, turnId);
394
383
  },
395
384
  });
396
- if (outcome.kind === "accepted") {
397
- settleSubmission(ctx.attempt, "accepted");
398
- }
399
- else if (outcome.kind === "adopt-active" ||
400
- outcome.kind === "rejected" ||
401
- outcome.kind === "stale") {
402
- settleSubmission(ctx.attempt, "rejected");
403
- }
404
385
  applyOwnershipBagToRefs(refs, bag);
405
386
  const ownershipPhase = turnPhaseFromOwnership(bag);
406
387
  setTurnPhase(visualTurnPhaseFromOwnership(bag));
@@ -415,84 +396,68 @@ export function useChatStream(params) {
415
396
  setStreaming,
416
397
  setTurnPhase,
417
398
  setStreamingAssistantId,
418
- settleSubmission,
399
+ setPendingImages,
419
400
  watchSessionTitle,
420
401
  setMessages,
402
+ setInput,
421
403
  setError,
422
404
  postAbortForTurn,
423
405
  ]);
424
406
  const queueDeferredTurn = useCallback(async (input) => {
425
- const { attempt } = input;
426
407
  const activeSession = refs.sessionRef.current;
427
- const isCurrent = () => refs.sessionRef.current?.sessionId === attempt.identity.sessionId &&
428
- refs.sessionGenerationRef.current === attempt.sessionGeneration;
429
- if (!activeSession ||
430
- activeSession.sessionId !== attempt.identity.sessionId ||
431
- !isCurrent()) {
432
- settleSubmission(attempt, "rejected");
408
+ if (!activeSession || (!input.text.trim() && input.images.length === 0))
433
409
  return { action: "handled" };
434
- }
435
- const showError = (message) => {
436
- if (isCurrent())
437
- setError(message);
438
- };
439
410
  const acceptDeferred = (deferredTurn, deferredTurns) => {
440
- settleSubmission(attempt, "accepted");
441
- if (!isCurrent())
442
- return;
443
411
  setDeferredTurn(deferredTurns?.[0] ?? deferredTurn);
444
412
  setDeferredTurns(deferredTurns ?? [deferredTurn]);
413
+ setInput("");
414
+ setPendingImages([]);
445
415
  setError(null);
446
416
  };
447
417
  const acceptDispatched = () => {
448
- settleSubmission(attempt, "accepted");
449
- if (!isCurrent())
450
- return;
451
418
  setDeferredTurn(null);
452
419
  setDeferredTurns([]);
420
+ setInput("");
421
+ setPendingImages([]);
453
422
  setError(null);
454
423
  };
455
424
  const putOnce = async () => {
456
- const response = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(attempt.identity.sessionId)}/deferred-turn`, {
425
+ const response = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(activeSession.sessionId)}/deferred-turn`, {
457
426
  method: "PUT",
458
427
  headers: {
459
428
  "Content-Type": "application/json",
460
429
  "x-loop-console-confirmation": confirmationToken(),
461
430
  },
462
431
  credentials: "include",
463
- signal: attempt.signal,
464
432
  body: JSON.stringify({
465
- clientRequestId: attempt.clientRequestId,
433
+ clientRequestId: input.clientRequestId,
466
434
  blockedByTurnId: input.blockedByTurnId,
467
- text: attempt.draftSnapshot,
468
- images: attempt.imageSnapshot,
435
+ text: input.text,
436
+ images: input.images,
469
437
  }),
470
438
  });
471
439
  const body = (await response.json().catch(() => ({})));
472
440
  return { response, body };
473
441
  };
474
442
  try {
475
- for (let retry = 0; retry < 2; retry += 1) {
443
+ for (let attempt = 0; attempt < 2; attempt += 1) {
476
444
  const { response, body } = await putOnce();
477
445
  if (response.ok && body.accepted && body.deferredTurn) {
478
446
  acceptDeferred(body.deferredTurn, body.deferredTurns);
479
447
  return { action: "handled" };
480
448
  }
449
+ if (body.deferredTurns)
450
+ setDeferredTurns(body.deferredTurns);
451
+ if (body.deferredTurn)
452
+ setDeferredTurn(body.deferredTurn);
481
453
  if (body.error?.code !== "TURN_NOT_ACTIVE") {
482
- if (isCurrent()) {
483
- if (body.deferredTurns)
484
- setDeferredTurns(body.deferredTurns);
485
- if (body.deferredTurn)
486
- setDeferredTurn(body.deferredTurn);
487
- }
488
- settleSubmission(attempt, "rejected");
489
- showError(body.error?.message ??
454
+ setError(body.error?.message ??
490
455
  `待发送消息未被服务端接受(HTTP ${response.status})`);
491
456
  return { action: "handled" };
492
457
  }
493
- const canonical = await fetchSessionState(attempt.identity.sessionId);
458
+ const canonical = await fetchSessionState(activeSession.sessionId);
494
459
  const decision = reconcileDeferredSubmissionFromState({
495
- clientRequestId: attempt.clientRequestId,
460
+ clientRequestId: input.clientRequestId,
496
461
  blockedByTurnId: input.blockedByTurnId,
497
462
  state: canonical ?? undefined,
498
463
  });
@@ -504,29 +469,20 @@ export function useChatStream(params) {
504
469
  acceptDispatched();
505
470
  return { action: "handled" };
506
471
  }
507
- if (decision.action === "retry-deferred" && retry === 0)
472
+ if (decision.action === "retry-deferred" && attempt === 0)
508
473
  continue;
509
- if (decision.action === "send-now")
474
+ if (decision.action === "send-now") {
510
475
  return { action: "send-now" };
511
- if (isCurrent()) {
512
- if (body.deferredTurns)
513
- setDeferredTurns(body.deferredTurns);
514
- if (body.deferredTurn)
515
- setDeferredTurn(body.deferredTurn);
516
476
  }
517
- settleSubmission(attempt, "rejected");
518
- showError(body.error?.message ?? "停止状态已变化,当前消息仍保留在输入框");
477
+ setError(body.error?.message ?? "停止状态已变化,当前消息仍保留在输入框");
519
478
  return { action: "handled" };
520
479
  }
521
- settleSubmission(attempt, "rejected");
522
480
  return { action: "handled" };
523
481
  }
524
482
  catch (error) {
525
- if (attempt.signal.aborted)
526
- return { action: "handled" };
527
- const canonical = await fetchSessionState(attempt.identity.sessionId);
483
+ const canonical = await fetchSessionState(activeSession.sessionId);
528
484
  const decision = reconcileDeferredSubmissionFromState({
529
- clientRequestId: attempt.clientRequestId,
485
+ clientRequestId: input.clientRequestId,
530
486
  blockedByTurnId: input.blockedByTurnId,
531
487
  state: canonical ?? undefined,
532
488
  });
@@ -538,10 +494,10 @@ export function useChatStream(params) {
538
494
  acceptDispatched();
539
495
  return { action: "handled" };
540
496
  }
541
- if (decision.action === "send-now")
497
+ if (decision.action === "send-now") {
542
498
  return { action: "send-now" };
543
- settleSubmission(attempt, "rejected");
544
- showError(error instanceof Error
499
+ }
500
+ setError(error instanceof Error
545
501
  ? `无法确认消息是否已排队:${error.message}`
546
502
  : "无法确认消息是否已排队,请刷新后查看服务端状态");
547
503
  return { action: "handled" };
@@ -552,7 +508,8 @@ export function useChatStream(params) {
552
508
  thread.deferredTurn,
553
509
  setDeferredTurn,
554
510
  setDeferredTurns,
555
- settleSubmission,
511
+ setInput,
512
+ setPendingImages,
556
513
  setError,
557
514
  fetchSessionState,
558
515
  ]);
@@ -565,10 +522,6 @@ export function useChatStream(params) {
565
522
  : thread.deferredTurns[0] ?? thread.deferredTurn;
566
523
  if (!activeSession || !deferred)
567
524
  return;
568
- const identity = { ...activeSession };
569
- const generation = refs.sessionGenerationRef.current;
570
- const isCurrent = () => refs.sessionRef.current?.sessionId === identity.sessionId &&
571
- refs.sessionGenerationRef.current === generation;
572
525
  try {
573
526
  const response = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(activeSession.sessionId)}/deferred-turn`, {
574
527
  method: "DELETE",
@@ -584,25 +537,20 @@ export function useChatStream(params) {
584
537
  });
585
538
  const body = (await response.json().catch(() => ({})));
586
539
  if (!response.ok || !body.cancelled) {
587
- if (isCurrent())
588
- setError(body.error?.message ?? "待发送消息撤回失败");
540
+ setError(body.error?.message ?? "待发送消息撤回失败");
589
541
  return;
590
542
  }
591
- if (mode === "restore") {
592
- replaceSessionDraft(identity, deferred.text ?? body.deferredTurn?.text ?? "", deferred.images ??
593
- body.deferredTurn?.images ??
594
- []);
595
- }
596
- if (!isCurrent())
597
- return;
598
543
  const remaining = thread.deferredTurns.filter((item) => item.clientRequestId !== deferred.clientRequestId);
599
544
  setDeferredTurns(remaining);
600
545
  setDeferredTurn(remaining[0] ?? null);
546
+ if (mode === "restore") {
547
+ setInput(deferred.text ?? body.deferredTurn?.text ?? "");
548
+ setPendingImages(deferred.images ?? body.deferredTurn?.images ?? []);
549
+ }
601
550
  setError(null);
602
551
  }
603
552
  catch (error) {
604
- if (isCurrent())
605
- setError(error instanceof Error ? error.message : String(error));
553
+ setError(error instanceof Error ? error.message : String(error));
606
554
  }
607
555
  }, [
608
556
  origin,
@@ -611,7 +559,8 @@ export function useChatStream(params) {
611
559
  thread.deferredTurns,
612
560
  setDeferredTurn,
613
561
  setDeferredTurns,
614
- replaceSessionDraft,
562
+ setInput,
563
+ setPendingImages,
615
564
  setError,
616
565
  ]);
617
566
  const cancelDeferredTurn = useCallback(() => requestDeferredTurnDelete("restore"), [requestDeferredTurnDelete]);
@@ -620,9 +569,6 @@ export function useChatStream(params) {
620
569
  const activeSession = refs.sessionRef.current;
621
570
  if (!activeSession)
622
571
  return;
623
- const generation = refs.sessionGenerationRef.current;
624
- const isCurrent = () => refs.sessionRef.current?.sessionId === activeSession.sessionId &&
625
- refs.sessionGenerationRef.current === generation;
626
572
  const item = thread.deferredTurns.find((entry) => entry.clientRequestId === target.clientRequestId);
627
573
  if (!item)
628
574
  return;
@@ -634,8 +580,6 @@ export function useChatStream(params) {
634
580
  body: JSON.stringify({ clientRequestId: target.clientRequestId, blockedByTurnId: item.blockedByTurnId, text, images: item.images ?? [], expectedRevision: target.revision }),
635
581
  });
636
582
  const body = (await response.json().catch(() => ({})));
637
- if (!isCurrent())
638
- return;
639
583
  if (!response.ok)
640
584
  throw new Error(body.error?.message ?? "编辑排队消息失败");
641
585
  if (body.deferredTurns)
@@ -645,17 +589,13 @@ export function useChatStream(params) {
645
589
  setError(null);
646
590
  }
647
591
  catch (error) {
648
- if (isCurrent())
649
- setError(error instanceof Error ? error.message : "编辑排队消息失败");
592
+ setError(error instanceof Error ? error.message : "编辑排队消息失败");
650
593
  }
651
594
  }, [origin, refs, thread.deferredTurns, setDeferredTurn, setDeferredTurns, setError]);
652
595
  const steerDeferredTurn = useCallback(async (target) => {
653
596
  const activeSession = refs.sessionRef.current;
654
597
  if (!activeSession)
655
598
  return;
656
- const generation = refs.sessionGenerationRef.current;
657
- const isCurrent = () => refs.sessionRef.current?.sessionId === activeSession.sessionId &&
658
- refs.sessionGenerationRef.current === generation;
659
599
  try {
660
600
  const response = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(activeSession.sessionId)}/deferred-turn/${encodeURIComponent(target.clientRequestId)}/steer`, {
661
601
  method: "POST",
@@ -664,8 +604,6 @@ export function useChatStream(params) {
664
604
  body: JSON.stringify({ expectedRevision: target.revision }),
665
605
  });
666
606
  const body = (await response.json().catch(() => ({})));
667
- if (!isCurrent())
668
- return;
669
607
  if (!response.ok)
670
608
  throw new Error(body.error?.message ?? "插话发送失败");
671
609
  const remaining = body.deferredTurns ?? thread.deferredTurns.filter((item) => item.clientRequestId !== target.clientRequestId);
@@ -674,39 +612,33 @@ export function useChatStream(params) {
674
612
  setError(null);
675
613
  }
676
614
  catch (error) {
677
- if (isCurrent())
678
- setError(error instanceof Error ? error.message : "插话发送失败");
615
+ setError(error instanceof Error ? error.message : "插话发送失败");
679
616
  }
680
617
  }, [origin, refs, thread.deferredTurns, setDeferredTurn, setDeferredTurns, setError]);
681
- const sendPrompt = useCallback(async (attempt) => {
682
- const text = attempt.draftSnapshot;
683
- const images = attempt.imageSnapshot;
618
+ const sendPrompt = useCallback(async (text, images) => {
684
619
  const initialSession = refs.sessionRef.current;
685
- if (!initialSession ||
686
- initialSession.sessionId !== attempt.identity.sessionId ||
687
- refs.sessionGenerationRef.current !== attempt.sessionGeneration ||
688
- (!text.trim() && images.length === 0)) {
689
- settleSubmission(attempt, "rejected");
620
+ if (!initialSession || (!text.trim() && images.length === 0))
690
621
  return;
691
- }
692
622
  const ownership = ownershipBagFromRefs(refs);
693
623
  const route = decideSubmissionRoute({
694
624
  turnPhase: turnPhaseFromOwnership(ownership),
695
625
  blockedByTurnId: ownership.stoppingTurnId ?? ownership.activeTurnId,
696
626
  });
697
- const clientRequestId = attempt.clientRequestId;
627
+ const clientRequestId = requestId("chat-submit");
698
628
  if (route.action === "hold") {
699
629
  const phase = turnPhaseFromOwnership(ownership);
700
630
  if (phase === "stopping")
701
631
  showNotice("正在确认上一轮停止状态,当前消息已保留");
702
632
  else if (phase === "running")
703
633
  showNotice("正在确认当前回合状态,当前消息已保留");
704
- settleSubmission(attempt, "rejected");
705
634
  return;
706
635
  }
636
+ recordHistory?.(text);
707
637
  if (route.action === "queue-deferred") {
708
638
  const deferred = await queueDeferredTurn({
709
- attempt,
639
+ text,
640
+ images,
641
+ clientRequestId,
710
642
  blockedByTurnId: route.blockedByTurnId,
711
643
  });
712
644
  if (deferred.action !== "send-now")
@@ -716,22 +648,19 @@ export function useChatStream(params) {
716
648
  const generation = refs.sessionGenerationRef.current;
717
649
  if (!activeSession ||
718
650
  activeSession.sessionId !== initialSession.sessionId ||
719
- generation !== attempt.sessionGeneration ||
720
651
  (!text.trim() && images.length === 0) ||
721
652
  refs.streamingRef.current ||
722
653
  refs.submitInFlightRef.current ||
723
654
  // AC-R5-002: ownership lease blocks send until terminal release.
724
- refs.pendingSubmissionRef.current) {
725
- settleSubmission(attempt, "rejected");
655
+ refs.pendingSubmissionRef.current)
726
656
  return;
727
- }
728
657
  // AC-001: acquire synchronous submit ownership before the first await.
729
658
  refs.submitInFlightRef.current = true;
730
659
  const submitGeneration = ++refs.submitGenerationRef.current;
731
660
  const userMessageId = requestId("u");
732
661
  const assistantId = requestId("a");
733
662
  const draftText = text.trim();
734
- const draftImages = [...images];
663
+ const draftImages = images.slice();
735
664
  const pending = createPendingSubmission({
736
665
  clientRequestId,
737
666
  submitGeneration,
@@ -763,6 +692,7 @@ export function useChatStream(params) {
763
692
  user: userMsg,
764
693
  assistant: assistantMsg,
765
694
  }));
695
+ setInput("");
766
696
  setAutoScroll(true);
767
697
  setError(null);
768
698
  try {
@@ -776,19 +706,13 @@ export function useChatStream(params) {
776
706
  releaseSubmitOwnership(clientRequestId);
777
707
  setMessages((messages) => removePendingSubmission(messages, clientRequestId));
778
708
  clearEmptyAssistant(setMessages, assistantId);
779
- settleSubmission(attempt, "rejected");
709
+ setInput(draftText);
710
+ setPendingImages(draftImages);
780
711
  return;
781
712
  }
782
713
  if (refs.sessionRef.current?.sessionId !== activeSession.sessionId ||
783
714
  refs.sessionGenerationRef.current !== generation) {
784
715
  releaseSubmitOwnership(clientRequestId);
785
- if (refs.sessionRef.current?.sessionId === activeSession.sessionId) {
786
- // Same session, newer generation: the activation snapshot replaced
787
- // the thread, so remove its obsolete optimistic projection.
788
- setMessages((messages) => removePendingSubmission(messages, clientRequestId));
789
- clearEmptyAssistant(setMessages, assistantId);
790
- }
791
- settleSubmission(attempt, "rejected");
792
716
  return;
793
717
  }
794
718
  // Attachments stay until accepted (AC/UIS-accepted).
@@ -804,7 +728,6 @@ export function useChatStream(params) {
804
728
  assistantId,
805
729
  text: draftText,
806
730
  images: draftImages,
807
- attempt,
808
731
  pending,
809
732
  };
810
733
  const first = await postCreateTurn({
@@ -812,7 +735,6 @@ export function useChatStream(params) {
812
735
  clientRequestId,
813
736
  text: draftText,
814
737
  images: draftImages,
815
- signal: attempt.signal,
816
738
  });
817
739
  if (!isSubmissionCurrent({
818
740
  pending,
@@ -822,7 +744,6 @@ export function useChatStream(params) {
822
744
  })) {
823
745
  pending.resolveOutcome({ kind: "stale" });
824
746
  releaseSubmitOwnership(clientRequestId);
825
- settleSubmission(attempt, first.httpOk && first.body.accepted ? "accepted" : "rejected");
826
747
  return;
827
748
  }
828
749
  let outcome = first.networkError
@@ -849,7 +770,6 @@ export function useChatStream(params) {
849
770
  clientRequestId,
850
771
  text: draftText,
851
772
  images: draftImages,
852
- signal: attempt.signal,
853
773
  });
854
774
  if (!isSubmissionCurrent({
855
775
  pending,
@@ -859,7 +779,6 @@ export function useChatStream(params) {
859
779
  })) {
860
780
  pending.resolveOutcome({ kind: "stale" });
861
781
  releaseSubmitOwnership(clientRequestId);
862
- settleSubmission(attempt, retry.httpOk && retry.body.accepted ? "accepted" : "rejected");
863
782
  return;
864
783
  }
865
784
  outcome = retry.networkError
@@ -890,13 +809,14 @@ export function useChatStream(params) {
890
809
  releaseSubmitOwnership,
891
810
  showNotice,
892
811
  setMessages,
893
- settleSubmission,
812
+ setInput,
894
813
  setAutoScroll,
895
814
  setError,
896
815
  setStreaming,
897
816
  setTurnPhase,
898
817
  setStreamingAssistantId,
899
818
  queueDeferredTurn,
819
+ recordHistory,
900
820
  ]);
901
821
  const handleStop = useCallback(async () => {
902
822
  // AC-R5-001: durable stopRequested is set synchronously before any await so
@@ -952,9 +872,8 @@ export function useChatStream(params) {
952
872
  // Intent remains; /state bind-active will fire exactly one abort.
953
873
  return;
954
874
  }
955
- if (outcome.kind === "rejected" ||
956
- outcome.kind === "stale") {
957
- // Rejected/stale settlement released this submission's stop intent.
875
+ else {
876
+ // rejected / stale — clear durable intent (settle already released).
958
877
  refs.stopRequestedRef.current = false;
959
878
  return;
960
879
  }
@@ -998,7 +917,6 @@ export function useChatStream(params) {
998
917
  if (!session || (!streaming && !hasOwnershipWork))
999
918
  return;
1000
919
  const sessionId = session.sessionId;
1001
- const sessionIdentity = { ...session };
1002
920
  const generation = refs.sessionGenerationRef.current;
1003
921
  let cancelled = false;
1004
922
  let syncInFlight = null;
@@ -1041,15 +959,11 @@ export function useChatStream(params) {
1041
959
  if (state && "deferredTurn" in state)
1042
960
  setDeferredTurn(state.deferredTurn ?? null);
1043
961
  if (result === "bound" && ownershipPhase !== "stopping") {
1044
- settleSubmissionByRequest(sessionIdentity, pending.clientRequestId, "accepted");
1045
962
  // US-BUSY-ACTIVE-BOUND: keep streaming/busy with same id.
1046
963
  setStreaming(true);
1047
964
  return;
1048
965
  }
1049
966
  if (result === "released") {
1050
- settleSubmissionByRequest(sessionIdentity, pending.clientRequestId, decision.action === "release-not-accepted"
1051
- ? "rejected"
1052
- : "accepted");
1053
967
  return;
1054
968
  }
1055
969
  // hold without acceptedTurnId: keep pending + busy.
@@ -1072,7 +986,6 @@ export function useChatStream(params) {
1072
986
  stillPending.clientRequestId &&
1073
987
  isTerminalTurnState(state?.activeTurn?.state);
1074
988
  if (latestMatches || activeMatches) {
1075
- settleSubmissionByRequest(sessionIdentity, stillPending.clientRequestId, "accepted");
1076
989
  const termTurnId = activeMatches
1077
990
  ? (state?.activeTurn?.turnId ?? null)
1078
991
  : (state?.latestTurn?.turnId ??
@@ -1144,10 +1057,7 @@ export function useChatStream(params) {
1144
1057
  stopRetryIndex >= STOP_RECONCILE_DELAYS_MS.length ||
1145
1058
  (!refs.stopRequestedRef.current && !refs.stoppingTurnIdRef.current))
1146
1059
  return;
1147
- const delay = STOP_RECONCILE_DELAYS_MS[stopRetryIndex];
1148
- stopRetryIndex += 1;
1149
- if (delay === undefined)
1150
- return;
1060
+ const delay = STOP_RECONCILE_DELAYS_MS[stopRetryIndex++];
1151
1061
  stopRetryTimer = window.setTimeout(() => {
1152
1062
  void sync().finally(scheduleStopRetry);
1153
1063
  }, delay);
@@ -1169,7 +1079,6 @@ export function useChatStream(params) {
1169
1079
  refs,
1170
1080
  session,
1171
1081
  streaming,
1172
- settleSubmissionByRequest,
1173
1082
  setTurnPhase,
1174
1083
  markIdleIfCurrent,
1175
1084
  releaseSubmitOwnership,