@tea-agent/loop-agent 0.44.0-next.1 → 0.44.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 (226) hide show
  1. package/CHANGELOG.md +379 -416
  2. package/dist/application/evaluation/budget.js +21 -0
  3. package/dist/application/evaluation/corpus-hash.js +10 -15
  4. package/dist/application/evaluation/corpus.js +2 -1
  5. package/dist/application/evaluation/frontend-browser-acceptance.js +77 -0
  6. package/dist/application/evaluation/frontend-corpus-browser-acceptance.js +175 -0
  7. package/dist/application/evaluation/frontend-gateway-evidence.js +226 -0
  8. package/dist/application/evaluation/frontend-pair-registration.js +143 -0
  9. package/dist/application/evaluation/frontend-paired-summary.js +150 -0
  10. package/dist/application/evaluation/frontend-run-observation.js +144 -0
  11. package/dist/application/evaluation/frontend-shared-evidence.js +105 -0
  12. package/dist/application/evaluation/types.js +45 -12
  13. package/dist/application/task-lifecycle/observe.js +26 -1
  14. package/dist/build-stamp.json +3 -3
  15. package/dist/cli/command-definitions.js +11 -0
  16. package/dist/cli/program.js +4 -0
  17. package/dist/commands/task-advance.js +3 -5
  18. package/dist/commands/task-source-prepare.js +13 -1
  19. package/dist/executors/dag-pi/guards.js +4 -0
  20. package/dist/executors/dag-pi/model-config.js +30 -0
  21. package/dist/executors/dag-pi/plan/fact-adoption.js +55 -0
  22. package/dist/{workflows/dag/frontend-plan-completeness.js → executors/dag-pi/plan/facts.js} +22 -28
  23. package/dist/executors/dag-pi/plan/finalize-tools.js +259 -0
  24. package/dist/executors/dag-pi/plan/ledger-contract.js +55 -0
  25. package/dist/executors/dag-pi/plan/record-tools.js +861 -0
  26. package/dist/executors/dag-pi/plan/schema.js +129 -0
  27. package/dist/executors/dag-pi/plan/session.js +1 -0
  28. package/dist/executors/dag-pi/plan/tool-helpers.js +16 -0
  29. package/dist/executors/dag-pi/tools/contract-tools.js +515 -0
  30. package/dist/executors/dag-pi/tools/design-terminal-tools.js +159 -0
  31. package/dist/executors/dag-pi/tools/plan-decision-tools.js +529 -0
  32. package/dist/executors/dag-pi/tools/review-terminal-tools.js +159 -0
  33. package/dist/executors/dag-pi/tools/scout-evidence-tools.js +270 -0
  34. package/dist/executors/dag-pi-executor.js +398 -3384
  35. package/dist/executors/pi-executor.js +6 -1
  36. package/dist/executors/pi-sdk-executor.js +112 -19
  37. package/dist/executors/shell-executor.js +88 -12
  38. package/dist/executors/shell-presets.js +1 -1
  39. package/dist/executors/shell-write-guard.js +26 -3
  40. package/dist/infrastructure/console/operation-store.js +48 -14
  41. package/dist/infrastructure/evaluation/corpus-store.js +37 -12
  42. package/dist/shared/file-identity.js +23 -0
  43. package/dist/shared/git-workspace-status.js +482 -0
  44. package/dist/shared/operator/capabilities.js +10 -5
  45. package/dist/shared/path-safety.js +2 -2
  46. package/dist/shared/wizard-local-telemetry.js +51 -0
  47. package/dist/task/config-types.js +22 -8
  48. package/dist/task/contract/project.js +13 -0
  49. package/dist/task/contract/schema.js +2 -7
  50. package/dist/task/frontend-preflight.js +1 -1
  51. package/dist/task/source-prepare/parse-intent.js +124 -5
  52. package/dist/task/source-prepare/prepare.js +6 -1
  53. package/dist/task/source-prepare/semantic-intake.js +18 -3
  54. package/dist/task/source-prepare/source-execution.js +65 -0
  55. package/dist/task/source-prepare/source-fidelity-pi.js +22 -9
  56. package/dist/task/source-prepare/source-provider-budget.js +290 -0
  57. package/dist/worker/console/chat/assistant-content.js +5 -34
  58. package/dist/worker/console/chat/chat-event-store.js +62 -10
  59. package/dist/worker/console/chat/context-insights.js +723 -0
  60. package/dist/worker/console/chat/conversation-nodes.js +383 -0
  61. package/dist/worker/console/chat/db-connections.js +854 -0
  62. package/dist/worker/console/chat/pi-runtime/custom-tools/operator-tools.js +1 -1
  63. package/dist/worker/console/chat/pi-runtime/custom-tools/sql-tools.js +655 -0
  64. package/dist/worker/console/chat/pi-runtime.js +445 -36
  65. package/dist/worker/console/chat/provider-error.js +0 -12
  66. package/dist/worker/console/chat/repo-browser.js +52 -4
  67. package/dist/worker/console/chat/routes.js +577 -72
  68. package/dist/worker/console/chat/sdd-data-alignment.js +8 -31
  69. package/dist/worker/console/chat/session-store.js +147 -19
  70. package/dist/worker/console/chat/shortcuts.js +7 -0
  71. package/dist/worker/console/chat/timeline-message-order.js +22 -0
  72. package/dist/worker/console/chat/tools.js +34 -0
  73. package/dist/worker/console/chat/turn-process.js +237 -66
  74. package/dist/worker/console/chat/user-turn-outline.js +19 -0
  75. package/dist/worker/console/console-update-runtime.js +2 -2
  76. package/dist/worker/console/dag-confirmation.js +3 -0
  77. package/dist/worker/console/dag-execution-receipt.js +3 -0
  78. package/dist/worker/console/operator-actions.js +32 -3
  79. package/dist/worker/console/operator-user-error.js +28 -1
  80. package/dist/worker/console/pi-readiness.js +4 -4
  81. package/dist/worker/console/prd-intake-bridge.js +5 -12
  82. package/dist/worker/console/recovery-cta.js +14 -14
  83. package/dist/worker/console/server.js +5 -1
  84. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-8-j6y-sd.js → abnfDiagram-N423BO3Z-r1NAPU_5.js} +1 -1
  85. package/dist/worker/console/static/assets/{arc-eoQiMvuk.js → arc-dT5T4sVL.js} +1 -1
  86. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-C4A3uMcI.js → architectureDiagram-T3A2C74G-CP_cOq46.js} +1 -1
  87. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-D4zD2F-Q.js → blockDiagram-VBNYF7ZC-sToEZxEf.js} +1 -1
  88. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-j1RkJziL.js → c4Diagram-5PPSVZJV-DSxwwi2m.js} +1 -1
  89. package/dist/worker/console/static/assets/channel-CyBY_yGk.js +1 -0
  90. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-YBHmhik1.js → chunk-2GRJ4B5K-B9fh0IkF.js} +1 -1
  91. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-COfWyo9P.js → chunk-2Q5K7J3B-CAUNl-A6.js} +1 -1
  92. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-I99OUkHY.js → chunk-5RXB4S5H-BomeqbNX.js} +1 -1
  93. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-XKxoJNJ4.js → chunk-5VM5RSS4-BNAuTOLE.js} +1 -1
  94. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DLT_cYx1.js → chunk-6Q2QTUOP-DfzjRD5y.js} +1 -1
  95. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CxcKZ9mV.js → chunk-GF5L2VYU-C3uE-yc2.js} +1 -1
  96. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-V1EqXdjY.js → chunk-JWPE2WC7-C3sQMMYr.js} +1 -1
  97. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DebTtdFp.js → chunk-KBJHAD2P-D_AtEYQL.js} +1 -1
  98. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-B-ivNRDf.js → chunk-RYQCIY6F-DW4TwhKv.js} +1 -1
  99. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DC_Ds11b.js → chunk-XXDRQBXY-CTQDmJcp.js} +1 -1
  100. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-D5lN4E_E.js +1 -0
  101. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-D5lN4E_E.js +1 -0
  102. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-sWEqKwIb.js → cose-bilkent-JH36ORCC-iMqPuBsL.js} +1 -1
  103. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BXa_dcu4.js → cynefin-VYW2F7L2-DktIuDWs.js} +1 -1
  104. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-C-Mle84F.js → cynefinDiagram-MW4NZA55-BgVnjyEs.js} +1 -1
  105. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CXPDBITe.js → dagre-VZM6K2ZE-CR25E4bu.js} +1 -1
  106. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CC-WJQfa.js → diagram-7IWD3JNH-CPM2h5HY.js} +1 -1
  107. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CDAV5Vs4.js → diagram-B4RE2ZJO-DqF0x2g1.js} +1 -1
  108. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CmFiAcNz.js → diagram-LBJQPF4R-G8BYp2eb.js} +1 -1
  109. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DPBZHuyn.js → diagram-Q27KOJAE-BbC4kT_D.js} +1 -1
  110. package/dist/worker/console/static/assets/{diagram-UB23O5K3-jUlm_Ds3.js → diagram-UB23O5K3-CKZwZ-G5.js} +1 -1
  111. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DWhQ3mfY.js → ebnfDiagram-BXEA7PRR-kJDmheia.js} +1 -1
  112. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-Tr2gMqet.js → erDiagram-JOGREHBK-DFTVHyxB.js} +1 -1
  113. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-DJjVQHPA.js → flowDiagram-UKHOOZJN-BqdN5_os.js} +1 -1
  114. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-D74dQ4u0.js → ganttDiagram-PKOTCBZU-SYDjVakh.js} +1 -1
  115. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DwW0tZ0X.js → gitGraphDiagram-DS77QQ5N-Bf8bNpGA.js} +1 -1
  116. package/dist/worker/console/static/assets/index-B28Onmfy.css +1 -0
  117. package/dist/worker/console/static/assets/index-CTMZsDmh.js +486 -0
  118. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-ILCbxJyb.js → infoDiagram-6WML65LV-DIeAmJ45.js} +1 -1
  119. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-DeuWBQ89.js → ishikawaDiagram-WSZJBQD7-oikqUAlo.js} +1 -1
  120. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-CF5ih8Fk.js → journeyDiagram-NVQOT4AX-mKEF_xYT.js} +1 -1
  121. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-C7yOSuRO.js → kanban-definition-27J2QSJJ-BdWJHPnN.js} +1 -1
  122. package/dist/worker/console/static/assets/{linear-BDZ9riWi.js → linear-DGRlieMH.js} +1 -1
  123. package/dist/worker/console/static/assets/{mermaid.core-7pKqYtpZ.js → mermaid.core-4P9yPC9c.js} +5 -5
  124. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-LeJDybSU.js → mindmap-definition-FAOFIHXS-5PTSkos6.js} +1 -1
  125. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BJvT3pMD.js → pegDiagram-VL7TDLO6-DONIFpG5.js} +1 -1
  126. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-_rGqpMan.js → pieDiagram-7S7Q4E2Y-D_Z4S70M.js} +1 -1
  127. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-DPcSv5aA.js → quadrantDiagram-CIZ2JOQS-CAsjh8iY.js} +1 -1
  128. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Drxx4hkJ.js → railroadDiagram-AXF67PYL-Cx4Dd6ac.js} +1 -1
  129. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BCTUdU4z.js → requirementDiagram-LRYGKXZP-cQb7mwyH.js} +1 -1
  130. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-B6wzbZmB.js → sankeyDiagram-W5VNT64P-Cd-DhUdf.js} +1 -1
  131. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BGt8d3QQ.js → sequenceDiagram-SI44F4Z6-AlKYRgIk.js} +1 -1
  132. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-7nlhJKo7.js → sizeCapture-X5ZJPWSS-CUy_ET4e.js} +1 -1
  133. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Cv2stqMA.js → stateDiagram-OKZ733FA-CTuVe6Sa.js} +1 -1
  134. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BsWhs8O7.js +1 -0
  135. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-qDAo4Yc1.js → swimlanes-SLNWSIFB-ChFLJb-v.js} +2 -2
  136. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CJcioYGH.js +8 -0
  137. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CKbDNKTr.js → timeline-definition-Z64GVDOM-DZZeiAPr.js} +1 -1
  138. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DI-9EHic.js → vennDiagram-T6HMQDX7-t4O91cZU.js} +1 -1
  139. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-BrzzRDpX.js → wardleyDiagram-T6FBY63Y-CV5qGHZu.js} +1 -1
  140. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-BclH5hGh.js → xychartDiagram-ELKLHX3M-DyXbytUI.js} +1 -1
  141. package/dist/worker/console/static/index.html +2 -2
  142. package/dist/worker/console/static-src/app/console-types.js +9 -1
  143. package/dist/worker/console/static-src/operator-chat/animation-frame-batcher.js +38 -0
  144. package/dist/worker/console/static-src/operator-chat/chat-link.js +20 -0
  145. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +186 -5
  146. package/dist/worker/console/static-src/operator-chat/chat-stream-continuity.js +101 -0
  147. package/dist/worker/console/static-src/operator-chat/compaction-message.js +18 -3
  148. package/dist/worker/console/static-src/operator-chat/context-insights-store.js +64 -0
  149. package/dist/worker/console/static-src/operator-chat/details-rail-surfaces.js +98 -0
  150. package/dist/worker/console/static-src/operator-chat/file-palette-nav.js +81 -0
  151. package/dist/worker/console/static-src/operator-chat/pending-user-message.js +17 -3
  152. package/dist/worker/console/static-src/operator-chat/process-label.js +27 -5
  153. package/dist/worker/console/static-src/operator-chat/tools-catalog.js +16 -7
  154. package/dist/worker/console/static-src/operator-chat/transcript-mode.js +21 -0
  155. package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +4 -1
  156. package/dist/worker/console/static-src/operator-chat/turn-process-disclosure.js +13 -5
  157. package/dist/worker/console/static-src/operator-chat/turn-state-projection.js +166 -0
  158. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +56 -2
  159. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +211 -40
  160. package/dist/worker/console/static-src/operator-chat/useChatStream.js +124 -27
  161. package/dist/worker/console/static-src/operator-chat/useChatThread.js +88 -20
  162. package/dist/worker/console/static-src/operator-chat/useComposer.js +80 -6
  163. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +13 -3
  164. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +36 -17
  165. package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +18 -0
  166. package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
  167. package/dist/worker/console/static-src/shell/console-update-reload.js +1 -1
  168. package/dist/worker/console/workspace-context.js +2 -0
  169. package/dist/worker/console/workspace-initialization.js +3 -3
  170. package/dist/worker/observability/read-model.js +23 -2
  171. package/dist/worker/observe/node-context-insights.js +479 -0
  172. package/dist/worker/observe/static/console-theme.css +174 -0
  173. package/dist/worker/observe/static/console-theme.js +36 -0
  174. package/dist/worker/observe/static/operator-chrome.css +85 -0
  175. package/dist/worker/observe/static/operator-chrome.js +51 -0
  176. package/dist/worker/scheduler/scheduled-goal-dispatch.js +3 -2
  177. package/dist/worker/scheduler/scheduled-goal-evidence.js +66 -3
  178. package/dist/worker/scheduler/scheduled-goal-store.js +10 -0
  179. package/dist/workflows/dag/budget-enforcement.js +31 -2
  180. package/dist/workflows/dag/frontend-closeout.js +3 -1
  181. package/dist/workflows/dag/frontend-contract-facts.js +11 -8
  182. package/dist/workflows/dag/frontend-design-policy.js +6 -6
  183. package/dist/workflows/dag/frontend-durable-tools.js +90 -70
  184. package/dist/workflows/dag/frontend-implementation-contract.js +146 -57
  185. package/dist/workflows/dag/frontend-plan-canary.js +66 -16
  186. package/dist/workflows/dag/frontend-plan-decision-contract.js +13 -21
  187. package/dist/workflows/dag/frontend-plan-progress.js +92 -0
  188. package/dist/workflows/dag/frontend-recovery-plan.js +10 -5
  189. package/dist/workflows/dag/frontend-recovery-run.js +51 -46
  190. package/dist/workflows/dag/frontend-repair-assertions.js +36 -0
  191. package/dist/workflows/dag/frontend-review-context.js +29 -1
  192. package/dist/workflows/dag/frontend-review-scopes.js +92 -2
  193. package/dist/workflows/dag/frontend-risk.js +8 -3
  194. package/dist/workflows/dag/frontend-root-observation.js +261 -0
  195. package/dist/workflows/dag/frontend-session-budget.js +52 -39
  196. package/dist/workflows/dag/frontend-session-context.js +10 -0
  197. package/dist/workflows/dag/frontend-test-execution-evidence.js +206 -48
  198. package/dist/workflows/dag/frontend-typed-event-store.js +11 -1
  199. package/dist/workflows/dag/frontend-verification-trace.js +6 -0
  200. package/dist/workflows/dag/frontend-writer-admission.js +3 -1
  201. package/dist/workflows/dag/init-hybrid.js +108 -37
  202. package/dist/workflows/dag/node-execution.js +11 -11
  203. package/dist/workflows/dag/output-protocol.js +1 -0
  204. package/dist/workflows/dag/rerun-feedback.js +35 -4
  205. package/dist/workflows/dag/rerun-task.js +62 -66
  206. package/dist/workflows/dag/runner.js +58 -12
  207. package/dist/workflows/dag/types.js +15 -8
  208. package/dist/workflows/loop/benchmark.js +11 -8
  209. package/docs/README.md +1 -1
  210. package/docs/architecture/runtime-boundaries.md +1 -1
  211. package/docs/governance/harness-methodology-verification.md +11 -0
  212. package/docs/templates/backend-test-dag.json +5 -4
  213. package/docs/templates/frontend-implementation-contract.schema.json +31 -11
  214. package/package.json +6 -4
  215. package/skills/frontend-design-review/SKILL.md +8 -11
  216. package/skills/frontend-design-review/references/review-checklist.md +3 -4
  217. package/skills/frontend-review/SKILL.md +5 -1
  218. package/skills/loop-agent/references/command-reference.md +8 -0
  219. package/skills/loop-agent/references/hybrid-dag.md +1 -1
  220. package/dist/worker/console/static/assets/channel-ChE7y-cx.js +0 -1
  221. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C01TCf2X.js +0 -1
  222. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C01TCf2X.js +0 -1
  223. package/dist/worker/console/static/assets/index-BWkIfcrK.css +0 -1
  224. package/dist/worker/console/static/assets/index-Cdkvw_H6.js +0 -469
  225. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DC7V5vcq.js +0 -1
  226. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Bwy4QUTO.js +0 -8
@@ -1,4 +1,4 @@
1
- import { useCallback, useEffect, useRef, useState } from "react";
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
2
  import { runtimeSelectionFromRecord } from "../../chat/runtime-selection.js";
3
3
  import { hydrateSessionThread } from "../../chat/turn-process.js";
4
4
  import { forgetActiveChatSession, readActiveChatSessionId, rememberActiveChatSession, } from "../../chat/workspace-landing.js";
@@ -8,6 +8,47 @@ import { isLostChatSessionError } from "./runtime-snapshot-store.js";
8
8
  import { upsertCompactionMessage, } from "./compaction-message.js";
9
9
  import { describeMutationGateFailure, isMutationGateFailureCode, isStaleTokenFailure, } from "./mutation-gate.js";
10
10
  import { SessionTitleWatcher } from "./session-title-watcher.js";
11
+ function isRecord(value) {
12
+ return typeof value === "object" && value !== null && !Array.isArray(value);
13
+ }
14
+ function isSessionListItem(value) {
15
+ if (!isRecord(value))
16
+ return false;
17
+ const optionalString = (field) => field === undefined || typeof field === "string";
18
+ return (typeof value.sessionId === "string" &&
19
+ value.sessionId.trim().length > 0 &&
20
+ (value.state === "active" || value.state === "archived") &&
21
+ typeof value.updatedAt === "string" &&
22
+ optionalString(value.workspaceId) &&
23
+ optionalString(value.title) &&
24
+ optionalString(value.displayTitle) &&
25
+ optionalString(value.activeTurnId) &&
26
+ (value.isBlank === undefined || typeof value.isBlank === "boolean") &&
27
+ (value.sessionMode === undefined ||
28
+ value.sessionMode === "workflow" ||
29
+ value.sessionMode === "pi") &&
30
+ (value.turnPhase === undefined ||
31
+ value.turnPhase === "idle" ||
32
+ value.turnPhase === "running" ||
33
+ value.turnPhase === "stopping"));
34
+ }
35
+ /** A 2xx response is authoritative only after its versioned payload validates. */
36
+ export function parseSessionListBaseline(value) {
37
+ if (!isRecord(value) || value.schemaVersion !== 2) {
38
+ throw new Error("会话列表响应版本无效");
39
+ }
40
+ if (!Array.isArray(value.sessions) || !value.sessions.every(isSessionListItem)) {
41
+ throw new Error("会话列表响应缺少有效的 sessions");
42
+ }
43
+ const workspaceErrors = value.workspaceErrors ?? [];
44
+ if (!Array.isArray(workspaceErrors) || !workspaceErrors.every(isRecord)) {
45
+ throw new Error("会话列表响应包含无效的 workspaceErrors");
46
+ }
47
+ return {
48
+ sessions: value.sessions,
49
+ workspaceErrors: workspaceErrors,
50
+ };
51
+ }
11
52
  /** Pure list mutations keep optimistic UI deterministic and easy to verify. */
12
53
  export function insertOptimisticSession(list, row) {
13
54
  return [row, ...list.filter((item) => item.sessionId !== row.sessionId)];
@@ -179,10 +220,11 @@ function mergeCompactionMessages(messages, compactions) {
179
220
  * compact, plus browser-refresh recovery from the durable session store. */
180
221
  export function useChatSessions(params) {
181
222
  const { origin, refs, setSession, thread, composer, browser, setError, setNotice, onCompactionChange, setProcessOpen, setAutoScroll, } = params;
182
- const { applyActivatedSession, resetForNewSession, setMessages } = thread;
223
+ const { applyActivatedSession, resetForNewSession, setMessages, refreshSessionTelemetry, } = thread;
183
224
  const { seedSession, releaseSession } = composer;
184
225
  const { setPreviews, setActivePreviewPath } = browser;
185
226
  const { workspaceId, blocked } = params;
227
+ const visibleWorkspaceIds = useMemo(() => new Set(params.visibleWorkspaceIds), [params.visibleWorkspaceIds]);
186
228
  const workspaceQuery = workspaceId
187
229
  ? `?workspaceId=${encodeURIComponent(workspaceId)}`
188
230
  : "";
@@ -198,11 +240,19 @@ export function useChatSessions(params) {
198
240
  return owner ? `?workspaceId=${encodeURIComponent(owner)}` : "";
199
241
  }, [refs]);
200
242
  const [sessionList, setSessionList] = useState([]);
243
+ const [sessionListStatus, setSessionListStatus] = useState({
244
+ phase: "pending",
245
+ state: "idle",
246
+ error: null,
247
+ });
201
248
  const [selectedSession, setSelectedSession] = useState(null);
202
249
  const selectedSessionRef = useRef(null);
203
250
  selectedSessionRef.current = selectedSession;
204
251
  const sessionListRef = useRef([]);
205
252
  const sessionListRequestVersionRef = useRef(0);
253
+ const sessionListRefreshRef = useRef(null);
254
+ const sessionListRefreshQueuedRef = useRef(false);
255
+ const sessionListGlobalErrorRef = useRef(null);
206
256
  const pendingDeletedIdsRef = useRef(new Set());
207
257
  const pendingCreatedRowsRef = useRef(new Map());
208
258
  const sessionMutationVersionRef = useRef(0);
@@ -256,43 +306,101 @@ export function useChatSessions(params) {
256
306
  setProcessOpen,
257
307
  setAutoScroll,
258
308
  ]);
259
- const refreshSessionList = useCallback(async () => {
309
+ const refreshSessionList = useCallback(() => {
260
310
  if (blocked)
261
- return; // AC-R5-002: fail-closed, zero session requests
262
- const version = ++sessionListRequestVersionRef.current;
263
- try {
264
- // The sidebar is a multi-workspace index. Keep one unscoped summary
265
- // collection so expanding or switching a workspace never projects the
266
- // previous workspace's list as an empty result while a scoped refresh is
267
- // in flight. Session detail and mutation requests remain owner-scoped.
268
- const res = await fetch(`${origin}/api/operator/v1/chat/sessions`, {
269
- credentials: "include",
270
- });
271
- const body = (await res.json().catch(() => ({})));
272
- if (version !== sessionListRequestVersionRef.current)
273
- return;
274
- if (!res.ok) {
275
- setError(body.error?.message ?? `会话列表加载失败(HTTP ${res.status})`);
276
- return;
277
- }
278
- const sessions = body.sessions ?? [];
279
- const durable = sessions.filter((item) => !pendingDeletedIdsRef.current.has(item.sessionId));
280
- const optimistic = [...pendingCreatedRowsRef.current.values()].filter((item) => !durable.some((session) => session.sessionId === item.sessionId));
281
- // Preserve every workspace's summaries. ChatWorkspaceSidebar applies the
282
- // visible recent-item limit independently inside each workspace group.
283
- const next = stabilizeSessionList(sessionListRef.current, [...optimistic, ...durable]);
284
- sessionListRef.current = next;
285
- setSessionList(next);
286
- if (body.workspaceErrors?.length) {
287
- setError(`部分工作区的会话加载失败(${body.workspaceErrors.length} 个)`);
288
- }
311
+ return Promise.resolve(); // AC-R5-002: fail-closed, zero requests
312
+ if (sessionListRefreshRef.current) {
313
+ // The newest refresh may carry a different visible-workspace projection.
314
+ // Do not briefly commit the older scope before the queued baseline runs.
315
+ sessionListRequestVersionRef.current += 1;
316
+ sessionListRefreshQueuedRef.current = true;
317
+ return sessionListRefreshRef.current;
289
318
  }
290
- catch (error) {
291
- if (version !== sessionListRequestVersionRef.current)
292
- return;
293
- setError(`会话列表加载失败:${error instanceof Error ? error.message : String(error)}`);
319
+ const request = (async () => {
320
+ do {
321
+ sessionListRefreshQueuedRef.current = false;
322
+ const version = ++sessionListRequestVersionRef.current;
323
+ setSessionListStatus((current) => ({
324
+ ...current,
325
+ state: "loading",
326
+ error: null,
327
+ }));
328
+ try {
329
+ // The sidebar is one unscoped multi-workspace index. Session detail
330
+ // and mutations remain owner-scoped.
331
+ const res = await fetch(`${origin}/api/operator/v1/chat/sessions`, {
332
+ credentials: "include",
333
+ });
334
+ let rawBody;
335
+ try {
336
+ rawBody = await res.json();
337
+ }
338
+ catch {
339
+ throw new Error("会话列表响应不是有效 JSON");
340
+ }
341
+ if (version !== sessionListRequestVersionRef.current) {
342
+ sessionListRefreshQueuedRef.current = true;
343
+ continue;
344
+ }
345
+ if (!res.ok) {
346
+ const message = isRecord(rawBody) &&
347
+ isRecord(rawBody.error) &&
348
+ typeof rawBody.error.message === "string"
349
+ ? rawBody.error.message
350
+ : `会话列表加载失败(HTTP ${res.status})`;
351
+ throw new Error(message);
352
+ }
353
+ const baseline = parseSessionListBaseline(rawBody);
354
+ if (baseline.workspaceErrors.length > 0) {
355
+ throw new Error(`部分工作区的会话加载失败(${baseline.workspaceErrors.length} 个)`);
356
+ }
357
+ const belongsToVisibleWorkspace = (item) => params.workspacesLoading ||
358
+ !item.workspaceId ||
359
+ visibleWorkspaceIds.has(item.workspaceId);
360
+ const durable = baseline.sessions.filter((item) => belongsToVisibleWorkspace(item) &&
361
+ !pendingDeletedIdsRef.current.has(item.sessionId));
362
+ const optimistic = [...pendingCreatedRowsRef.current.values()].filter((item) => belongsToVisibleWorkspace(item) &&
363
+ !durable.some((session) => session.sessionId === item.sessionId));
364
+ const next = stabilizeSessionList(sessionListRef.current, [...optimistic, ...durable]);
365
+ sessionListRef.current = next;
366
+ setSessionList(next);
367
+ setSessionListStatus({ phase: "ready", state: "idle", error: null });
368
+ const previousListError = sessionListGlobalErrorRef.current;
369
+ if (previousListError) {
370
+ setError((current) => current === previousListError ? null : current);
371
+ sessionListGlobalErrorRef.current = null;
372
+ }
373
+ }
374
+ catch (error) {
375
+ if (version !== sessionListRequestVersionRef.current) {
376
+ sessionListRefreshQueuedRef.current = true;
377
+ continue;
378
+ }
379
+ const message = `会话列表加载失败:${error instanceof Error ? error.message : String(error)}`;
380
+ setSessionListStatus((current) => ({
381
+ ...current,
382
+ state: "error",
383
+ error: message,
384
+ }));
385
+ sessionListGlobalErrorRef.current = message;
386
+ setError(message);
387
+ }
388
+ } while (sessionListRefreshQueuedRef.current && !blocked);
389
+ })();
390
+ sessionListRefreshRef.current = request;
391
+ void request.finally(() => {
392
+ if (sessionListRefreshRef.current === request) {
393
+ sessionListRefreshRef.current = null;
394
+ }
395
+ });
396
+ return request;
397
+ }, [origin, blocked, setError, visibleWorkspaceIds, params.workspacesLoading]);
398
+ const invalidateSessionListBaseline = useCallback(() => {
399
+ sessionListRequestVersionRef.current += 1;
400
+ if (sessionListRefreshRef.current) {
401
+ sessionListRefreshQueuedRef.current = true;
294
402
  }
295
- }, [origin, blocked, setError]);
403
+ }, []);
296
404
  const deleteSession = useCallback(async (sessionId) => {
297
405
  if (pendingDeletedIdsRef.current.has(sessionId))
298
406
  return false;
@@ -302,7 +410,7 @@ export function useChatSessions(params) {
302
410
  if (!removed || removedIndex < 0)
303
411
  return false;
304
412
  // No pre-mutation snapshot may overwrite this deletion or its rollback.
305
- sessionListRequestVersionRef.current += 1;
413
+ invalidateSessionListBaseline();
306
414
  pendingDeletedIdsRef.current.add(sessionId);
307
415
  deleteRequestVersionsRef.current.set(sessionId, requestId);
308
416
  const activeSession = refs.sessionRef.current?.sessionId === sessionId
@@ -335,7 +443,7 @@ export function useChatSessions(params) {
335
443
  const restore = () => {
336
444
  if (deleteRequestVersionsRef.current.get(sessionId) !== requestId)
337
445
  return;
338
- sessionListRequestVersionRef.current += 1;
446
+ invalidateSessionListBaseline();
339
447
  pendingDeletedIdsRef.current.delete(sessionId);
340
448
  deleteRequestVersionsRef.current.delete(sessionId);
341
449
  const restored = restoreSessionAt(sessionListRef.current.filter((item) => item.sessionId !== sessionId), removed, removedIndex);
@@ -406,6 +514,7 @@ export function useChatSessions(params) {
406
514
  setError,
407
515
  sessionOwnerQuery,
408
516
  workspaceId,
517
+ invalidateSessionListBaseline,
409
518
  ]);
410
519
  const forgetLostSession = useCallback((sessionId, ownerWorkspaceId) => {
411
520
  const active = refs.sessionRef.current;
@@ -418,6 +527,7 @@ export function useChatSessions(params) {
418
527
  forgetActiveChatSession(window.localStorage, scope, sessionId);
419
528
  releaseSession(identity);
420
529
  pendingDeletedIdsRef.current.add(sessionId);
530
+ invalidateSessionListBaseline();
421
531
  const { next } = removeSessionById(sessionListRef.current, sessionId);
422
532
  sessionListRef.current = next;
423
533
  setSessionList(next);
@@ -430,7 +540,15 @@ export function useChatSessions(params) {
430
540
  setSession(null);
431
541
  }
432
542
  void refreshSessionList();
433
- }, [refs, releaseSession, resetActiveProjection, setSession, workspaceId, refreshSessionList]);
543
+ }, [
544
+ refs,
545
+ releaseSession,
546
+ resetActiveProjection,
547
+ setSession,
548
+ workspaceId,
549
+ refreshSessionList,
550
+ invalidateSessionListBaseline,
551
+ ]);
434
552
  const activateSession = useCallback(async (sessionId, ownerWorkspaceId) => {
435
553
  const selectionVersion = ++sessionSelectionVersionRef.current;
436
554
  const listedOwner = sessionListRef.current.find((item) => item.sessionId === sessionId)?.workspaceId;
@@ -497,9 +615,13 @@ export function useChatSessions(params) {
497
615
  applyActivatedSession({
498
616
  messages: hydrated.messages,
499
617
  toolCalls: hydrated.toolCalls,
618
+ turnStates: body.session.turnStates,
619
+ conversationEvents: body.conversationEvents,
500
620
  sessionStats: body.sessionStats,
501
621
  usage: body.usage,
502
622
  turnTelemetry: body.turnTelemetry,
623
+ userTurnOutline: body.userTurnOutline,
624
+ sddSessionIdentity: body.sddDataAlignment ?? null,
503
625
  streaming: visuallyStopped ? false : Boolean(body.session.activeTurnId),
504
626
  turnPhase: body.session.turnPhase ??
505
627
  (body.session.activeTurnId ? "running" : "idle"),
@@ -535,6 +657,32 @@ export function useChatSessions(params) {
535
657
  forgetLostSession,
536
658
  ]);
537
659
  activateSessionRef.current = activateSession;
660
+ const forkSessionAt = useCallback(async (throughMessageId) => {
661
+ const current = refs.sessionRef.current;
662
+ if (!current || refs.activeTurnIdRef.current)
663
+ return false;
664
+ try {
665
+ const res = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(current.sessionId)}/fork${sessionOwnerQuery(current.workspaceId)}`, {
666
+ method: "POST",
667
+ credentials: "include",
668
+ headers: {
669
+ "content-type": "application/json",
670
+ "x-loop-console-confirmation": confirmationToken(),
671
+ },
672
+ body: JSON.stringify({ throughMessageId }),
673
+ });
674
+ const body = (await res.json().catch(() => ({})));
675
+ if (!res.ok || !body.sessionId) {
676
+ throw new Error(body.error?.message ?? "创建分叉会话失败");
677
+ }
678
+ await refreshSessionList();
679
+ return activateSession(body.sessionId, current.workspaceId);
680
+ }
681
+ catch (error) {
682
+ setError(error instanceof Error ? error.message : String(error));
683
+ return false;
684
+ }
685
+ }, [activateSession, origin, refreshSessionList, refs, sessionOwnerQuery, setError]);
538
686
  useEffect(() => {
539
687
  void refreshSessionList();
540
688
  }, [refreshSessionList]);
@@ -631,9 +779,13 @@ export function useChatSessions(params) {
631
779
  applyActivatedSession({
632
780
  messages: hydrated.messages,
633
781
  toolCalls: hydrated.toolCalls,
782
+ turnStates: body.session.turnStates,
783
+ conversationEvents: body.conversationEvents,
634
784
  sessionStats: body.sessionStats,
635
785
  usage: body.usage,
636
786
  turnTelemetry: body.turnTelemetry,
787
+ userTurnOutline: body.userTurnOutline,
788
+ sddSessionIdentity: body.sddDataAlignment ?? null,
637
789
  streaming: visuallyStopped ? false : Boolean(body.session.activeTurnId),
638
790
  turnPhase: body.session.turnPhase ??
639
791
  (body.session.activeTurnId ? "running" : "idle"),
@@ -688,6 +840,7 @@ export function useChatSessions(params) {
688
840
  optimisticState: "creating",
689
841
  };
690
842
  pendingCreatedRowsRef.current.set(optimisticSessionId, optimisticRow);
843
+ invalidateSessionListBaseline();
691
844
  const optimisticList = insertOptimisticSession(sessionListRef.current, optimisticRow);
692
845
  sessionListRef.current = optimisticList;
693
846
  setSessionList(optimisticList);
@@ -703,6 +856,14 @@ export function useChatSessions(params) {
703
856
  creatingRef: creatingSessionRef,
704
857
  });
705
858
  pendingCreatedRowsRef.current.delete(optimisticSessionId);
859
+ const durableRow = sessionListRef.current.find((item) => item.sessionId === created.sessionId);
860
+ if (durableRow) {
861
+ pendingCreatedRowsRef.current.set(created.sessionId, durableRow);
862
+ invalidateSessionListBaseline();
863
+ void refreshSessionList().finally(() => {
864
+ pendingCreatedRowsRef.current.delete(created.sessionId);
865
+ });
866
+ }
706
867
  return created;
707
868
  }
708
869
  catch (error) {
@@ -712,7 +873,13 @@ export function useChatSessions(params) {
712
873
  setSessionList(cleaned);
713
874
  throw error;
714
875
  }
715
- }, [origin, refreshSessionList, workspaceId, setError]);
876
+ }, [
877
+ origin,
878
+ refreshSessionList,
879
+ workspaceId,
880
+ setError,
881
+ invalidateSessionListBaseline,
882
+ ]);
716
883
  const createSession = useCallback(async (options) => {
717
884
  // Suppress concurrent create from any entry (sidebar / Landing / chip).
718
885
  if (creatingSessionRef.current)
@@ -760,6 +927,7 @@ export function useChatSessions(params) {
760
927
  setSelectedSession(created);
761
928
  rememberActiveChatSession(window.localStorage, created.workspaceId ?? options?.workspaceId ?? workspaceId, created.sessionId);
762
929
  setSession(created);
930
+ void refreshSessionTelemetry();
763
931
  return created;
764
932
  }
765
933
  catch (e) {
@@ -783,6 +951,7 @@ export function useChatSessions(params) {
783
951
  activateSession,
784
952
  blocked,
785
953
  workspaceId,
954
+ refreshSessionTelemetry,
786
955
  ]);
787
956
  // Phase-1: fork / in-session branch / mainline switch UI deferred; API routes remain.
788
957
  /** Single compact helper for toolbar + `/compact <instructions>` (AC-1). */
@@ -892,11 +1061,13 @@ export function useChatSessions(params) {
892
1061
  };
893
1062
  return {
894
1063
  sessionList,
1064
+ sessionListStatus,
895
1065
  selectedSession,
896
1066
  sessionSwitching: Boolean(selectedSession &&
897
1067
  refs.sessionRef.current?.sessionId !== selectedSession.sessionId),
898
1068
  creatingSession,
899
1069
  activateSession,
1070
+ forkSessionAt,
900
1071
  refreshSessionList,
901
1072
  deleteSession,
902
1073
  createSession,
@@ -1,9 +1,52 @@
1
1
  import { useCallback, useEffect } from "react";
2
2
  import { formatOperatorUserError } from "../../operator-user-error.js";
3
+ import { inspectChatSseContinuity, readChatStreamReadyCursor, selectChatSseRepairBlocks, selectLaterChatEventId, } from "./chat-stream-continuity.js";
3
4
  import { confirmationToken, requestId, sleep } from "./format.js";
4
5
  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, visualTurnPhaseFromOwnership, } from "./turn-stream-controller.js";
6
+ import { applyAbortDecision, applyPendingReconcileDecision, classifyAbortResponse, createBoundedOwnerSignal, markStopRequested, operatorChatAbortUrl, operatorChatEventsUrl, operatorChatStateUrl, operatorChatTurnsUrl, releaseOwnershipOnTerminalEvidence, releaseSubmitOwnership as releaseOwnershipBag, requestStopOnOwnership, settleCreateTurnOutcome, turnPhaseFromOwnership, visualTurnPhaseFromOwnership, } from "./turn-stream-controller.js";
6
7
  import { appendPendingSubmission, removePendingSubmission } from "./pending-user-message.js";
8
+ import { createTurnStateReconcileQueue, turnStateReconcileTrigger, } from "./turn-state-projection.js";
9
+ const STATE_REQUEST_TIMEOUT_MS = 2_000;
10
+ async function fetchReconciledSessionState(origin, sessionId, options) {
11
+ const bounded = createBoundedOwnerSignal({
12
+ ownerSignal: options?.ownerSignal,
13
+ timeoutMs: STATE_REQUEST_TIMEOUT_MS,
14
+ });
15
+ try {
16
+ const res = await fetch(operatorChatStateUrl(origin, sessionId, options?.turnIds), {
17
+ credentials: "include",
18
+ signal: bounded.signal,
19
+ });
20
+ const body = (await res.json().catch(() => ({})));
21
+ if (!res.ok)
22
+ return null;
23
+ return body.state ?? null;
24
+ }
25
+ catch {
26
+ return null;
27
+ }
28
+ finally {
29
+ bounded.dispose();
30
+ }
31
+ }
32
+ function readChatSseTurnId(block) {
33
+ const data = block
34
+ .split("\n")
35
+ .filter((line) => line.startsWith("data: "))
36
+ .map((line) => line.slice(6))
37
+ .join("");
38
+ if (!data)
39
+ return undefined;
40
+ try {
41
+ const parsed = JSON.parse(data);
42
+ return typeof parsed.turnId === "string" && parsed.turnId
43
+ ? parsed.turnId
44
+ : undefined;
45
+ }
46
+ catch {
47
+ return undefined;
48
+ }
49
+ }
7
50
  /** Project ChatRefs into the executable ownership bag used by the controller. */
8
51
  function ownershipBagFromRefs(refs) {
9
52
  return {
@@ -104,6 +147,18 @@ export function useChatStream(params) {
104
147
  * means replay cannot render duplicate messages while live text remains
105
148
  * smooth (M4.1/M4.3). */
106
149
  const openEventsStream = useCallback(async (sessionId, generation, signal) => {
150
+ const gapReplayBuffer = [];
151
+ const turnStateReconciles = createTurnStateReconcileQueue({
152
+ read: (turnIds) => fetchReconciledSessionState(origin, sessionId, {
153
+ turnIds,
154
+ ownerSignal: signal,
155
+ }),
156
+ apply: (summaries) => thread.reconcileTurnStates([...summaries]),
157
+ wait: (delayMs) => sleep(delayMs, signal),
158
+ isCancelled: () => signal.aborted ||
159
+ refs.sessionRef.current?.sessionId !== sessionId ||
160
+ refs.sessionGenerationRef.current !== generation,
161
+ });
107
162
  while (!signal.aborted) {
108
163
  const state = refs.eventsStreamStateRef.current;
109
164
  if (!state ||
@@ -129,21 +184,48 @@ export function useChatStream(params) {
129
184
  const decoder = new TextDecoder();
130
185
  let buffer = "";
131
186
  let replay = true;
187
+ let replayRepairNeeded = false;
188
+ let liveContinuityCursor = null;
132
189
  const fallbackAssistantId = requestId("a");
133
- for (;;) {
190
+ readLoop: for (;;) {
134
191
  const { done, value } = await reader.read();
135
192
  if (done)
136
193
  break;
137
194
  buffer += decoder.decode(value, { stream: true });
138
195
  const blocks = buffer.split("\n\n");
139
196
  buffer = blocks.pop() ?? "";
140
- for (const block of blocks) {
141
- if (sseEventName(block) === "stream_ready") {
197
+ for (let blockIndex = 0; blockIndex < blocks.length; blockIndex += 1) {
198
+ const block = blocks[blockIndex];
199
+ if (block === undefined)
200
+ continue;
201
+ const eventName = sseEventName(block);
202
+ const reconcileTrigger = turnStateReconcileTrigger(replay, eventName);
203
+ if (reconcileTrigger) {
204
+ const settledTurnId = readChatSseTurnId(block);
205
+ if (settledTurnId) {
206
+ turnStateReconciles.enqueue(settledTurnId, {
207
+ defer: reconcileTrigger === "after-replay",
208
+ });
209
+ }
210
+ }
211
+ if (!replay) {
212
+ const continuity = inspectChatSseContinuity(block, liveContinuityCursor);
213
+ if (continuity.action === "reconnect") {
214
+ // Buffer this block and the rest of the decoded chunk before closing
215
+ // the reader. Retained replay repairs structure; the buffer preserves
216
+ // cumulative text frames that persistence may have coalesced away.
217
+ gapReplayBuffer.push(...blocks.slice(blockIndex));
218
+ replayRepairNeeded = true;
219
+ break readLoop;
220
+ }
221
+ }
222
+ if (eventName === "stream_ready") {
142
223
  const current = refs.eventsStreamStateRef.current;
143
224
  if (current?.sessionId === sessionId &&
144
225
  current.generation === generation)
145
226
  current.ready = true;
146
227
  replay = false;
228
+ liveContinuityCursor = readChatStreamReadyCursor(block);
147
229
  // Fall through: the applier owns the buffered-replay flush and
148
230
  // returns immediately for this event. `continue` here silently
149
231
  // dropped that flush, so a reconnect that landed after the
@@ -155,15 +237,41 @@ export function useChatStream(params) {
155
237
  targetSessionId: sessionId,
156
238
  generation,
157
239
  });
158
- if (eventId)
159
- refs.lastEventIdRef.current = eventId;
240
+ if (eventId) {
241
+ refs.lastEventIdRef.current = selectLaterChatEventId(refs.lastEventIdRef.current, eventId);
242
+ if (!replay)
243
+ liveContinuityCursor = selectLaterChatEventId(liveContinuityCursor, eventId);
244
+ }
245
+ if (eventName === "stream_ready" &&
246
+ gapReplayBuffer.length > 0) {
247
+ const repairedBlocks = selectChatSseRepairBlocks(gapReplayBuffer.splice(0), liveContinuityCursor);
248
+ for (const repairedBlock of repairedBlocks) {
249
+ const repairedEventId = applySseBlock(repairedBlock, refs.assistantIdRef.current ?? fallbackAssistantId, {
250
+ replay: false,
251
+ targetSessionId: sessionId,
252
+ generation,
253
+ });
254
+ if (!repairedEventId)
255
+ continue;
256
+ refs.lastEventIdRef.current = selectLaterChatEventId(refs.lastEventIdRef.current, repairedEventId);
257
+ }
258
+ }
259
+ if (eventName === "stream_ready") {
260
+ void turnStateReconciles.start();
261
+ }
160
262
  }
161
263
  }
264
+ if (replayRepairNeeded) {
265
+ await reader.cancel().catch(() => undefined);
266
+ }
162
267
  const current = refs.eventsStreamStateRef.current;
163
268
  if (current?.sessionId === sessionId &&
164
269
  current.generation === generation)
165
270
  current.ready = false;
166
- await sleep(1_000, signal);
271
+ // A detected hole is expected to be immediately recoverable from the
272
+ // durable ring. Other EOFs retain the normal reconnect backoff.
273
+ if (!replayRepairNeeded)
274
+ await sleep(1_000, signal);
167
275
  }
168
276
  catch {
169
277
  if (signal.aborted)
@@ -171,7 +279,7 @@ export function useChatStream(params) {
171
279
  await sleep(2_000, signal);
172
280
  }
173
281
  }
174
- }, [origin, refs, applySseBlock]);
282
+ }, [origin, refs, applySseBlock, thread.reconcileTurnStates]);
175
283
  const startEventsStream = useCallback((sessionId, generation) => {
176
284
  const existing = refs.eventsStreamStateRef.current;
177
285
  if (existing?.sessionId === sessionId &&
@@ -207,20 +315,7 @@ export function useChatStream(params) {
207
315
  }
208
316
  throw new Error("事件连接未就绪,请检查 Console 连接后重试");
209
317
  }, [refs, startEventsStream]);
210
- const fetchSessionState = useCallback(async (sessionId) => {
211
- try {
212
- const res = await fetch(operatorChatStateUrl(origin, sessionId), {
213
- credentials: "include",
214
- });
215
- const body = (await res.json().catch(() => ({})));
216
- if (!res.ok)
217
- return null;
218
- return body.state ?? null;
219
- }
220
- catch {
221
- return null;
222
- }
223
- }, [origin]);
318
+ const fetchSessionState = useCallback((sessionId) => fetchReconciledSessionState(origin, sessionId), [origin]);
224
319
  const postCreateTurn = useCallback(async (input) => {
225
320
  try {
226
321
  // Resolve against the configured Console origin and fail closed if a
@@ -1000,20 +1095,20 @@ export function useChatStream(params) {
1000
1095
  const sessionId = session.sessionId;
1001
1096
  const sessionIdentity = { ...session };
1002
1097
  const generation = refs.sessionGenerationRef.current;
1098
+ const stateReconcileAbort = new AbortController();
1003
1099
  let cancelled = false;
1004
1100
  let syncInFlight = null;
1005
1101
  const runSync = async () => {
1006
1102
  try {
1007
- const res = await fetch(operatorChatStateUrl(origin, sessionId), {
1008
- credentials: "include",
1103
+ const state = await fetchReconciledSessionState(origin, sessionId, {
1104
+ ownerSignal: stateReconcileAbort.signal,
1009
1105
  });
1010
- const body = (await res.json().catch(() => ({})));
1011
1106
  if (cancelled ||
1012
- !res.ok ||
1107
+ !state ||
1013
1108
  refs.sessionRef.current?.sessionId !== sessionId ||
1014
1109
  refs.sessionGenerationRef.current !== generation)
1015
1110
  return;
1016
- const state = body.state;
1111
+ thread.reconcileTurnStates(state?.turnStates);
1017
1112
  const pending = refs.pendingSubmissionRef.current;
1018
1113
  // Pending ownership reconcile. Controller owns bind/release; same
1019
1114
  // clientRequestId only. Re-enter after acceptedTurnId so terminal
@@ -1158,6 +1253,7 @@ export function useChatStream(params) {
1158
1253
  void sync().finally(scheduleStopRetry);
1159
1254
  return () => {
1160
1255
  cancelled = true;
1256
+ stateReconcileAbort.abort();
1161
1257
  window.clearInterval(interval);
1162
1258
  if (stopRetryTimer !== undefined)
1163
1259
  window.clearTimeout(stopRetryTimer);
@@ -1174,6 +1270,7 @@ export function useChatStream(params) {
1174
1270
  markIdleIfCurrent,
1175
1271
  releaseSubmitOwnership,
1176
1272
  postAbortForTurn,
1273
+ thread.reconcileTurnStates,
1177
1274
  ]);
1178
1275
  // Exactly one owner starts/aborts GET /events. Switching sessions or
1179
1276
  // recovering a cursor aborts the prior stream before the next one is made.