@tea-agent/loop-agent 0.40.0-next.1 → 0.40.0-next.10

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 (208) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +106 -109
  3. package/README.md +2 -2
  4. package/dist/application/task-lifecycle/advance.js +12 -0
  5. package/dist/build-stamp.json +3 -3
  6. package/dist/cli/update/runtime-activity.js +1 -29
  7. package/dist/commands/dag-rerun.js +1 -1
  8. package/dist/executors/dag-pi-executor.js +69 -7
  9. package/dist/executors/pi-executor.js +4 -2
  10. package/dist/executors/pi-sdk-executor.js +46 -1
  11. package/dist/executors/pi-writer-tool-policy.js +57 -1
  12. package/dist/executors/shell-executor.js +43 -3
  13. package/dist/executors/workspace-write-snapshot.js +68 -0
  14. package/dist/infrastructure/harness/atomic-write.js +23 -0
  15. package/dist/shared/dag-prompt-override.js +4 -5
  16. package/dist/shared/update/npm-client.js +33 -11
  17. package/dist/task/config-types.js +10 -0
  18. package/dist/task/contract/apply.js +11 -1
  19. package/dist/task/contract/constants.js +2 -0
  20. package/dist/task/contract/hash.js +3 -0
  21. package/dist/task/contract/observe.js +25 -4
  22. package/dist/task/contract/paths.js +2 -1
  23. package/dist/task/contract/project.js +15 -1
  24. package/dist/task/contract/schema.js +2 -0
  25. package/dist/task/contract/transaction.js +12 -0
  26. package/dist/task/source-prepare/completeness.js +188 -0
  27. package/dist/task/source-prepare/fragment-inventory.js +474 -0
  28. package/dist/task/source-prepare/index.js +3 -0
  29. package/dist/task/source-prepare/ledger-reconciliation.js +127 -0
  30. package/dist/task/source-prepare/ledger-review.js +214 -0
  31. package/dist/task/source-prepare/ledger.js +545 -0
  32. package/dist/task/source-prepare/prepare.js +262 -1
  33. package/dist/task/source-prepare/semantic-intake.js +19 -3
  34. package/dist/task/source-prepare/source-fidelity-pi.js +384 -0
  35. package/dist/task/source-prepare/types.js +22 -0
  36. package/dist/worker/cli.js +12 -26
  37. package/dist/worker/console/chat/chat-event-store.js +75 -5
  38. package/dist/worker/console/chat/context-panel.js +7 -0
  39. package/dist/worker/console/chat/deferred-turn.js +12 -0
  40. package/dist/worker/console/chat/pi-runtime.js +331 -18
  41. package/dist/worker/console/chat/provider-error.js +98 -0
  42. package/dist/worker/console/chat/routes.js +389 -56
  43. package/dist/worker/console/chat/session-stats.js +179 -0
  44. package/dist/worker/console/chat/session-store.js +93 -63
  45. package/dist/worker/console/chat/todos.js +115 -0
  46. package/dist/worker/console/chat/tool-preview.js +42 -0
  47. package/dist/worker/console/chat/turn-process.js +20 -55
  48. package/dist/worker/console/chat/user-questions.js +266 -0
  49. package/dist/worker/console/chat/workspace-landing.js +79 -0
  50. package/dist/worker/console/console-handoff.js +82 -0
  51. package/dist/worker/console/console-update-and-init.js +115 -0
  52. package/dist/worker/console/console-update-runtime.js +84 -0
  53. package/dist/worker/console/draft-store.js +49 -0
  54. package/dist/worker/console/native-directory-picker.js +13 -0
  55. package/dist/worker/console/operation-store.js +1 -1
  56. package/dist/worker/console/operator-actions.js +129 -4
  57. package/dist/worker/console/operator-surface-health.js +1 -0
  58. package/dist/worker/console/prd-intake-bridge.js +13 -0
  59. package/dist/worker/console/routes.js +312 -1
  60. package/dist/worker/console/security.js +4 -4
  61. package/dist/worker/console/server.js +583 -172
  62. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Br92EGb7.js → abnfDiagram-N423BO3Z-Bs-CDAXM.js} +1 -1
  63. package/dist/worker/console/static/assets/{arc-oulImtQq.js → arc-CAkA3We3.js} +1 -1
  64. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-tyHc6aqO.js → architectureDiagram-T3A2C74G-DEmI_zqr.js} +1 -1
  65. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CY86BSpT.js → blockDiagram-VBNYF7ZC-uDHANebE.js} +1 -1
  66. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BKKsDmNs.js → c4Diagram-5PPSVZJV-Bv97b4_B.js} +1 -1
  67. package/dist/worker/console/static/assets/channel-amxUpk7o.js +1 -0
  68. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-CXZdjl3B.js → chunk-2GRJ4B5K-shmQKsFz.js} +1 -1
  69. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-B-aIFDQ5.js → chunk-2Q5K7J3B-BAraNvcN.js} +1 -1
  70. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BDgAW_B2.js → chunk-5RXB4S5H-BirhxUop.js} +1 -1
  71. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-DABa4UPR.js → chunk-5VM5RSS4-C9LgUJpR.js} +1 -1
  72. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-aGbysVOx.js → chunk-6Q2QTUOP-DrU-s-t7.js} +1 -1
  73. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-B8MSZDVs.js → chunk-GF5L2VYU-Tx-H_FA2.js} +1 -1
  74. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CpelV73g.js → chunk-JWPE2WC7-BjLtNFrR.js} +1 -1
  75. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BE04ty3z.js → chunk-KBJHAD2P-UNLVwSsy.js} +1 -1
  76. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BUxTkaaR.js → chunk-RYQCIY6F-CBEUX8XD.js} +1 -1
  77. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CYYBVKfX.js → chunk-XXDRQBXY-WTL7IZOq.js} +1 -1
  78. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-bejwBVIz.js +1 -0
  79. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-bejwBVIz.js +1 -0
  80. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CEQF_xmQ.js → cose-bilkent-JH36ORCC-CkHG6W3s.js} +1 -1
  81. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-CqpxN5IT.js → cynefin-VYW2F7L2-BRzFqTaL.js} +1 -1
  82. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-B3PzanVi.js → cynefinDiagram-MW4NZA55-2_CFp_i7.js} +1 -1
  83. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BSUGCNx1.js → dagre-VZM6K2ZE-BJtsmdkE.js} +1 -1
  84. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CdLevU5T.js → diagram-7IWD3JNH-DNEFFo6w.js} +1 -1
  85. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BjpF_Kok.js → diagram-B4RE2ZJO-B_IWV7eo.js} +1 -1
  86. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-C4I6NJny.js → diagram-LBJQPF4R-DvvRx2dz.js} +1 -1
  87. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-C8hecCr5.js → diagram-Q27KOJAE-CsibsWLg.js} +1 -1
  88. package/dist/worker/console/static/assets/{diagram-UB23O5K3-Daod6tOS.js → diagram-UB23O5K3-les_djBQ.js} +1 -1
  89. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BvvmazGv.js → ebnfDiagram-BXEA7PRR-BEm2n7qJ.js} +1 -1
  90. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DeN_vfV4.js → erDiagram-JOGREHBK-BI7vHHVm.js} +1 -1
  91. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-D6dq1t9z.js → flowDiagram-UKHOOZJN-CUFCERrB.js} +1 -1
  92. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DziIdNY4.js → ganttDiagram-PKOTCBZU-wxyLiGWt.js} +1 -1
  93. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dy4s5CXr.js → gitGraphDiagram-DS77QQ5N-Bt897K8Z.js} +1 -1
  94. package/dist/worker/console/static/assets/index-BHhUOTri.css +1 -0
  95. package/dist/worker/console/static/assets/index-DqJbO3-p.js +407 -0
  96. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Czv5eYx2.js → infoDiagram-6WML65LV-DYNx_IPv.js} +1 -1
  97. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-Bsk3nFRT.js → ishikawaDiagram-WSZJBQD7-CzpLNEu6.js} +1 -1
  98. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DxGmNj5K.js → journeyDiagram-NVQOT4AX-97f9owNU.js} +1 -1
  99. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CYCFqkO_.js → kanban-definition-27J2QSJJ-BxM9hyBn.js} +1 -1
  100. package/dist/worker/console/static/assets/{linear-B0YzRwrd.js → linear-ByuxcHvp.js} +1 -1
  101. package/dist/worker/console/static/assets/{mermaid.core-t3EZtIGh.js → mermaid.core-DwPGvpCv.js} +5 -5
  102. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BTXpTv0T.js → mindmap-definition-FAOFIHXS-DCoqG9RT.js} +1 -1
  103. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DU8UkKqA.js → pegDiagram-VL7TDLO6-BLSBmBMx.js} +1 -1
  104. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DP4y8ohp.js → pieDiagram-7S7Q4E2Y-B0t_0oJW.js} +1 -1
  105. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-B07vNaYR.js → quadrantDiagram-CIZ2JOQS-sTzDk7Nt.js} +1 -1
  106. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-xDWktb2i.js → railroadDiagram-AXF67PYL-B3jRdATC.js} +1 -1
  107. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BLNMKqeS.js → requirementDiagram-LRYGKXZP-D-cTtXEN.js} +1 -1
  108. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-n0adalxH.js → sankeyDiagram-W5VNT64P-B25igJRE.js} +1 -1
  109. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BbnS-2DL.js → sequenceDiagram-SI44F4Z6-jW_lM1_D.js} +1 -1
  110. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4uPpCLu.js → sizeCapture-X5ZJPWSS-CrzTgsHE.js} +1 -1
  111. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA--MMbB0fX.js → stateDiagram-OKZ733FA-B01MvlqE.js} +1 -1
  112. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BvFKEtm2.js +1 -0
  113. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BXUgp2QY.js → swimlanes-SLNWSIFB-YcF12FiU.js} +2 -2
  114. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DIB2eVg-.js +8 -0
  115. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CsaMxki7.js → timeline-definition-Z64GVDOM-ClnRcg2s.js} +1 -1
  116. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-C2IuG9iH.js → vennDiagram-T6HMQDX7-B6gr_z-q.js} +1 -1
  117. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CmEWOXCW.js → wardleyDiagram-T6FBY63Y-CvSgguGH.js} +1 -1
  118. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-D_9LE0MG.js → xychartDiagram-ELKLHX3M-23OtSZCb.js} +1 -1
  119. package/dist/worker/console/static/index.html +2 -2
  120. package/dist/worker/console/static-src/app/console-types.js +68 -4
  121. package/dist/worker/console/static-src/app/useConsoleShell.js +30 -12
  122. package/dist/worker/console/static-src/app/useOperatorActions.js +41 -6
  123. package/dist/worker/console/static-src/app/useRecoveryConsole.js +10 -11
  124. package/dist/worker/console/static-src/app/useRunProgress.js +2 -1
  125. package/dist/worker/console/static-src/app/useTaskWizard.js +74 -2
  126. package/dist/worker/console/static-src/night/useNightBoard.js +8 -5
  127. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +141 -8
  128. package/dist/worker/console/static-src/operator-chat/compaction-message.js +79 -0
  129. package/dist/worker/console/static-src/operator-chat/details-dag-actions.js +122 -0
  130. package/dist/worker/console/static-src/operator-chat/refs.js +3 -0
  131. package/dist/worker/console/static-src/operator-chat/runtime-selection-labels.js +27 -0
  132. package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +1 -2
  133. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +25 -0
  134. package/dist/worker/console/static-src/operator-chat/turn-submission.js +3 -2
  135. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +181 -87
  136. package/dist/worker/console/static-src/operator-chat/useChatStream.js +159 -44
  137. package/dist/worker/console/static-src/operator-chat/useChatThread.js +177 -8
  138. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +63 -9
  139. package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
  140. package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -3
  141. package/dist/worker/console/static-src/shell/useWorkspaces.js +299 -0
  142. package/dist/worker/console/static-src/shell/workspace-route.js +339 -0
  143. package/dist/worker/console/workspace-context.js +234 -0
  144. package/dist/worker/console/workspace-registry.js +214 -0
  145. package/dist/worker/observability/init-runtime-activity.js +26 -0
  146. package/dist/worker/observe/node-input.js +88 -2
  147. package/dist/worker/observe/routes.js +188 -0
  148. package/dist/worker/observe/server.js +4 -0
  149. package/dist/worker/observe/static/api.js +29 -9
  150. package/dist/worker/observe/static/app.js +18 -70
  151. package/dist/worker/observe/static/dag-node-purpose.d.ts +6 -0
  152. package/dist/worker/observe/static/dag-node-purpose.js +203 -0
  153. package/dist/worker/observe/static/format.js +37 -0
  154. package/dist/worker/observe/static/index.html +6 -1
  155. package/dist/worker/observe/static/inspect-workspace.js +307 -0
  156. package/dist/worker/observe/static/operator-chrome.css +145 -20
  157. package/dist/worker/observe/static/operator-chrome.d.ts +20 -3
  158. package/dist/worker/observe/static/operator-chrome.js +330 -100
  159. package/dist/worker/observe/static/prompt-restart-candidates.d.ts +18 -0
  160. package/dist/worker/observe/static/prompt-restart-candidates.js +36 -0
  161. package/dist/worker/observe/static/router.d.ts +46 -0
  162. package/dist/worker/observe/static/router.js +25 -1
  163. package/dist/worker/observe/static/state.d.ts +50 -0
  164. package/dist/worker/observe/static/state.js +174 -2
  165. package/dist/worker/observe/static/styles.css +357 -35
  166. package/dist/worker/observe/static/views/dag-graph.js +4 -2
  167. package/dist/worker/observe/static/views/dag-inspector.js +533 -79
  168. package/dist/worker/observe/static/views/dag.d.ts +13 -0
  169. package/dist/worker/observe/static/views/dag.js +6 -4
  170. package/dist/worker/observe/static/views/dashboard.js +6 -4
  171. package/dist/worker/observe/static/views/night.js +2 -2
  172. package/dist/worker/observe/static/views/pool.js +1 -0
  173. package/dist/worker/observe/static/views/run.js +3 -1
  174. package/dist/worker/observe/static/views/task.js +56 -1
  175. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +29 -1
  176. package/dist/workflows/dag/backend-test-markdown-workflow.js +176 -1
  177. package/dist/workflows/dag/backend-test-pytest-collection.js +98 -14
  178. package/dist/workflows/dag/backend-test-writer-completeness.js +17 -17
  179. package/dist/workflows/dag/dynamic-runtime/map.js +1 -0
  180. package/dist/workflows/dag/frontend-implementation-contract.js +88 -2
  181. package/dist/workflows/dag/frontend-plan-render.js +24 -0
  182. package/dist/workflows/dag/frontend-prewrite-gate.js +197 -0
  183. package/dist/workflows/dag/frontend-review-context.js +92 -10
  184. package/dist/workflows/dag/init-hybrid.js +238 -51
  185. package/dist/workflows/dag/interrupt-request.js +3 -3
  186. package/dist/workflows/dag/lifecycle.js +3 -3
  187. package/dist/workflows/dag/node-execution.js +45 -0
  188. package/dist/workflows/dag/types.js +62 -12
  189. package/docs/README.md +2 -0
  190. package/docs/architecture/evolution.md +45 -0
  191. package/docs/init-surface.manifest.json +1 -0
  192. package/docs/operations/local-development-environment.md +21 -0
  193. package/docs/templates/README.md +1 -1
  194. package/docs/templates/agent-dag.schema.json +18 -0
  195. package/docs/templates/backend-test-dag.json +265 -237
  196. package/docs/templates/frontend-implementation-contract.schema.json +681 -24
  197. package/harness.json +3 -3
  198. package/package.json +3 -1
  199. package/skills/loop-agent/references/hybrid-dag.md +7 -0
  200. package/dist/worker/console/static/assets/channel-TY36zt-i.js +0 -1
  201. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Dk7QCChM.js +0 -1
  202. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Dk7QCChM.js +0 -1
  203. package/dist/worker/console/static/assets/index-C3GgHg-g.css +0 -1
  204. package/dist/worker/console/static/assets/index-Ci8ksvBm.js +0 -406
  205. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BqOigMQ4.js +0 -1
  206. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Caxz9UA2.js +0 -8
  207. package/dist/worker/console/static-src/operator-chat/activity-rail-presentation.js +0 -73
  208. package/dist/worker/console/static-src/operator-chat/useActivityRailTransition.js +0 -59
@@ -2,7 +2,7 @@ import { useCallback, useEffect } from "react";
2
2
  import { formatOperatorUserError } from "../../operator-user-error.js";
3
3
  import { confirmationToken, requestId, sleep } from "./format.js";
4
4
  import { classifyCreateTurnNetworkFailure, classifyCreateTurnResponse, createPendingSubmission, decideSubmissionRoute, isRuntimeBusyUnion, isSubmissionCurrent, isTerminalTurnState, keepAmbiguousAfterRetry, reconcileDeferredSubmissionFromState, reconcilePendingSubmissionFromState, resolveAmbiguousFromState, } from "./turn-submission.js";
5
- import { applyAbortDecision, applyPendingReconcileDecision, classifyAbortResponse, markStopRequested, operatorChatAbortUrl, operatorChatEventsUrl, operatorChatStateUrl, operatorChatTurnsUrl, releaseOwnershipOnTerminalEvidence, releaseSubmitOwnership as releaseOwnershipBag, requestStopOnOwnership, settleCreateTurnOutcome, turnPhaseFromOwnership, } from "./turn-stream-controller.js";
5
+ import { applyAbortDecision, applyPendingReconcileDecision, classifyAbortResponse, markStopRequested, operatorChatAbortUrl, operatorChatEventsUrl, operatorChatStateUrl, operatorChatTurnsUrl, releaseOwnershipOnTerminalEvidence, releaseSubmitOwnership as releaseOwnershipBag, requestStopOnOwnership, settleCreateTurnOutcome, turnPhaseFromOwnership, visualTurnPhaseFromOwnership, } from "./turn-stream-controller.js";
6
6
  /** Project ChatRefs into the executable ownership bag used by the controller. */
7
7
  function ownershipBagFromRefs(refs) {
8
8
  return {
@@ -46,6 +46,7 @@ function sseEventName(block) {
46
46
  function sleepBriefly() {
47
47
  return new Promise((resolve) => window.setTimeout(resolve, 25));
48
48
  }
49
+ const STOP_RECONCILE_DELAYS_MS = [100, 250, 500, 1_000, 2_000];
49
50
  function clearEmptyAssistant(setMessages, assistantId) {
50
51
  if (!assistantId)
51
52
  return;
@@ -57,7 +58,7 @@ function clearEmptyAssistant(setMessages, assistantId) {
57
58
  * detached Turn resource only; it never opens a second inline SSE consumer. */
58
59
  export function useChatStream(params) {
59
60
  const { origin, refs, session, thread, composer, setError, setAutoScroll, setNotice, recoveryVersion = 0, watchSessionTitle, } = params;
60
- const { applySseBlock, setMessages, setStreaming, setStreamingAssistantId, streaming, setTurnPhase, setDeferredTurn, } = thread;
61
+ const { applySseBlock, setMessages, setStreaming, setStreamingAssistantId, streaming, setTurnPhase, setDeferredTurn, setDeferredTurns, } = thread;
61
62
  const { setInput, setPendingImages, recordHistory } = composer;
62
63
  const showNotice = useCallback((message) => {
63
64
  if (!setNotice)
@@ -73,15 +74,26 @@ export function useChatStream(params) {
73
74
  if (refs.sessionRef.current?.sessionId !== sessionId ||
74
75
  refs.sessionGenerationRef.current !== generation)
75
76
  return;
77
+ const pendingClientRequestId = refs.pendingSubmissionRef.current?.clientRequestId;
78
+ if (pendingClientRequestId)
79
+ releaseSubmitOwnership(pendingClientRequestId);
76
80
  refs.streamingRef.current = false;
77
81
  refs.activeTurnIdRef.current = null;
78
82
  refs.assistantIdRef.current = null;
79
83
  refs.stoppingTurnIdRef.current = null;
84
+ refs.stopRequestedRef.current = false;
80
85
  setStreaming(false);
81
86
  setTurnPhase("idle");
82
87
  setStreamingAssistantId(null);
83
88
  clearEmptyAssistant(setMessages, assistantId);
84
- }, [refs, setStreaming, setTurnPhase, setStreamingAssistantId, setMessages]);
89
+ }, [
90
+ refs,
91
+ releaseSubmitOwnership,
92
+ setStreaming,
93
+ setTurnPhase,
94
+ setStreamingAssistantId,
95
+ setMessages,
96
+ ]);
85
97
  /** Follow one session's event stream until it is explicitly aborted. The
86
98
  * server's `stream_ready` marker separates replay from live events; this
87
99
  * means replay cannot render duplicate messages while live text remains
@@ -243,12 +255,12 @@ export function useChatStream(params) {
243
255
  /** Issue real POST /abort for a known turnId (Stop never disposes Session). */
244
256
  const postAbortForTurn = useCallback(async (sessionId, generation, turnId) => {
245
257
  refs.stoppingTurnIdRef.current = turnId;
246
- // Optimistic visual stop: remove the running spinner immediately, but
247
- // retain ownership as `stopping` until the server proves a terminal state.
248
- // This keeps the composer responsive without falsely advertising idle.
258
+ // DSH-style visual stop: the composer becomes idle immediately. Hidden
259
+ // ownership remains stopping so a new message routes to the durable queue
260
+ // until terminal evidence proves the old Turn is gone.
249
261
  refs.streamingRef.current = false;
250
262
  setStreaming(false);
251
- setTurnPhase("stopping");
263
+ setTurnPhase("idle");
252
264
  setStreamingAssistantId(null);
253
265
  try {
254
266
  const res = await fetch(operatorChatAbortUrl(origin, sessionId, turnId), {
@@ -277,13 +289,17 @@ export function useChatStream(params) {
277
289
  const state = await fetchSessionState(sessionId);
278
290
  const stillCurrent = refs.sessionRef.current?.sessionId === sessionId &&
279
291
  refs.sessionGenerationRef.current === generation;
292
+ // Terminal SSE may settle the Turn while /state is in flight. Do not
293
+ // write the pre-request ownership snapshot back after that newer fact.
294
+ if (!stillCurrent || refs.stoppingTurnIdRef.current !== turnId)
295
+ return;
280
296
  applyAbortDecision(bag, decision, noticeSink(), {
281
- sessionStillCurrent: stillCurrent,
297
+ sessionStillCurrent: true,
282
298
  runtimeBusy: isRuntimeBusyUnion(state ?? undefined),
283
299
  markIdle: () => markIdleIfCurrent(sessionId, generation, refs.assistantIdRef.current),
284
300
  });
285
301
  applyOwnershipBagToRefs(refs, bag);
286
- setTurnPhase(turnPhaseFromOwnership(bag));
302
+ setTurnPhase(visualTurnPhaseFromOwnership(bag));
287
303
  return;
288
304
  }
289
305
  applyAbortDecision(bag, decision, noticeSink(), {
@@ -291,11 +307,7 @@ export function useChatStream(params) {
291
307
  markIdle: () => markIdleIfCurrent(sessionId, generation, refs.assistantIdRef.current),
292
308
  });
293
309
  applyOwnershipBagToRefs(refs, bag);
294
- setTurnPhase(turnPhaseFromOwnership(bag));
295
- // Sync the post-decision busy state back to React: optimistic stop
296
- // flipped it idle up front; pending/keep-busy outcomes restore it.
297
- if (bag.streaming)
298
- setStreaming(true);
310
+ setTurnPhase(visualTurnPhaseFromOwnership(bag));
299
311
  }
300
312
  catch {
301
313
  // Abort network failure: keep busy (UIS-abort-network-fail).
@@ -308,11 +320,11 @@ export function useChatStream(params) {
308
320
  networkError: true,
309
321
  }), noticeSink());
310
322
  applyOwnershipBagToRefs(refs, bag);
311
- // Network failure: the optimistic idle was premature — restore busy
312
- // so the turn is not silently dropped; /state reconcile converges.
313
- refs.streamingRef.current = true;
314
- setStreaming(true);
315
- setTurnPhase("stopping");
323
+ // Transport failure is ambiguous: keep the visual composer available,
324
+ // retain hidden ownership, and let the short /state backoff converge.
325
+ refs.streamingRef.current = false;
326
+ setStreaming(false);
327
+ setTurnPhase("idle");
316
328
  }
317
329
  }, [
318
330
  origin,
@@ -368,7 +380,13 @@ export function useChatStream(params) {
368
380
  },
369
381
  });
370
382
  applyOwnershipBagToRefs(refs, bag);
371
- setTurnPhase(turnPhaseFromOwnership(bag));
383
+ const ownershipPhase = turnPhaseFromOwnership(bag);
384
+ setTurnPhase(visualTurnPhaseFromOwnership(bag));
385
+ if (ownershipPhase === "stopping") {
386
+ refs.streamingRef.current = false;
387
+ setStreaming(false);
388
+ setStreamingAssistantId(null);
389
+ }
372
390
  }, [
373
391
  refs,
374
392
  noticeSink,
@@ -386,17 +404,18 @@ export function useChatStream(params) {
386
404
  const activeSession = refs.sessionRef.current;
387
405
  if (!activeSession || (!input.text.trim() && input.images.length === 0))
388
406
  return { action: "handled" };
389
- const existing = thread.deferredTurn;
390
407
  showNotice("正在排队…");
391
- const acceptDeferred = (deferredTurn) => {
392
- setDeferredTurn(deferredTurn);
408
+ const acceptDeferred = (deferredTurn, deferredTurns) => {
409
+ setDeferredTurn(deferredTurns?.[0] ?? deferredTurn);
410
+ setDeferredTurns(deferredTurns ?? [deferredTurn]);
393
411
  setInput("");
394
412
  setPendingImages([]);
395
413
  setError(null);
396
- showNotice(existing ? "已更新排队消息" : "已排队,将在当前回合结束后自动发送");
414
+ showNotice("已排队,将在当前回合结束后自动发送");
397
415
  };
398
416
  const acceptDispatched = () => {
399
417
  setDeferredTurn(null);
418
+ setDeferredTurns([]);
400
419
  setInput("");
401
420
  setPendingImages([]);
402
421
  setError(null);
@@ -415,7 +434,6 @@ export function useChatStream(params) {
415
434
  blockedByTurnId: input.blockedByTurnId,
416
435
  text: input.text,
417
436
  images: input.images,
418
- ...(existing ? { expectedRevision: existing.revision } : {}),
419
437
  }),
420
438
  });
421
439
  const body = (await response.json().catch(() => ({})));
@@ -425,9 +443,11 @@ export function useChatStream(params) {
425
443
  for (let attempt = 0; attempt < 2; attempt += 1) {
426
444
  const { response, body } = await putOnce();
427
445
  if (response.ok && body.accepted && body.deferredTurn) {
428
- acceptDeferred(body.deferredTurn);
446
+ acceptDeferred(body.deferredTurn, body.deferredTurns);
429
447
  return { action: "handled" };
430
448
  }
449
+ if (body.deferredTurns)
450
+ setDeferredTurns(body.deferredTurns);
431
451
  if (body.deferredTurn)
432
452
  setDeferredTurn(body.deferredTurn);
433
453
  if (body.error?.code !== "TURN_NOT_ACTIVE") {
@@ -442,7 +462,7 @@ export function useChatStream(params) {
442
462
  state: canonical ?? undefined,
443
463
  });
444
464
  if (decision.action === "deferred") {
445
- acceptDeferred(decision.deferredTurn);
465
+ acceptDeferred(decision.deferredTurn, canonical?.deferredTurns);
446
466
  return { action: "handled" };
447
467
  }
448
468
  if (decision.action === "accepted") {
@@ -451,7 +471,7 @@ export function useChatStream(params) {
451
471
  }
452
472
  if (decision.action === "retry-deferred" && attempt === 0)
453
473
  continue;
454
- if (decision.action === "send-now" && !existing) {
474
+ if (decision.action === "send-now") {
455
475
  showNotice("上一轮已停止,正在发送当前消息…");
456
476
  return { action: "send-now" };
457
477
  }
@@ -468,14 +488,14 @@ export function useChatStream(params) {
468
488
  state: canonical ?? undefined,
469
489
  });
470
490
  if (decision.action === "deferred") {
471
- acceptDeferred(decision.deferredTurn);
491
+ acceptDeferred(decision.deferredTurn, canonical?.deferredTurns);
472
492
  return { action: "handled" };
473
493
  }
474
494
  if (decision.action === "accepted") {
475
495
  acceptDispatched();
476
496
  return { action: "handled" };
477
497
  }
478
- if (decision.action === "send-now" && !existing) {
498
+ if (decision.action === "send-now") {
479
499
  showNotice("上一轮已停止,正在发送当前消息…");
480
500
  return { action: "send-now" };
481
501
  }
@@ -489,6 +509,7 @@ export function useChatStream(params) {
489
509
  refs,
490
510
  thread.deferredTurn,
491
511
  setDeferredTurn,
512
+ setDeferredTurns,
492
513
  setInput,
493
514
  setPendingImages,
494
515
  setError,
@@ -497,9 +518,11 @@ export function useChatStream(params) {
497
518
  ]);
498
519
  /** DELETE the queued turn; "restore" moves its text back into the composer,
499
520
  * "discard" drops it entirely (2026-08-23 queue chip actions). */
500
- const requestDeferredTurnDelete = useCallback(async (mode) => {
521
+ const requestDeferredTurnDelete = useCallback(async (mode, target) => {
501
522
  const activeSession = refs.sessionRef.current;
502
- const deferred = thread.deferredTurn;
523
+ const deferred = target
524
+ ? thread.deferredTurns.find((item) => item.clientRequestId === target.clientRequestId)
525
+ : thread.deferredTurns[0] ?? thread.deferredTurn;
503
526
  if (!activeSession || !deferred)
504
527
  return;
505
528
  try {
@@ -520,10 +543,12 @@ export function useChatStream(params) {
520
543
  setError(body.error?.message ?? "待发送消息撤回失败");
521
544
  return;
522
545
  }
523
- setDeferredTurn(null);
546
+ const remaining = thread.deferredTurns.filter((item) => item.clientRequestId !== deferred.clientRequestId);
547
+ setDeferredTurns(remaining);
548
+ setDeferredTurn(remaining[0] ?? null);
524
549
  if (mode === "restore") {
525
- setInput(body.deferredTurn?.text ?? "");
526
- setPendingImages(body.deferredTurn?.images ?? []);
550
+ setInput(deferred.text ?? body.deferredTurn?.text ?? "");
551
+ setPendingImages(deferred.images ?? body.deferredTurn?.images ?? []);
527
552
  showNotice("待发送消息已移回输入框");
528
553
  }
529
554
  else {
@@ -538,14 +563,66 @@ export function useChatStream(params) {
538
563
  origin,
539
564
  refs,
540
565
  thread.deferredTurn,
566
+ thread.deferredTurns,
541
567
  setDeferredTurn,
568
+ setDeferredTurns,
542
569
  setInput,
543
570
  setPendingImages,
544
571
  setError,
545
572
  showNotice,
546
573
  ]);
547
574
  const cancelDeferredTurn = useCallback(() => requestDeferredTurnDelete("restore"), [requestDeferredTurnDelete]);
548
- const discardDeferredTurn = useCallback(() => requestDeferredTurnDelete("discard"), [requestDeferredTurnDelete]);
575
+ const discardDeferredTurn = useCallback((target) => requestDeferredTurnDelete("discard", target), [requestDeferredTurnDelete]);
576
+ const editDeferredTurn = useCallback(async (target, text) => {
577
+ const activeSession = refs.sessionRef.current;
578
+ if (!activeSession)
579
+ return;
580
+ const item = thread.deferredTurns.find((entry) => entry.clientRequestId === target.clientRequestId);
581
+ if (!item)
582
+ return;
583
+ try {
584
+ const response = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(activeSession.sessionId)}/deferred-turn`, {
585
+ method: "PUT",
586
+ headers: { "Content-Type": "application/json", "x-loop-console-confirmation": confirmationToken() },
587
+ credentials: "include",
588
+ body: JSON.stringify({ clientRequestId: target.clientRequestId, blockedByTurnId: item.blockedByTurnId, text, images: item.images ?? [], expectedRevision: target.revision }),
589
+ });
590
+ const body = (await response.json().catch(() => ({})));
591
+ if (!response.ok)
592
+ throw new Error(body.error?.message ?? "编辑排队消息失败");
593
+ if (body.deferredTurns)
594
+ setDeferredTurns(body.deferredTurns);
595
+ if (body.deferredTurn)
596
+ setDeferredTurn(body.deferredTurn);
597
+ setError(null);
598
+ }
599
+ catch (error) {
600
+ setError(error instanceof Error ? error.message : "编辑排队消息失败");
601
+ }
602
+ }, [origin, refs, thread.deferredTurns, setDeferredTurn, setDeferredTurns, setError]);
603
+ const steerDeferredTurn = useCallback(async (target) => {
604
+ const activeSession = refs.sessionRef.current;
605
+ if (!activeSession)
606
+ return;
607
+ try {
608
+ const response = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(activeSession.sessionId)}/deferred-turn/${encodeURIComponent(target.clientRequestId)}/steer`, {
609
+ method: "POST",
610
+ headers: { "Content-Type": "application/json", "x-loop-console-confirmation": confirmationToken() },
611
+ credentials: "include",
612
+ body: JSON.stringify({ expectedRevision: target.revision }),
613
+ });
614
+ const body = (await response.json().catch(() => ({})));
615
+ if (!response.ok)
616
+ throw new Error(body.error?.message ?? "插话发送失败");
617
+ const remaining = body.deferredTurns ?? thread.deferredTurns.filter((item) => item.clientRequestId !== target.clientRequestId);
618
+ setDeferredTurns(remaining);
619
+ setDeferredTurn(remaining[0] ?? null);
620
+ setError(null);
621
+ }
622
+ catch (error) {
623
+ setError(error instanceof Error ? error.message : "插话发送失败");
624
+ }
625
+ }, [origin, refs, thread.deferredTurns, setDeferredTurn, setDeferredTurns, setError]);
549
626
  const sendPrompt = useCallback(async (text, images) => {
550
627
  const initialSession = refs.sessionRef.current;
551
628
  if (!initialSession || (!text.trim() && images.length === 0))
@@ -747,10 +824,12 @@ export function useChatStream(params) {
747
824
  // Decide + mark intent/coalesce only; POST is issued once below.
748
825
  const stopResult = requestStopOnOwnership(bag, {
749
826
  postAbort: () => { },
750
- notice: noticeSink(),
751
827
  });
752
828
  applyOwnershipBagToRefs(refs, bag);
753
- setTurnPhase("stopping");
829
+ refs.streamingRef.current = false;
830
+ setStreaming(false);
831
+ setStreamingAssistantId(null);
832
+ setTurnPhase("idle");
754
833
  // Immediate abort already scheduled for a known turnId — coalesce via
755
834
  // abortPostedTurnIds; stopInFlight only gates concurrent in-flight POSTs.
756
835
  if (stopResult.abortedTurnId) {
@@ -800,7 +879,6 @@ export function useChatStream(params) {
800
879
  markStopRequested(lateBag);
801
880
  const late = requestStopOnOwnership(lateBag, {
802
881
  postAbort: () => { },
803
- notice: noticeSink(),
804
882
  });
805
883
  applyOwnershipBagToRefs(refs, lateBag);
806
884
  if (late.abortedTurnId) {
@@ -810,7 +888,13 @@ export function useChatStream(params) {
810
888
  finally {
811
889
  refs.stopInFlightRef.current = false;
812
890
  }
813
- }, [refs, noticeSink, postAbortForTurn]);
891
+ }, [
892
+ refs,
893
+ postAbortForTurn,
894
+ setStreaming,
895
+ setStreamingAssistantId,
896
+ setTurnPhase,
897
+ ]);
814
898
  // State reconciliation is only a status fence: it never consumes/creates
815
899
  // message events, and a generation check discards late responses (M5.3).
816
900
  // Busy truth = durable activeTurn union runtime busy facts (AC-003/AC-006).
@@ -830,7 +914,8 @@ export function useChatStream(params) {
830
914
  const sessionId = session.sessionId;
831
915
  const generation = refs.sessionGenerationRef.current;
832
916
  let cancelled = false;
833
- const sync = async () => {
917
+ let syncInFlight = null;
918
+ const runSync = async () => {
834
919
  try {
835
920
  const res = await fetch(operatorChatStateUrl(origin, sessionId), {
836
921
  credentials: "include",
@@ -859,10 +944,16 @@ export function useChatStream(params) {
859
944
  },
860
945
  });
861
946
  applyOwnershipBagToRefs(refs, bag);
862
- setTurnPhase(turnPhaseFromOwnership(bag));
947
+ const ownershipPhase = turnPhaseFromOwnership(bag);
948
+ setTurnPhase(visualTurnPhaseFromOwnership(bag));
949
+ if (ownershipPhase === "stopping") {
950
+ refs.streamingRef.current = false;
951
+ setStreaming(false);
952
+ setStreamingAssistantId(null);
953
+ }
863
954
  if (state && "deferredTurn" in state)
864
955
  setDeferredTurn(state.deferredTurn ?? null);
865
- if (result === "bound") {
956
+ if (result === "bound" && ownershipPhase !== "stopping") {
866
957
  // US-BUSY-ACTIVE-BOUND: keep streaming/busy with same id.
867
958
  setStreaming(true);
868
959
  return;
@@ -940,19 +1031,41 @@ export function useChatStream(params) {
940
1031
  // offline/background case: interval, visibility and online retry.
941
1032
  }
942
1033
  };
1034
+ const sync = () => {
1035
+ if (syncInFlight)
1036
+ return syncInFlight;
1037
+ syncInFlight = runSync().finally(() => {
1038
+ syncInFlight = null;
1039
+ });
1040
+ return syncInFlight;
1041
+ };
943
1042
  const onVisible = () => {
944
1043
  if (document.visibilityState === "visible")
945
1044
  void sync();
946
1045
  };
947
1046
  const onOnline = () => void sync();
948
1047
  const interval = window.setInterval(() => void sync(), 15_000);
1048
+ let stopRetryTimer;
1049
+ let stopRetryIndex = 0;
1050
+ const scheduleStopRetry = () => {
1051
+ if (cancelled ||
1052
+ stopRetryIndex >= STOP_RECONCILE_DELAYS_MS.length ||
1053
+ (!refs.stopRequestedRef.current && !refs.stoppingTurnIdRef.current))
1054
+ return;
1055
+ const delay = STOP_RECONCILE_DELAYS_MS[stopRetryIndex++];
1056
+ stopRetryTimer = window.setTimeout(() => {
1057
+ void sync().finally(scheduleStopRetry);
1058
+ }, delay);
1059
+ };
949
1060
  document.addEventListener("visibilitychange", onVisible);
950
1061
  window.addEventListener("online", onOnline);
951
1062
  // Immediate tick so terminal-before-streaming-false races converge promptly.
952
- void sync();
1063
+ void sync().finally(scheduleStopRetry);
953
1064
  return () => {
954
1065
  cancelled = true;
955
1066
  window.clearInterval(interval);
1067
+ if (stopRetryTimer !== undefined)
1068
+ window.clearTimeout(stopRetryTimer);
956
1069
  document.removeEventListener("visibilitychange", onVisible);
957
1070
  window.removeEventListener("online", onOnline);
958
1071
  };
@@ -988,6 +1101,8 @@ export function useChatStream(params) {
988
1101
  handleStop,
989
1102
  cancelDeferredTurn,
990
1103
  discardDeferredTurn,
1104
+ editDeferredTurn,
1105
+ steerDeferredTurn,
991
1106
  openEventsStream,
992
1107
  };
993
1108
  }