@tea-agent/loop-agent 0.39.0-beta.1 → 0.39.0-beta.11

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 (338) hide show
  1. package/AGENTS.md +4 -1
  2. package/CHANGELOG.md +349 -97
  3. package/README.md +9 -5
  4. package/bin/loop-agent.js +7 -3
  5. package/dist/application/task-lifecycle/advance.js +12 -0
  6. package/dist/application/task-lifecycle/recommendations.js +13 -2
  7. package/dist/build-stamp.json +3 -3
  8. package/dist/cli/command-definitions.js +23 -5
  9. package/dist/cli/program.js +21 -2
  10. package/dist/cli/update/notifier.js +2 -2
  11. package/dist/cli/update/runtime-activity.js +1 -29
  12. package/dist/cli.js +2 -1
  13. package/dist/commands/client-recovery.js +3 -0
  14. package/dist/commands/dag-follow-up.js +138 -0
  15. package/dist/commands/dag-rerun.js +55 -1
  16. package/dist/commands/init-model-catalog.js +464 -0
  17. package/dist/commands/init-upgrade.js +265 -97
  18. package/dist/commands/init.js +455 -28
  19. package/dist/commands/inspect-next.js +8 -0
  20. package/dist/commands/task-advance.js +19 -0
  21. package/dist/executors/dag-pi-executor.js +2370 -64
  22. package/dist/executors/pi-executor.js +11 -2
  23. package/dist/executors/pi-extension-resolver.js +233 -0
  24. package/dist/executors/pi-playwright-cli-tool.js +74 -28
  25. package/dist/executors/pi-read-budget-policy.js +239 -0
  26. package/dist/executors/pi-sdk-executor.js +225 -33
  27. package/dist/executors/pi-writer-tool-policy.js +57 -1
  28. package/dist/executors/shell-executor.js +1258 -224
  29. package/dist/executors/shell-write-guard.js +14 -0
  30. package/dist/executors/workspace-write-snapshot.js +68 -0
  31. package/dist/infrastructure/harness/atomic-write.js +23 -0
  32. package/dist/shared/dag-failure-category.js +150 -0
  33. package/dist/shared/dag-prompt-override.js +27 -0
  34. package/dist/shared/openspec-spec.js +70 -4
  35. package/dist/shared/operator/capabilities.js +39 -0
  36. package/dist/shared/playwright-cli-command-policy.js +15 -0
  37. package/dist/shared/update/console-notifier.js +100 -0
  38. package/dist/{cli → shared}/update/npm-client.js +70 -15
  39. package/dist/{cli → shared}/update/state.js +41 -13
  40. package/dist/task/config-types.js +50 -5
  41. package/dist/task/contract/apply.js +11 -1
  42. package/dist/task/contract/constants.js +2 -0
  43. package/dist/task/contract/hash.js +3 -0
  44. package/dist/task/contract/observe.js +25 -4
  45. package/dist/task/contract/paths.js +2 -1
  46. package/dist/task/contract/project.js +18 -1
  47. package/dist/task/contract/schema.js +4 -1
  48. package/dist/task/contract/transaction.js +12 -0
  49. package/dist/task/frontend-project-capability.js +199 -18
  50. package/dist/task/source-prepare/completeness.js +188 -0
  51. package/dist/task/source-prepare/fragment-inventory.js +477 -0
  52. package/dist/task/source-prepare/index.js +3 -0
  53. package/dist/task/source-prepare/ledger-reconciliation.js +127 -0
  54. package/dist/task/source-prepare/ledger-review.js +214 -0
  55. package/dist/task/source-prepare/ledger.js +545 -0
  56. package/dist/task/source-prepare/parse-intent.js +24 -4
  57. package/dist/task/source-prepare/prepare.js +298 -1
  58. package/dist/task/source-prepare/semantic-intake.js +25 -5
  59. package/dist/task/source-prepare/source-fidelity-pi.js +384 -0
  60. package/dist/task/source-prepare/types.js +22 -0
  61. package/dist/task/source-references.js +22 -1
  62. package/dist/worker/cli.js +18 -2
  63. package/dist/worker/console/chat/chat-event-store.js +75 -5
  64. package/dist/worker/console/chat/context-panel.js +7 -0
  65. package/dist/worker/console/chat/deferred-turn.js +12 -0
  66. package/dist/worker/console/chat/operation-card.js +8 -1
  67. package/dist/worker/console/chat/pi-console-config.js +81 -10
  68. package/dist/worker/console/chat/pi-runtime.js +331 -18
  69. package/dist/worker/console/chat/provider-error.js +98 -0
  70. package/dist/worker/console/chat/routes.js +395 -59
  71. package/dist/worker/console/chat/session-stats.js +179 -0
  72. package/dist/worker/console/chat/session-store.js +93 -63
  73. package/dist/worker/console/chat/todos.js +115 -0
  74. package/dist/worker/console/chat/tool-preview.js +42 -0
  75. package/dist/worker/console/chat/turn-process.js +20 -55
  76. package/dist/worker/console/chat/user-questions.js +266 -0
  77. package/dist/worker/console/chat/workspace-landing.js +79 -0
  78. package/dist/worker/console/console-handoff.js +82 -0
  79. package/dist/worker/console/console-update-and-init.js +115 -0
  80. package/dist/worker/console/console-update-runtime.js +84 -0
  81. package/dist/worker/console/dag-execution-receipt.js +33 -0
  82. package/dist/worker/console/draft-store.js +49 -0
  83. package/dist/worker/console/frontend-human-decision-adapter.js +19 -0
  84. package/dist/worker/console/frontend-split-operation-adapter.js +20 -0
  85. package/dist/worker/console/index.js +3 -0
  86. package/dist/worker/console/inspect-split.js +18 -0
  87. package/dist/worker/console/native-directory-picker.js +13 -0
  88. package/dist/worker/console/operation-run-facts.js +19 -3
  89. package/dist/worker/console/operation-store.js +227 -7
  90. package/dist/worker/console/operator-actions.js +388 -0
  91. package/dist/worker/console/operator-surface-health.js +1 -0
  92. package/dist/worker/console/operator-user-error.js +2 -2
  93. package/dist/worker/console/prd-intake-bridge.js +13 -0
  94. package/dist/worker/console/routes.js +355 -3
  95. package/dist/worker/console/security.js +67 -13
  96. package/dist/worker/console/server.js +609 -164
  97. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4kVYweA.js → abnfDiagram-N423BO3Z-CXj_GnSb.js} +1 -1
  98. package/dist/worker/console/static/assets/{arc-Bj2M8iO5.js → arc-BZp6JAp7.js} +1 -1
  99. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CLlXe4-6.js → architectureDiagram-T3A2C74G-DfpcEuYU.js} +1 -1
  100. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-psEp5xH0.js → blockDiagram-VBNYF7ZC-_Gf0xadb.js} +1 -1
  101. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DeKcOCGJ.js → c4Diagram-5PPSVZJV-Ct2QPCmv.js} +1 -1
  102. package/dist/worker/console/static/assets/channel-3TxJgYaH.js +1 -0
  103. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-K0C744p1.js → chunk-2GRJ4B5K-BfklkzKl.js} +1 -1
  104. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-xbNw4J9-.js → chunk-2Q5K7J3B-Dy25vZJV.js} +1 -1
  105. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLaSDUO.js → chunk-5RXB4S5H-BFlCRZep.js} +1 -1
  106. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CtiPlKel.js → chunk-5VM5RSS4-op3oVxIE.js} +1 -1
  107. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DZtolir7.js → chunk-6Q2QTUOP-C0R7rzF2.js} +1 -1
  108. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CPcdNeew.js → chunk-GF5L2VYU-Bt44TCGy.js} +1 -1
  109. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-NgHc6V37.js → chunk-JWPE2WC7-_uEE_XFx.js} +1 -1
  110. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DQ_T-hg8.js → chunk-KBJHAD2P-C3TOYGZ9.js} +1 -1
  111. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DgLsxcVP.js → chunk-RYQCIY6F-Cz60oBPV.js} +1 -1
  112. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-UrVoM9z1.js → chunk-XXDRQBXY-8Bik0qis.js} +1 -1
  113. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BHIkXpp3.js +1 -0
  114. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BHIkXpp3.js +1 -0
  115. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CLkYvTb3.js → cose-bilkent-JH36ORCC-C2QIOA4a.js} +1 -1
  116. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BwT_xKrE.js → cynefin-VYW2F7L2-CSH_yUUd.js} +1 -1
  117. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bi9tuzif.js → cynefinDiagram-MW4NZA55-BDLw2XFM.js} +1 -1
  118. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BCIqkWBV.js → dagre-VZM6K2ZE-CcD9ZtF4.js} +1 -1
  119. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dip6l9_Z.js → diagram-7IWD3JNH-DqwtkBsI.js} +1 -1
  120. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B-xkh_wK.js → diagram-B4RE2ZJO-BWVEcqsC.js} +1 -1
  121. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DZO0kTt_.js → diagram-LBJQPF4R-M-WAbtQi.js} +1 -1
  122. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D6ZplNbZ.js → diagram-Q27KOJAE-DKW7SixP.js} +1 -1
  123. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPekbhoS.js → diagram-UB23O5K3-PHHPPtrj.js} +1 -1
  124. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BtmaJpK5.js → ebnfDiagram-BXEA7PRR-BQD-B4RX.js} +1 -1
  125. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-6DS0Xc44.js → erDiagram-JOGREHBK-BFvULb51.js} +1 -1
  126. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CihnROSm.js → flowDiagram-UKHOOZJN-Bw-aXTCb.js} +1 -1
  127. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CZC4zOE2.js → ganttDiagram-PKOTCBZU-BGKw04Qx.js} +1 -1
  128. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js → gitGraphDiagram-DS77QQ5N-RqKaPR-I.js} +1 -1
  129. package/dist/worker/console/static/assets/index-B_D8rbWc.js +407 -0
  130. package/dist/worker/console/static/assets/index-BdNx6fj0.css +1 -0
  131. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DlSl4BGx.js → infoDiagram-6WML65LV-YO5dnzrJ.js} +1 -1
  132. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-snnzFl_U.js → ishikawaDiagram-WSZJBQD7-Bi1VZHMq.js} +1 -1
  133. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BDE7qpMx.js → journeyDiagram-NVQOT4AX-Bszls1DD.js} +1 -1
  134. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CD2Ci04G.js → kanban-definition-27J2QSJJ-PhzeaZ09.js} +1 -1
  135. package/dist/worker/console/static/assets/{linear-DdnapKIH.js → linear-BsjbDoXi.js} +1 -1
  136. package/dist/worker/console/static/assets/{mermaid.core-BVjAT9b8.js → mermaid.core-0B7NnWKk.js} +5 -5
  137. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-D0yJk-zA.js → mindmap-definition-FAOFIHXS-BJr4Fj-q.js} +1 -1
  138. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BVo9tFP-.js → pegDiagram-VL7TDLO6-moC4fpGB.js} +1 -1
  139. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DnOV-mZ_.js → pieDiagram-7S7Q4E2Y-BEw37-2c.js} +1 -1
  140. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CzUJo58i.js → quadrantDiagram-CIZ2JOQS-Cq6LyasU.js} +1 -1
  141. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Bvikrolh.js → railroadDiagram-AXF67PYL-DUCMcK0D.js} +1 -1
  142. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DtaSVCap.js → requirementDiagram-LRYGKXZP-C3upTZm7.js} +1 -1
  143. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-C2c9A0wW.js → sankeyDiagram-W5VNT64P-BI_gMsCW.js} +1 -1
  144. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DmXJcU7r.js → sequenceDiagram-SI44F4Z6-YFOIRzfN.js} +1 -1
  145. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4GUFW92.js → sizeCapture-X5ZJPWSS-dOnB7UDD.js} +1 -1
  146. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Dsad2MXf.js → stateDiagram-OKZ733FA-BXUniaIh.js} +1 -1
  147. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Cdi6UhLa.js +1 -0
  148. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DGq48Fbi.js → swimlanes-SLNWSIFB-2tA4wTNu.js} +2 -2
  149. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D-RJBbb0.js +8 -0
  150. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CSSi6OFf.js → timeline-definition-Z64GVDOM-DO0HkJXC.js} +1 -1
  151. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BqyzPrWv.js → vennDiagram-T6HMQDX7-DvIOzixv.js} +1 -1
  152. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DlznVb6S.js → wardleyDiagram-T6FBY63Y-DXDZ0cTj.js} +1 -1
  153. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CfBcgJ_K.js → xychartDiagram-ELKLHX3M-B32Ark0D.js} +1 -1
  154. package/dist/worker/console/static/index.html +2 -2
  155. package/dist/worker/console/static-src/active-run-badge.js +17 -0
  156. package/dist/worker/console/static-src/app/console-types.js +68 -4
  157. package/dist/worker/console/static-src/app/useConsoleShell.js +30 -12
  158. package/dist/worker/console/static-src/app/useOperatorActions.js +41 -6
  159. package/dist/worker/console/static-src/app/useRecoveryConsole.js +45 -10
  160. package/dist/worker/console/static-src/app/useRunProgress.js +47 -0
  161. package/dist/worker/console/static-src/app/useTaskWizard.js +74 -2
  162. package/dist/worker/console/static-src/night/useNightBoard.js +8 -5
  163. package/dist/worker/console/static-src/operator-chat/cards/failure-category-advice.js +25 -0
  164. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +141 -8
  165. package/dist/worker/console/static-src/operator-chat/compaction-message.js +79 -0
  166. package/dist/worker/console/static-src/operator-chat/details-dag-actions.js +122 -0
  167. package/dist/worker/console/static-src/operator-chat/input-history.js +76 -0
  168. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
  169. package/dist/worker/console/static-src/operator-chat/refs.js +3 -0
  170. package/dist/worker/console/static-src/operator-chat/runtime-selection-labels.js +27 -0
  171. package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +1 -2
  172. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +25 -0
  173. package/dist/worker/console/static-src/operator-chat/turn-submission.js +16 -3
  174. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +181 -87
  175. package/dist/worker/console/static-src/operator-chat/useChatStream.js +183 -48
  176. package/dist/worker/console/static-src/operator-chat/useChatThread.js +177 -8
  177. package/dist/worker/console/static-src/operator-chat/useComposer.js +32 -0
  178. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +63 -9
  179. package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
  180. package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -3
  181. package/dist/worker/console/static-src/pages/tasks/run-id-resolution.js +79 -0
  182. package/dist/worker/console/static-src/pages/tasks/run-ownership-verify.js +23 -0
  183. package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +110 -0
  184. package/dist/worker/console/static-src/shell/useWorkspaces.js +299 -0
  185. package/dist/worker/console/static-src/shell/workspace-route.js +339 -0
  186. package/dist/worker/console/workspace-context.js +344 -0
  187. package/dist/worker/console/workspace-registry.js +214 -0
  188. package/dist/worker/loop-agent/loop-agent-client.js +7 -3
  189. package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
  190. package/dist/worker/materialize/harness-task-lineage.js +5 -2
  191. package/dist/worker/observability/init-runtime-activity.js +26 -0
  192. package/dist/worker/observability/read-model.js +22 -0
  193. package/dist/worker/observe/node-input.js +160 -3
  194. package/dist/worker/observe/node-process.js +377 -0
  195. package/dist/worker/observe/routes.js +224 -3
  196. package/dist/worker/observe/server.js +4 -0
  197. package/dist/worker/observe/shell-handler-keys.js +34 -0
  198. package/dist/worker/observe/static/api.js +90 -4
  199. package/dist/worker/observe/static/app.js +18 -70
  200. package/dist/worker/observe/static/constants.js +8 -1
  201. package/dist/worker/observe/static/dag-history-labels.js +4 -0
  202. package/dist/worker/observe/static/dag-node-purpose.d.ts +6 -0
  203. package/dist/worker/observe/static/dag-node-purpose.js +208 -0
  204. package/dist/worker/observe/static/format.js +60 -0
  205. package/dist/worker/observe/static/index.html +6 -1
  206. package/dist/worker/observe/static/inspect-workspace.js +307 -0
  207. package/dist/worker/observe/static/markdown-render.js +20 -1
  208. package/dist/worker/observe/static/operator-chrome.css +145 -20
  209. package/dist/worker/observe/static/operator-chrome.d.ts +20 -3
  210. package/dist/worker/observe/static/operator-chrome.js +330 -96
  211. package/dist/worker/observe/static/prompt-restart-candidates.d.ts +18 -0
  212. package/dist/worker/observe/static/prompt-restart-candidates.js +36 -0
  213. package/dist/worker/observe/static/router.d.ts +46 -0
  214. package/dist/worker/observe/static/router.js +25 -1
  215. package/dist/worker/observe/static/state.d.ts +50 -0
  216. package/dist/worker/observe/static/state.js +184 -1
  217. package/dist/worker/observe/static/styles.css +395 -0
  218. package/dist/worker/observe/static/views/dag-graph.js +4 -2
  219. package/dist/worker/observe/static/views/dag-inspector.js +1067 -9
  220. package/dist/worker/observe/static/views/dag.d.ts +13 -0
  221. package/dist/worker/observe/static/views/dag.js +14 -5
  222. package/dist/worker/observe/static/views/dashboard.js +6 -4
  223. package/dist/worker/observe/static/views/failures.js +5 -2
  224. package/dist/worker/observe/static/views/night.js +2 -2
  225. package/dist/worker/observe/static/views/pool.js +1 -0
  226. package/dist/worker/observe/static/views/run.js +3 -1
  227. package/dist/worker/observe/static/views/session-timeline.js +78 -9
  228. package/dist/worker/observe/static/views/task.js +56 -1
  229. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +49 -3
  230. package/dist/workflows/dag/backend-test-markdown-workflow.js +176 -1
  231. package/dist/workflows/dag/backend-test-pytest-collection.js +98 -14
  232. package/dist/workflows/dag/backend-test-writer-completeness.js +17 -17
  233. package/dist/workflows/dag/contract-output-registry.js +15 -0
  234. package/dist/workflows/dag/contract-validator-registrations.js +1 -2
  235. package/dist/workflows/dag/dynamic-runtime/loop-until.js +1 -1
  236. package/dist/workflows/dag/dynamic-runtime/map.js +2 -1
  237. package/dist/workflows/dag/failure-category.js +7 -116
  238. package/dist/workflows/dag/frontend-closeout.js +221 -0
  239. package/dist/workflows/dag/frontend-design-policy.js +400 -0
  240. package/dist/workflows/dag/frontend-human-decision.js +182 -0
  241. package/dist/workflows/dag/frontend-implementation-contract.js +538 -166
  242. package/dist/workflows/dag/frontend-plan-render.js +24 -0
  243. package/dist/workflows/dag/frontend-prewrite-gate.js +388 -392
  244. package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
  245. package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
  246. package/dist/workflows/dag/frontend-recovery-controller.js +202 -0
  247. package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
  248. package/dist/workflows/dag/frontend-recovery-plan.js +17 -10
  249. package/dist/workflows/dag/frontend-recovery-run.js +33 -20
  250. package/dist/workflows/dag/frontend-repair.js +7 -424
  251. package/dist/workflows/dag/frontend-review-context.js +288 -14
  252. package/dist/workflows/dag/frontend-review-findings.js +270 -0
  253. package/dist/workflows/dag/frontend-risk.js +15 -2
  254. package/dist/workflows/dag/frontend-shadow-dual-write.js +814 -0
  255. package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
  256. package/dist/workflows/dag/frontend-shape-facts.js +409 -0
  257. package/dist/workflows/dag/frontend-shape.js +427 -0
  258. package/dist/workflows/dag/frontend-source-fidelity-ledger.js +108 -0
  259. package/dist/workflows/dag/frontend-split-application-service.js +203 -0
  260. package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
  261. package/dist/workflows/dag/frontend-test-case-checklist.js +30 -4
  262. package/dist/workflows/dag/frontend-test-case-manifest.js +11 -4
  263. package/dist/workflows/dag/frontend-test-case-quality.js +11 -16
  264. package/dist/workflows/dag/frontend-test-environment-probe.js +230 -0
  265. package/dist/workflows/dag/frontend-test-html-report.js +3 -1
  266. package/dist/workflows/dag/frontend-test-l5-report.js +3 -1
  267. package/dist/workflows/dag/frontend-test-layout.js +159 -0
  268. package/dist/workflows/dag/frontend-test-markdown.js +61 -0
  269. package/dist/workflows/dag/frontend-test-result-contract.js +188 -42
  270. package/dist/workflows/dag/frontend-test-standard-scenarios.js +70 -0
  271. package/dist/workflows/dag/frontend-typed-event-store.js +452 -0
  272. package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
  273. package/dist/workflows/dag/frontend-verification-trace.js +52 -17
  274. package/dist/workflows/dag/frontend-worktree-diff.js +250 -17
  275. package/dist/workflows/dag/frontend-writer-admission.js +272 -0
  276. package/dist/workflows/dag/frontend-writer-status.js +244 -0
  277. package/dist/workflows/dag/init-hybrid.js +1072 -660
  278. package/dist/workflows/dag/interrupt-request.js +10 -3
  279. package/dist/workflows/dag/lifecycle.js +3 -3
  280. package/dist/workflows/dag/node-execution.js +552 -58
  281. package/dist/workflows/dag/prompt.js +44 -19
  282. package/dist/workflows/dag/recovery-lease.js +80 -0
  283. package/dist/workflows/dag/report.js +37 -1
  284. package/dist/workflows/dag/rerun-feedback.js +124 -1
  285. package/dist/workflows/dag/rerun-plan.js +144 -5
  286. package/dist/workflows/dag/rerun-run.js +81 -6
  287. package/dist/workflows/dag/rerun-task.js +29 -0
  288. package/dist/workflows/dag/retry-policy.js +318 -8
  289. package/dist/workflows/dag/runner.js +429 -122
  290. package/dist/workflows/dag/scheduler.js +114 -16
  291. package/dist/workflows/dag/structured-output-repair.js +712 -0
  292. package/dist/workflows/dag/types.js +338 -31
  293. package/dist/workflows/dag/upstream-artifacts.js +0 -4
  294. package/dist/workflows/dag/validate.js +92 -32
  295. package/docs/README.md +2 -0
  296. package/docs/architecture/evolution.md +45 -0
  297. package/docs/init-surface.manifest.json +9 -0
  298. package/docs/operations/README.md +1 -1
  299. package/docs/operations/local-development-environment.md +21 -0
  300. package/docs/templates/README.md +2 -1
  301. package/docs/templates/agent-dag-report.schema.json +8 -2
  302. package/docs/templates/agent-dag.schema.json +63 -1
  303. package/docs/templates/backend-test-dag.json +265 -237
  304. package/docs/templates/frontend-implementation-contract.schema.json +684 -24
  305. package/docs/templates/frontend-test-case-checklist.md +2 -2
  306. package/docs/templates/frontend-test-dag.json +8 -10
  307. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
  308. package/docs/templates/init-managed-agents.md +13 -3
  309. package/docs/templates/spec-registry.schema.json +45 -0
  310. package/harness.json +1 -1
  311. package/package.json +7 -1
  312. package/scripts/next-info.mjs +356 -0
  313. package/scripts/next-publish-gate.mjs +238 -0
  314. package/scripts/release-source-binding.mjs +251 -0
  315. package/skills/codebase-scout/SKILL.md +1 -1
  316. package/skills/fe-test-ui-scout/SKILL.md +65 -0
  317. package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
  318. package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
  319. package/skills/frontend-bounded-implement/SKILL.md +8 -14
  320. package/skills/frontend-design-review/SKILL.md +20 -41
  321. package/skills/frontend-implementation/SKILL.md +3 -4
  322. package/skills/frontend-implementation/references/design-spec.md +15 -10
  323. package/skills/frontend-implementation/references/node-contracts.md +23 -13
  324. package/skills/frontend-review/SKILL.md +22 -14
  325. package/skills/frontend-review/references/review-findings.md +6 -7
  326. package/skills/loop-agent/references/command-reference.md +12 -4
  327. package/skills/loop-agent/references/hybrid-dag.md +8 -1
  328. package/skills/playwright-cli/SKILL.md +23 -1
  329. package/skills/playwright-cli-case-generator/SKILL.md +29 -8
  330. package/dist/worker/console/static/assets/channel-CPF4N7pf.js +0 -1
  331. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C2DwA_Y1.js +0 -1
  332. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C2DwA_Y1.js +0 -1
  333. package/dist/worker/console/static/assets/index-DTZOKgAn.js +0 -404
  334. package/dist/worker/console/static/assets/index-Ya5FE7cD.css +0 -1
  335. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8FB_cNk.js +0 -1
  336. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CrZYaWfx.js +0 -8
  337. package/dist/worker/console/static-src/operator-chat/activity-rail-presentation.js +0 -73
  338. package/dist/worker/console/static-src/operator-chat/useActivityRailTransition.js +0 -59
@@ -12,12 +12,13 @@
12
12
  * Mutation routes go through the Console mutation gate (boot token + Origin).
13
13
  * Read routes (GET capabilities / sessions) are open to loopback.
14
14
  */
15
+ import { randomUUID } from "node:crypto";
15
16
  import { parseAssistantContent } from "./assistant-content.js";
16
17
  import { OPERATOR_CHAT_ALLOWED_TOOLS, OPERATOR_CHAT_DENIED_TOOLS, OPERATOR_CHAT_DENIED_OPERATOR_ACTIONS, } from "./tools.js";
17
18
  import { buildModelCallableToolSchemas } from "./tool-adapter.js";
18
19
  import { cleanChatTitle, acceptedOperationIdFromResult } from "./session-store.js";
19
20
  import { hashChatTurnPayload, projectChatToolEventPayload, projectCompactSnapshot, } from "./chat-event-store.js";
20
- import { projectDeferredTurnSummary, } from "./deferred-turn.js";
21
+ import { projectDeferredTurnSummary, projectDeferredTurnSummaries, } from "./deferred-turn.js";
21
22
  import { THINKING_LEVELS, RuntimeSnapshotUnavailableError, } from "./pi-runtime.js";
22
23
  import { ResourcePreferencesError, setSkillAutoInvocationPreference, } from "./resource-preferences-store.js";
23
24
  import { ComposerDraftStore } from "./composer-draft-store.js";
@@ -33,9 +34,11 @@ import { dispatchOperatorAction } from "../operator-actions.js";
33
34
  import { makeHumanGateCard, } from "./human-gate-card.js";
34
35
  import { ContractApplyReceiptStore } from "./contract-apply-receipt-store.js";
35
36
  import { projectTaskContext } from "./context-panel.js";
37
+ import { deriveChatSessionTelemetry } from "./session-stats.js";
36
38
  import { contractApplyPayloadHash, issueHumanGateToken, verifyHumanGateToken, } from "../human-gate-token.js";
37
39
  import { sendJson } from "../routes.js";
38
40
  import { openSseResponse, writeSseEvent } from "../operation-sse.js";
41
+ import { buildTodoCompactionInstructions, projectCurrentTodos } from "./todos.js";
39
42
  function turnSummary(turn) {
40
43
  return {
41
44
  turnId: turn.turnId,
@@ -52,7 +55,6 @@ function turnSummary(turn) {
52
55
  function isTerminalTurnState(state) {
53
56
  return state === "settled" || state === "aborted" || state === "failed";
54
57
  }
55
- const DEFAULT_ABORT_WAIT_MS = 2_500;
56
58
  /** Parse Chat's `<epoch>:<seq>` SSE cursor. Legacy `<sessionId>:<seq>` has
57
59
  * the same wire shape; its prefix intentionally fails the current process
58
60
  * epoch comparison and triggers safe snapshot recovery (M5.1). */
@@ -87,11 +89,38 @@ function turnEventToStorePartial(event) {
87
89
  case "message_update":
88
90
  case "message_end": {
89
91
  const content = parseAssistantContent(event.content);
92
+ const timingSource = event.type === "message_end" &&
93
+ event.timing &&
94
+ typeof event.timing === "object"
95
+ ? event.timing
96
+ : undefined;
97
+ const timing = timingSource &&
98
+ typeof timingSource.llmMs === "number" &&
99
+ Number.isFinite(timingSource.llmMs)
100
+ ? {
101
+ llmMs: Math.max(0, timingSource.llmMs),
102
+ ...(typeof timingSource.ttftMs === "number" &&
103
+ Number.isFinite(timingSource.ttftMs)
104
+ ? { ttftMs: Math.max(0, timingSource.ttftMs) }
105
+ : {}),
106
+ ...(typeof timingSource.decodeMs === "number" &&
107
+ Number.isFinite(timingSource.decodeMs)
108
+ ? { decodeMs: Math.max(0, timingSource.decodeMs) }
109
+ : {}),
110
+ ...(typeof timingSource.outputTokens === "number" &&
111
+ Number.isFinite(timingSource.outputTokens)
112
+ ? {
113
+ outputTokens: Math.max(0, timingSource.outputTokens),
114
+ }
115
+ : {}),
116
+ }
117
+ : undefined;
90
118
  return {
91
119
  kind: event.type,
92
120
  data: {
93
121
  text: String(event.text ?? ""),
94
122
  ...(content ? { content } : {}),
123
+ ...(timing ? { timing } : {}),
95
124
  },
96
125
  };
97
126
  }
@@ -123,6 +152,9 @@ function turnEventToStorePartial(event) {
123
152
  }),
124
153
  };
125
154
  }
155
+ case "todo_updated":
156
+ case "todo_cleared":
157
+ return { kind: event.type, data: { todos: Array.isArray(event.todos) ? event.todos : [], ...(event.toolCallId ? { toolCallId: String(event.toolCallId) } : {}) } };
126
158
  case "agent_end":
127
159
  return {
128
160
  kind: "agent_end",
@@ -130,8 +162,53 @@ function turnEventToStorePartial(event) {
130
162
  };
131
163
  case "agent_settled":
132
164
  return { kind: "agent_settled", data: {} };
133
- case "error":
134
- return { kind: "error", data: { message: String(event.message ?? "") } };
165
+ case "compaction_start":
166
+ case "compaction_end": {
167
+ const reason = event.reason === "threshold" || event.reason === "overflow"
168
+ ? event.reason
169
+ : "manual";
170
+ const status = event.type === "compaction_start"
171
+ ? "running"
172
+ : event.status === "aborted" || event.status === "failed"
173
+ ? event.status
174
+ : "completed";
175
+ const data = {
176
+ schemaVersion: 1,
177
+ compactionId: String(event.compactionId ?? ""),
178
+ reason,
179
+ status,
180
+ ...(typeof event.summary === "string"
181
+ ? { summary: event.summary }
182
+ : {}),
183
+ ...(typeof event.firstKeptEntryId === "string"
184
+ ? { firstKeptEntryId: event.firstKeptEntryId }
185
+ : {}),
186
+ ...(typeof event.tokensBefore === "number"
187
+ ? { tokensBefore: event.tokensBefore }
188
+ : {}),
189
+ ...(typeof event.estimatedTokensAfter === "number"
190
+ ? { estimatedTokensAfter: event.estimatedTokensAfter }
191
+ : {}),
192
+ willRetry: Boolean(event.willRetry),
193
+ ...(typeof event.errorMessage === "string"
194
+ ? { errorMessage: event.errorMessage }
195
+ : {}),
196
+ };
197
+ return { kind: event.type, data };
198
+ }
199
+ case "error": {
200
+ const content = parseAssistantContent(event.content);
201
+ return {
202
+ kind: "error",
203
+ data: {
204
+ message: String(event.message ?? ""),
205
+ ...(content ? { content } : {}),
206
+ ...(event.providerError && typeof event.providerError === "object"
207
+ ? { providerError: event.providerError }
208
+ : {}),
209
+ },
210
+ };
211
+ }
135
212
  default:
136
213
  return undefined;
137
214
  }
@@ -197,6 +274,13 @@ function ensureAgentEndEvent(deps, sessionId, turnId) {
197
274
  }
198
275
  }
199
276
  async function readJsonBody(req, maxBytes = 2 * 1024 * 1024) {
277
+ // AC-R5-003: the multi-workspace dispatcher (server.ts readBodyOnce)
278
+ // pre-reads and JSON-parses the body for workspaceId resolution, stashing
279
+ // it on req.__preReadBody. Consume it first so the body is parsed exactly
280
+ // once per request; re-reading the consumed stream would yield {}.
281
+ const preRead = req.__preReadBody;
282
+ if (preRead !== undefined)
283
+ return preRead;
200
284
  const chunks = [];
201
285
  let total = 0;
202
286
  for await (const chunk of req) {
@@ -270,6 +354,7 @@ export function parseChatImages(value) {
270
354
  }
271
355
  function gateMutation(req, deps) {
272
356
  const failure = evaluateMutationGate(req, {
357
+ cookieName: deps.bootToken.cookieName,
273
358
  bootToken: deps.bootToken.value,
274
359
  confirmationToken: deps.bootToken.confirmationToken,
275
360
  consoleOrigin: deps.getConsoleOrigin(),
@@ -343,7 +428,10 @@ export async function handleChatCapabilities(_req, res, deps) {
343
428
  hasBash: true,
344
429
  });
345
430
  }
346
- export async function handleCreateChatSession(req, res, deps) {
431
+ export async function handleCreateChatSession(req, res, deps,
432
+ /** Explicit owning workspaceId from the multi-workspace dispatcher
433
+ * (AC-R7-002). The body still parses exactly once via req.__preReadBody. */
434
+ workspaceId) {
347
435
  const gate = gateMutation(req, deps);
348
436
  if (!gate.ok) {
349
437
  sendJson(res, gate.status, {
@@ -421,6 +509,10 @@ export async function handleCreateChatSession(req, res, deps) {
421
509
  sessionFile: handle.sessionFile,
422
510
  modelProvider: handle.model?.provider,
423
511
  modelId: handle.model?.modelId,
512
+ // AC-R7-002: explicit parameter from the dispatcher — the runtime
513
+ // store.create wrapper is gone; body workspaceId (if any) only acts as
514
+ // a fallback for direct (non-dispatcher) handler tests.
515
+ workspaceId: workspaceId ?? (typeof body.workspaceId === "string" && body.workspaceId ? body.workspaceId : undefined),
424
516
  });
425
517
  if (snapshot)
426
518
  deps.events.append(handle.sessionId, `${handle.sessionId}:snapshot`, {
@@ -645,8 +737,10 @@ export async function handleGetChatSession(_req, res, deps, sessionId) {
645
737
  await deps.operationLinker.recoverSession(sessionId);
646
738
  const composerDraft = await new ComposerDraftStore(deps.appData).get(sessionId);
647
739
  const snapshot = deps.events.snapshot(sessionId);
740
+ const telemetry = deriveChatSessionTelemetry(snapshot);
648
741
  const lastEvent = snapshot.at(-1);
649
742
  const activeTurn = deps.events.getActiveTurn(sessionId);
743
+ const pendingQuestion = await deps.questionRegistry?.get(sessionId);
650
744
  sendJson(res, 200, {
651
745
  ok: true,
652
746
  eventEpoch: deps.events.epoch(sessionId),
@@ -659,8 +753,22 @@ export async function handleGetChatSession(_req, res, deps, sessionId) {
659
753
  ? "stopping"
660
754
  : "running"
661
755
  : "idle",
756
+ pendingQuestion: pendingQuestion ?? null,
757
+ todos: projectCurrentTodos(snapshot) ?? [],
662
758
  },
663
759
  hasBash: true,
760
+ sessionStats: telemetry.stats,
761
+ usage: telemetry.usage,
762
+ compactions: snapshot
763
+ .filter((event) => event.kind === "compact" ||
764
+ event.kind === "compaction_start" ||
765
+ event.kind === "compaction_end")
766
+ .map((event) => ({
767
+ eventId: event.eventId,
768
+ kind: event.kind,
769
+ at: event.at,
770
+ data: event.data,
771
+ })),
664
772
  lastEventId: lastEvent?.eventId ?? null,
665
773
  lastEventSeq: lastEvent?.seq ?? 0,
666
774
  });
@@ -727,13 +835,15 @@ export async function handlePutDeferredTurn(req, res, deps, sessionId) {
727
835
  return;
728
836
  }
729
837
  const active = deps.events.getActiveTurn(sessionId);
730
- if (!active || !active.abortRequestedAt || active.turnId !== blockedByTurnId) {
838
+ // Queueing binds to the active Turn (running OR stopping); the durable
839
+ // follow-up auto-dispatches when that Turn reaches a terminal state.
840
+ if (!active || active.turnId !== blockedByTurnId) {
731
841
  sendJson(res, 409, {
732
842
  ok: false,
733
843
  accepted: false,
734
844
  error: {
735
- code: "TURN_NOT_STOPPING",
736
- message: "只有仍在停止中的当前 Turn 可以保存停止后发送消息",
845
+ code: "TURN_NOT_ACTIVE",
846
+ message: "没有匹配的运行中 Turn,无法保存待发送消息",
737
847
  },
738
848
  activeTurn: active ? turnSummary(active) : null,
739
849
  });
@@ -766,11 +876,12 @@ export async function handlePutDeferredTurn(req, res, deps, sessionId) {
766
876
  : "待发送消息无效或会话不存在",
767
877
  },
768
878
  deferredTurn: result.deferredTurn ?? null,
879
+ deferredTurns: projectDeferredTurnSummaries(await deps.store.listDeferredTurns(sessionId)),
769
880
  summary: projectDeferredTurnSummary(result.deferredTurn),
770
881
  });
771
882
  return;
772
883
  }
773
- emitDeferredTurnUpdate(deps, sessionId, blockedByTurnId, result.deferredTurn, result.deferredTurn.revision);
884
+ await emitDeferredTurnUpdate(deps, sessionId, blockedByTurnId, result.deferredTurn, result.deferredTurn.revision);
774
885
  sendJson(res, 200, {
775
886
  ok: true,
776
887
  accepted: true,
@@ -778,6 +889,7 @@ export async function handlePutDeferredTurn(req, res, deps, sessionId) {
778
889
  state: result.deferredTurn.state,
779
890
  revision: result.deferredTurn.revision,
780
891
  deferredTurn: result.deferredTurn,
892
+ deferredTurns: projectDeferredTurnSummaries(await deps.store.listDeferredTurns(sessionId)),
781
893
  summary: projectDeferredTurnSummary(result.deferredTurn),
782
894
  });
783
895
  void dispatchDeferredTurn(deps, sessionId);
@@ -823,18 +935,90 @@ export async function handleDeleteDeferredTurn(req, res, deps, sessionId) {
823
935
  : "待发送消息已变化,请刷新后重试",
824
936
  },
825
937
  deferredTurn: result.deferredTurn ?? null,
938
+ deferredTurns: projectDeferredTurnSummaries(await deps.store.listDeferredTurns(sessionId)),
826
939
  });
827
940
  return;
828
941
  }
829
942
  const clearedRevision = result.deferredTurn.revision + 1;
830
- emitDeferredTurnUpdate(deps, sessionId, result.deferredTurn.blockedByTurnId, undefined, clearedRevision);
943
+ await emitDeferredTurnUpdate(deps, sessionId, result.deferredTurn.blockedByTurnId, undefined, clearedRevision);
831
944
  sendJson(res, 200, {
832
945
  ok: true,
833
946
  cancelled: true,
834
947
  deferredTurn: result.deferredTurn,
948
+ deferredTurns: projectDeferredTurnSummaries(await deps.store.listDeferredTurns(sessionId)),
835
949
  revision: clearedRevision,
836
950
  });
837
951
  }
952
+ /** Move one addressed FIFO item into Pi's live steering queue. */
953
+ export async function handleSteerDeferredTurn(req, res, deps, sessionId, clientRequestId) {
954
+ const gate = gateMutation(req, deps);
955
+ if (!gate.ok) {
956
+ sendJson(res, gate.status, { ok: false, error: { code: gate.code, message: gate.message } });
957
+ return;
958
+ }
959
+ let body = {};
960
+ try {
961
+ body = await readJsonBody(req);
962
+ }
963
+ catch (error) {
964
+ sendJson(res, 400, { ok: false, error: { code: "INVALID_INPUT", message: error instanceof Error ? error.message : String(error) } });
965
+ return;
966
+ }
967
+ const expectedRevision = typeof body.expectedRevision === "number" && Number.isSafeInteger(body.expectedRevision)
968
+ ? body.expectedRevision
969
+ : -1;
970
+ const active = deps.events.getActiveTurn(sessionId);
971
+ if (!active) {
972
+ sendJson(res, 409, { ok: false, error: { code: "TURN_NOT_ACTIVE", message: "当前没有可插话的运行中回合" } });
973
+ return;
974
+ }
975
+ const claim = await deps.store.claimDeferredTurn(sessionId, expectedRevision, clientRequestId);
976
+ if (!claim.ok) {
977
+ sendJson(res, claim.code === "NOT_FOUND" ? 404 : 409, {
978
+ ok: false,
979
+ error: { code: claim.code, message: "排队消息已变化,请刷新后重试" },
980
+ });
981
+ return;
982
+ }
983
+ const item = claim.deferredTurn;
984
+ try {
985
+ await deps.runtime.steer(sessionId, item.text, item.images);
986
+ const userMessage = await deps.store.appendMessage(sessionId, {
987
+ role: "user",
988
+ text: item.text,
989
+ });
990
+ deps.events.append(sessionId, active.turnId, {
991
+ kind: "user_message",
992
+ data: {
993
+ clientRequestId: item.clientRequestId,
994
+ message: { id: userMessage.id, text: userMessage.text, at: userMessage.at },
995
+ },
996
+ });
997
+ await deps.store.clearDeferredTurnForRequest(sessionId, item.clientRequestId);
998
+ await emitDeferredTurnUpdate(deps, sessionId, active.turnId, undefined, item.revision + 2);
999
+ sendJson(res, 200, {
1000
+ ok: true,
1001
+ accepted: true,
1002
+ deferredTurns: projectDeferredTurnSummaries(await deps.store.listDeferredTurns(sessionId)),
1003
+ });
1004
+ }
1005
+ catch (error) {
1006
+ const restored = await deps.store.transitionDeferredTurn(sessionId, {
1007
+ expectedRevision: item.revision,
1008
+ clientRequestId: item.clientRequestId,
1009
+ state: "queued",
1010
+ });
1011
+ if (restored)
1012
+ await emitDeferredTurnUpdate(deps, sessionId, active.turnId, restored, restored.revision);
1013
+ sendJson(res, 409, {
1014
+ ok: false,
1015
+ error: {
1016
+ code: "STEER_FAILED",
1017
+ message: error instanceof Error ? error.message : "插话发送失败",
1018
+ },
1019
+ });
1020
+ }
1021
+ }
838
1022
  /** Lightweight busy-truth endpoint for the client reconcile loop (pi-web
839
1023
  * get_state equivalent): exposes the active turn + the latest turn's durable
840
1024
  * outcome WITHOUT the full message payload, so the UI can poll it on an
@@ -862,6 +1046,7 @@ export async function handleGetChatSessionState(_req, res, deps, sessionId) {
862
1046
  isBashRunning: false,
863
1047
  };
864
1048
  const runtimeBusy = Object.values(runtimeFacts).some((value) => value === true);
1049
+ const pendingQuestion = await deps.questionRegistry?.get(sessionId);
865
1050
  const turnPhase = active
866
1051
  ? active.abortRequestedAt
867
1052
  ? "stopping"
@@ -881,10 +1066,60 @@ export async function handleGetChatSessionState(_req, res, deps, sessionId) {
881
1066
  latestTurn: latest ? turnSummary(latest) : null,
882
1067
  runtime: runtimeFacts,
883
1068
  turnPhase,
884
- deferredTurn: projectDeferredTurnSummary(record.deferredTurn),
1069
+ deferredTurn: projectDeferredTurnSummary(record.deferredTurns?.[0]),
1070
+ deferredTurns: projectDeferredTurnSummaries(record.deferredTurns ?? []),
1071
+ pendingQuestion: pendingQuestion ?? null,
1072
+ todos: projectCurrentTodos(deps.events.snapshot(sessionId)) ?? [],
885
1073
  },
886
1074
  });
887
1075
  }
1076
+ export async function handleAnswerUserQuestion(req, res, deps, sessionId, requestId) {
1077
+ const gate = gateMutation(req, deps);
1078
+ if (!gate.ok) {
1079
+ sendJson(res, gate.status, { ok: false, error: { code: gate.code, message: gate.message } });
1080
+ return;
1081
+ }
1082
+ if (!(await requireChatSession(res, deps, sessionId)))
1083
+ return;
1084
+ if (!deps.questionRegistry) {
1085
+ sendJson(res, 503, { ok: false, error: { code: "USER_QUESTIONS_UNAVAILABLE", message: "user question capability is unavailable" } });
1086
+ return;
1087
+ }
1088
+ try {
1089
+ const body = await readJsonBody(req);
1090
+ const question = await deps.questionRegistry.answer(sessionId, requestId, body);
1091
+ sendJson(res, 200, { ok: true, resolved: true, question });
1092
+ }
1093
+ catch (error) {
1094
+ const message = error instanceof Error ? error.message : String(error);
1095
+ const status = message === "USER_QUESTION_NOT_FOUND" ? 404 : message === "USER_QUESTION_STALE" ? 409 : 400;
1096
+ sendJson(res, status, { ok: false, error: { code: message, message } });
1097
+ }
1098
+ }
1099
+ export async function handleCancelUserQuestion(req, res, deps, sessionId, requestId) {
1100
+ const gate = gateMutation(req, deps);
1101
+ if (!gate.ok) {
1102
+ sendJson(res, gate.status, { ok: false, error: { code: gate.code, message: gate.message } });
1103
+ return;
1104
+ }
1105
+ if (!(await requireChatSession(res, deps, sessionId)))
1106
+ return;
1107
+ if (!deps.questionRegistry) {
1108
+ sendJson(res, 503, { ok: false, error: { code: "USER_QUESTIONS_UNAVAILABLE", message: "user question capability is unavailable" } });
1109
+ return;
1110
+ }
1111
+ try {
1112
+ const body = await readJsonBody(req).catch(() => ({}));
1113
+ const reason = typeof body.reason === "string" && body.reason.trim() ? body.reason.trim().slice(0, 120) : "USER_CANCELLED";
1114
+ const question = await deps.questionRegistry.cancel(sessionId, requestId, reason);
1115
+ sendJson(res, 200, { ok: true, cancelled: true, question });
1116
+ }
1117
+ catch (error) {
1118
+ const message = error instanceof Error ? error.message : String(error);
1119
+ const status = message === "USER_QUESTION_NOT_FOUND" ? 404 : 409;
1120
+ sendJson(res, status, { ok: false, error: { code: message, message } });
1121
+ }
1122
+ }
888
1123
  export async function handleListChatSessions(_req, res, deps) {
889
1124
  const sessions = await deps.store.list({ includeArchived: true });
890
1125
  sendJson(res, 200, {
@@ -900,7 +1135,8 @@ export async function handleListChatSessions(_req, res, deps) {
900
1135
  : active
901
1136
  ? "running"
902
1137
  : "idle",
903
- deferredTurn: projectDeferredTurnSummary(session.deferredTurn),
1138
+ deferredTurn: projectDeferredTurnSummary(session.deferredTurns?.[0]),
1139
+ deferredTurns: projectDeferredTurnSummaries(session.deferredTurns ?? []),
904
1140
  };
905
1141
  }),
906
1142
  hasBash: true,
@@ -1147,6 +1383,7 @@ export async function handleChatPrompt(req, res, deps, sessionId) {
1147
1383
  store: deps.store,
1148
1384
  actionContext,
1149
1385
  sessionId,
1386
+ turnId,
1150
1387
  text,
1151
1388
  images,
1152
1389
  signal: ac.signal,
@@ -1442,6 +1679,7 @@ export async function handleCreateChatTurn(req, res, deps, sessionId, trustedPay
1442
1679
  store: deps.store,
1443
1680
  actionContext,
1444
1681
  sessionId,
1682
+ turnId: turn.turnId,
1445
1683
  text,
1446
1684
  images,
1447
1685
  signal: controller.signal,
@@ -1527,12 +1765,16 @@ export async function handleCreateChatTurn(req, res, deps, sessionId, trustedPay
1527
1765
  }
1528
1766
  })();
1529
1767
  }
1530
- function emitDeferredTurnUpdate(deps, sessionId, turnId, deferredTurn, revision) {
1768
+ async function emitDeferredTurnUpdate(deps, sessionId, turnId, _deferredTurn, _revision) {
1769
+ const record = await deps.store.get(sessionId);
1770
+ if (!record)
1771
+ return;
1531
1772
  deps.events.append(sessionId, turnId, {
1532
1773
  kind: "queue_update",
1533
1774
  data: {
1534
- deferredTurn: projectDeferredTurnSummary(deferredTurn),
1535
- revision,
1775
+ deferredTurn: projectDeferredTurnSummary(record.deferredTurns?.[0]),
1776
+ deferredTurns: projectDeferredTurnSummaries(record.deferredTurns ?? []),
1777
+ revision: record.deferredQueueRevision ?? 0,
1536
1778
  },
1537
1779
  });
1538
1780
  }
@@ -1568,7 +1810,7 @@ export async function dispatchDeferredTurn(deps, sessionId) {
1568
1810
  deferredDispatchingSessions.add(sessionId);
1569
1811
  try {
1570
1812
  let record = await deps.store.get(sessionId);
1571
- let deferred = record?.deferredTurn;
1813
+ let deferred = record?.deferredTurns?.find((item) => item.state === "dispatching" || item.state === "queued");
1572
1814
  if (!deferred)
1573
1815
  return;
1574
1816
  if (deferred.state === "accepted")
@@ -1580,17 +1822,19 @@ export async function dispatchDeferredTurn(deps, sessionId) {
1580
1822
  if (existing) {
1581
1823
  const accepted = await deps.store.transitionDeferredTurn(sessionId, {
1582
1824
  expectedRevision: deferred.revision,
1825
+ clientRequestId: deferred.clientRequestId,
1583
1826
  state: "accepted",
1584
1827
  acceptedTurnId: existing.turnId,
1585
1828
  });
1586
1829
  if (accepted)
1587
- emitDeferredTurnUpdate(deps, sessionId, existing.turnId, accepted, accepted.revision);
1830
+ await emitDeferredTurnUpdate(deps, sessionId, existing.turnId, accepted, accepted.revision);
1588
1831
  return;
1589
1832
  }
1590
1833
  if (deps.events.getActiveTurn(sessionId))
1591
1834
  return;
1592
1835
  const requeued = await deps.store.transitionDeferredTurn(sessionId, {
1593
1836
  expectedRevision: deferred.revision,
1837
+ clientRequestId: deferred.clientRequestId,
1594
1838
  state: "queued",
1595
1839
  });
1596
1840
  if (!requeued)
@@ -1601,11 +1845,11 @@ export async function dispatchDeferredTurn(deps, sessionId) {
1601
1845
  const runtimeBusy = Boolean(runtimeFacts && Object.values(runtimeFacts).some((value) => value === true));
1602
1846
  if (deps.events.getActiveTurn(sessionId) || runtimeBusy)
1603
1847
  return;
1604
- const claim = await deps.store.claimDeferredTurn(sessionId, deferred.revision);
1848
+ const claim = await deps.store.claimDeferredTurn(sessionId, deferred.revision, deferred.clientRequestId);
1605
1849
  if (!claim.ok)
1606
1850
  return;
1607
1851
  deferred = claim.deferredTurn;
1608
- emitDeferredTurnUpdate(deps, sessionId, deferred.blockedByTurnId, deferred, deferred.revision);
1852
+ await emitDeferredTurnUpdate(deps, sessionId, deferred.blockedByTurnId, deferred, deferred.revision);
1609
1853
  const started = await startTrustedChatTurn(deps, sessionId, {
1610
1854
  clientRequestId: deferred.clientRequestId,
1611
1855
  text: deferred.text,
@@ -1624,7 +1868,7 @@ export async function dispatchDeferredTurn(deps, sessionId) {
1624
1868
  });
1625
1869
  const cleared = await deps.store.clearDeferredTurnForRequest(sessionId, deferred.clientRequestId);
1626
1870
  if (cleared)
1627
- emitDeferredTurnUpdate(deps, sessionId, turnId, undefined, cleared.clearedRevision);
1871
+ await emitDeferredTurnUpdate(deps, sessionId, turnId, undefined, cleared.clearedRevision);
1628
1872
  },
1629
1873
  });
1630
1874
  const accepted = started.body.accepted === true;
@@ -1634,11 +1878,12 @@ export async function dispatchDeferredTurn(deps, sessionId) {
1634
1878
  if (accepted && turn?.turnId) {
1635
1879
  const next = await deps.store.transitionDeferredTurn(sessionId, {
1636
1880
  expectedRevision: deferred.revision,
1881
+ clientRequestId: deferred.clientRequestId,
1637
1882
  state: "accepted",
1638
1883
  acceptedTurnId: turn.turnId,
1639
1884
  });
1640
1885
  if (next)
1641
- emitDeferredTurnUpdate(deps, sessionId, turn.turnId, next, next.revision);
1886
+ await emitDeferredTurnUpdate(deps, sessionId, turn.turnId, next, next.revision);
1642
1887
  return;
1643
1888
  }
1644
1889
  const error = started.body.error && typeof started.body.error === "object"
@@ -1647,14 +1892,16 @@ export async function dispatchDeferredTurn(deps, sessionId) {
1647
1892
  if (error.code === "TURN_ACTIVE") {
1648
1893
  const next = await deps.store.transitionDeferredTurn(sessionId, {
1649
1894
  expectedRevision: deferred.revision,
1895
+ clientRequestId: deferred.clientRequestId,
1650
1896
  state: "queued",
1651
1897
  });
1652
1898
  if (next)
1653
- emitDeferredTurnUpdate(deps, sessionId, deferred.blockedByTurnId, next, next.revision);
1899
+ await emitDeferredTurnUpdate(deps, sessionId, deferred.blockedByTurnId, next, next.revision);
1654
1900
  return;
1655
1901
  }
1656
1902
  const failed = await deps.store.transitionDeferredTurn(sessionId, {
1657
1903
  expectedRevision: deferred.revision,
1904
+ clientRequestId: deferred.clientRequestId,
1658
1905
  state: "failed",
1659
1906
  lastError: {
1660
1907
  code: error.code ?? `HTTP_${started.status}`,
@@ -1662,17 +1909,18 @@ export async function dispatchDeferredTurn(deps, sessionId) {
1662
1909
  },
1663
1910
  });
1664
1911
  if (failed)
1665
- emitDeferredTurnUpdate(deps, sessionId, deferred.blockedByTurnId, failed, failed.revision);
1912
+ await emitDeferredTurnUpdate(deps, sessionId, deferred.blockedByTurnId, failed, failed.revision);
1666
1913
  }
1667
1914
  catch (error) {
1668
1915
  // Keep fire-and-forget callers safe and make a dispatch failure visible and
1669
1916
  // recoverable from the composer instead of leaving the slot indefinitely
1670
1917
  // in an ambiguous queued/dispatching state.
1671
1918
  try {
1672
- const current = (await deps.store.get(sessionId))?.deferredTurn;
1919
+ const current = (await deps.store.get(sessionId))?.deferredTurns?.find((item) => item.state === "queued" || item.state === "dispatching");
1673
1920
  if (current && (current.state === "queued" || current.state === "dispatching")) {
1674
1921
  const failed = await deps.store.transitionDeferredTurn(sessionId, {
1675
1922
  expectedRevision: current.revision,
1923
+ clientRequestId: current.clientRequestId,
1676
1924
  state: "failed",
1677
1925
  lastError: {
1678
1926
  code: "DEFERRED_TURN_DISPATCH_FAILED",
@@ -1680,7 +1928,7 @@ export async function dispatchDeferredTurn(deps, sessionId) {
1680
1928
  },
1681
1929
  });
1682
1930
  if (failed)
1683
- emitDeferredTurnUpdate(deps, sessionId, failed.blockedByTurnId, failed, failed.revision);
1931
+ await emitDeferredTurnUpdate(deps, sessionId, failed.blockedByTurnId, failed, failed.revision);
1684
1932
  }
1685
1933
  }
1686
1934
  catch {
@@ -1695,12 +1943,9 @@ export async function dispatchDeferredTurn(deps, sessionId) {
1695
1943
  export async function recoverDeferredTurns(deps) {
1696
1944
  const sessions = await deps.store.list({ includeArchived: false });
1697
1945
  await Promise.all(sessions
1698
- .filter((session) => session.deferredTurn
1699
- ? session.deferredTurn.state === "queued" ||
1700
- session.deferredTurn.state === "dispatching"
1701
- : false)
1946
+ .filter((session) => (session.deferredTurns ?? []).some((item) => item.state === "queued" || item.state === "dispatching"))
1702
1947
  .map(async (session) => {
1703
- const deferred = session.deferredTurn;
1948
+ const deferred = session.deferredTurns.find((item) => item.state === "dispatching" || item.state === "queued");
1704
1949
  const active = deps.events.getActiveTurn(session.sessionId);
1705
1950
  // A process restart cannot retain AbortController ownership. When the
1706
1951
  // durable blocker was already stopping and no execution owns it now,
@@ -1767,34 +2012,24 @@ export async function handleAbortChatTurn(req, res, deps, sessionId, turnId) {
1767
2012
  });
1768
2013
  return;
1769
2014
  }
1770
- deps.events.markAbortRequested?.(sessionId, turnId);
2015
+ const stopping = deps.events.markAbortRequested?.(sessionId, turnId) ?? turn;
2016
+ // DSH-style acknowledgement: deliver the AbortSignal synchronously, then let
2017
+ // the detached Turn closeout own SDK/process cleanup and terminal persistence.
2018
+ // The HTTP request reports that Stop was accepted; it does not wait for Pi to
2019
+ // become idle. This keeps cancellation latency independent of slow providers,
2020
+ // tools and process-tree cleanup while preserving the active-Turn lease.
1771
2021
  if (!execution.controller.signal.aborted)
1772
2022
  execution.controller.abort();
1773
- const waited = await (deps.turnExecutions?.waitForCompletion(sessionId, turnId, DEFAULT_ABORT_WAIT_MS) ?? Promise.resolve({ status: "missing" }));
1774
- if (waited.status === "completed") {
1775
- const finalTurn = deps.events.getTurn(sessionId, turnId) ??
1776
- {
1777
- ...turn,
1778
- state: waited.outcome.state,
1779
- };
1780
- sendJson(res, 200, {
1781
- ok: true,
1782
- accepted: true,
1783
- idempotent: false,
1784
- turn: turnSummary(finalTurn),
1785
- });
1786
- return;
1787
- }
1788
- // Timeout or missing completion: keep non-terminal; client reconciles via /state.
1789
- const pending = deps.events.getTurn(sessionId, turnId) ?? turn;
2023
+ const questionCancellation = deps.questionRegistry?.cancelForSession(sessionId, "TURN_ABORTED");
2024
+ if (questionCancellation)
2025
+ void questionCancellation.catch(() => undefined);
2026
+ const acceptedTurn = deps.events.getTurn(sessionId, turnId) ?? stopping;
1790
2027
  sendJson(res, 202, {
1791
- ok: false,
1792
- accepted: false,
1793
- error: {
1794
- code: "TURN_ABORT_PENDING",
1795
- message: "abort requested; turn has not yet reached a terminal state",
1796
- },
1797
- turn: turnSummary(pending),
2028
+ ok: true,
2029
+ accepted: true,
2030
+ idempotent: false,
2031
+ cleanupPending: !isTerminalTurnState(acceptedTurn.state),
2032
+ turn: turnSummary(acceptedTurn),
1798
2033
  });
1799
2034
  }
1800
2035
  export async function handleGetChatTurn(_req, res, deps, sessionId, turnId) {
@@ -1918,6 +2153,8 @@ export async function handleDeleteChatSession(req, res, deps, sessionId) {
1918
2153
  });
1919
2154
  return;
1920
2155
  }
2156
+ await deps.questionRegistry?.cancelForSession(sessionId, "SESSION_DELETED").catch(() => undefined);
2157
+ deps.todoRegistry?.clear(sessionId);
1921
2158
  deps.runtime.dispose(sessionId);
1922
2159
  deps.operationLinker.clearSession(sessionId);
1923
2160
  deps.events.clear(sessionId);
@@ -2592,6 +2829,36 @@ async function handleLinkWorkspaceOperation(req, res, deps, sessionId) {
2592
2829
  });
2593
2830
  }
2594
2831
  }
2832
+ async function handleUnlinkWorkspaceOperation(req, res, deps, sessionId, operationId) {
2833
+ const gate = gateMutation(req, deps);
2834
+ if (!gate.ok) {
2835
+ sendJson(res, gate.status, {
2836
+ ok: false,
2837
+ error: { code: gate.code, message: gate.message },
2838
+ });
2839
+ return;
2840
+ }
2841
+ if (!(await requireChatSession(res, deps, sessionId)))
2842
+ return;
2843
+ // AC-R7-001 (2026-08-23): removing a workspace-linked DAG association is
2844
+ // idempotent — a missing link is already-removed (200), never an error.
2845
+ try {
2846
+ const removed = await deps.operationLinker.unlink({
2847
+ sessionId,
2848
+ operationId,
2849
+ });
2850
+ sendJson(res, 200, { ok: true, removed });
2851
+ }
2852
+ catch (error) {
2853
+ sendJson(res, 400, {
2854
+ ok: false,
2855
+ error: {
2856
+ code: "INVALID_INPUT",
2857
+ message: error instanceof Error ? error.message : String(error),
2858
+ },
2859
+ });
2860
+ }
2861
+ }
2595
2862
  async function handleStartInterview(req, res, deps, sessionId) {
2596
2863
  const gate = gateMutation(req, deps);
2597
2864
  if (!gate.ok) {
@@ -3244,14 +3511,59 @@ export async function handleChatCompact(req, res, deps, sessionId) {
3244
3511
  return;
3245
3512
  }
3246
3513
  const rawInstructions = typeof body.instructions === "string" ? body.instructions : undefined;
3247
- const customInstructions = rawInstructions?.trim() || undefined;
3514
+ const currentTodos = projectCurrentTodos(deps.events.snapshot(sessionId));
3515
+ const customInstructions = buildTodoCompactionInstructions(rawInstructions, currentTodos);
3516
+ const turnId = deps.events.latestTurnId(sessionId) ?? `${sessionId}:compact`;
3517
+ const compactionId = `${sessionId}:compaction:${randomUUID()}`;
3518
+ deps.events.append(sessionId, turnId, {
3519
+ kind: "compaction_start",
3520
+ data: {
3521
+ schemaVersion: 1,
3522
+ compactionId,
3523
+ reason: "manual",
3524
+ status: "running",
3525
+ willRetry: false,
3526
+ },
3527
+ });
3248
3528
  try {
3249
3529
  const snapshot = await deps.runtime.compact(sessionId, customInstructions);
3250
- const event = deps.events.append(sessionId, deps.events.latestTurnId(sessionId) ?? `${sessionId}:compact`, { kind: "compact", data: snapshot });
3530
+ const event = deps.events.append(sessionId, turnId, {
3531
+ kind: "compaction_end",
3532
+ data: {
3533
+ schemaVersion: 1,
3534
+ compactionId,
3535
+ reason: "manual",
3536
+ status: "completed",
3537
+ summary: snapshot.summary,
3538
+ ...(snapshot.firstKeptEntryId
3539
+ ? { firstKeptEntryId: snapshot.firstKeptEntryId }
3540
+ : {}),
3541
+ ...(snapshot.tokensBefore === undefined
3542
+ ? {}
3543
+ : { tokensBefore: snapshot.tokensBefore }),
3544
+ ...(snapshot.estimatedTokensAfter === undefined
3545
+ ? {}
3546
+ : { estimatedTokensAfter: snapshot.estimatedTokensAfter }),
3547
+ willRetry: false,
3548
+ },
3549
+ });
3251
3550
  sendJson(res, 200, { ok: true, snapshot, eventId: event.eventId });
3252
3551
  }
3253
3552
  catch (error) {
3254
3553
  const classified = classifyChatCompactionFailure(error);
3554
+ const aborted = error instanceof Error &&
3555
+ (error.name === "AbortError" || /cancel(?:led|ed)|aborted/i.test(error.message));
3556
+ deps.events.append(sessionId, turnId, {
3557
+ kind: "compaction_end",
3558
+ data: {
3559
+ schemaVersion: 1,
3560
+ compactionId,
3561
+ reason: "manual",
3562
+ status: aborted ? "aborted" : "failed",
3563
+ willRetry: false,
3564
+ ...(aborted ? {} : { errorMessage: classified.message }),
3565
+ },
3566
+ });
3255
3567
  sendJson(res, classified.status, {
3256
3568
  ok: false,
3257
3569
  error: { code: classified.code, message: classified.message },
@@ -3280,8 +3592,10 @@ export async function handleChatMessages(res, deps, sessionId, url) {
3280
3592
  /**
3281
3593
  * Dispatch a chat HTTP request. Returns true if handled.
3282
3594
  * Mounted under /api/operator/v1/chat/*.
3595
+ * workspaceScope (AC-R7-002): the explicit owning workspaceId for session
3596
+ * create, forwarded to handleCreateChatSession → store.create.
3283
3597
  */
3284
- export async function handleChatRequest(req, res, deps, pathname) {
3598
+ export async function handleChatRequest(req, res, deps, pathname, workspaceScope) {
3285
3599
  if (!deps)
3286
3600
  return false;
3287
3601
  if (!pathname.startsWith("/api/operator/v1/chat/") &&
@@ -3359,6 +3673,16 @@ export async function handleChatRequest(req, res, deps, pathname) {
3359
3673
  await handleStartInterview(req, res, deps, decodeURIComponent(interviewStart[1]));
3360
3674
  return true;
3361
3675
  }
3676
+ const userQuestionAnswer = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/questions\/([^/]+)\/answer$/);
3677
+ if (method === "POST" && userQuestionAnswer) {
3678
+ await handleAnswerUserQuestion(req, res, deps, decodeURIComponent(userQuestionAnswer[1]), decodeURIComponent(userQuestionAnswer[2]));
3679
+ return true;
3680
+ }
3681
+ const userQuestionCancel = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/questions\/([^/]+)\/cancel$/);
3682
+ if (method === "POST" && userQuestionCancel) {
3683
+ await handleCancelUserQuestion(req, res, deps, decodeURIComponent(userQuestionCancel[1]), decodeURIComponent(userQuestionCancel[2]));
3684
+ return true;
3685
+ }
3362
3686
  const interviewAnswer = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/interviews\/([^/]+)\/answers$/);
3363
3687
  if (method === "POST" && interviewAnswer) {
3364
3688
  await handleAnswerInterview(req, res, deps, decodeURIComponent(interviewAnswer[1]), decodeURIComponent(interviewAnswer[2]));
@@ -3465,6 +3789,11 @@ export async function handleChatRequest(req, res, deps, pathname) {
3465
3789
  await handleDeleteDeferredTurn(req, res, deps, decodeURIComponent(deferredTurn[1]));
3466
3790
  return true;
3467
3791
  }
3792
+ const deferredTurnSteer = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/deferred-turn\/([^/]+)\/steer$/);
3793
+ if (method === "POST" && deferredTurnSteer) {
3794
+ await handleSteerDeferredTurn(req, res, deps, decodeURIComponent(deferredTurnSteer[1]), decodeURIComponent(deferredTurnSteer[2]));
3795
+ return true;
3796
+ }
3468
3797
  const sessionTurnAbort = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/turns\/([^/]+)\/abort$/);
3469
3798
  if (method === "POST" && sessionTurnAbort) {
3470
3799
  await handleAbortChatTurn(req, res, deps, decodeURIComponent(sessionTurnAbort[1]), decodeURIComponent(sessionTurnAbort[2]));
@@ -3480,6 +3809,13 @@ export async function handleChatRequest(req, res, deps, pathname) {
3480
3809
  await handleLinkWorkspaceOperation(req, res, deps, decodeURIComponent(operationLink[1]));
3481
3810
  return true;
3482
3811
  }
3812
+ const operationLinkItem = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/operation-links\/([^/]+)$/);
3813
+ if (method === "DELETE" && operationLinkItem) {
3814
+ // AC-R7-001: Details rail DAG remove — real DELETE of the workspace
3815
+ // operation-link association (idempotent).
3816
+ await handleUnlinkWorkspaceOperation(req, res, deps, decodeURIComponent(operationLinkItem[1]), decodeURIComponent(operationLinkItem[2]));
3817
+ return true;
3818
+ }
3483
3819
  const sessionCompact = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/compact$/);
3484
3820
  if (method === "POST" && sessionCompact) {
3485
3821
  await handleChatCompact(req, res, deps, decodeURIComponent(sessionCompact[1]));
@@ -3527,7 +3863,7 @@ export async function handleChatRequest(req, res, deps, pathname) {
3527
3863
  }
3528
3864
  }
3529
3865
  if (method === "POST" && pathname === "/api/operator/v1/chat/sessions") {
3530
- await handleCreateChatSession(req, res, deps);
3866
+ await handleCreateChatSession(req, res, deps, workspaceScope);
3531
3867
  return true;
3532
3868
  }
3533
3869
  if (isMutationMethod(method)) {