@tea-agent/loop-agent 0.39.0-beta.1 → 0.39.0-beta.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 (331) hide show
  1. package/AGENTS.md +4 -1
  2. package/CHANGELOG.md +248 -95
  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 +2306 -61
  22. package/dist/executors/pi-executor.js +7 -0
  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 +180 -33
  27. package/dist/executors/shell-executor.js +1249 -224
  28. package/dist/executors/shell-write-guard.js +14 -0
  29. package/dist/executors/workspace-write-snapshot.js +68 -0
  30. package/dist/infrastructure/harness/atomic-write.js +23 -0
  31. package/dist/shared/dag-failure-category.js +150 -0
  32. package/dist/shared/dag-prompt-override.js +27 -0
  33. package/dist/shared/openspec-spec.js +70 -4
  34. package/dist/shared/operator/capabilities.js +39 -0
  35. package/dist/shared/playwright-cli-command-policy.js +15 -0
  36. package/dist/shared/update/console-notifier.js +100 -0
  37. package/dist/{cli → shared}/update/npm-client.js +70 -15
  38. package/dist/{cli → shared}/update/state.js +41 -13
  39. package/dist/task/config-types.js +50 -5
  40. package/dist/task/contract/apply.js +11 -1
  41. package/dist/task/contract/constants.js +2 -0
  42. package/dist/task/contract/hash.js +3 -0
  43. package/dist/task/contract/observe.js +25 -4
  44. package/dist/task/contract/paths.js +2 -1
  45. package/dist/task/contract/project.js +18 -1
  46. package/dist/task/contract/schema.js +4 -1
  47. package/dist/task/contract/transaction.js +12 -0
  48. package/dist/task/frontend-project-capability.js +199 -18
  49. package/dist/task/source-prepare/completeness.js +188 -0
  50. package/dist/task/source-prepare/fragment-inventory.js +477 -0
  51. package/dist/task/source-prepare/index.js +3 -0
  52. package/dist/task/source-prepare/ledger-reconciliation.js +127 -0
  53. package/dist/task/source-prepare/ledger-review.js +214 -0
  54. package/dist/task/source-prepare/ledger.js +545 -0
  55. package/dist/task/source-prepare/parse-intent.js +24 -4
  56. package/dist/task/source-prepare/prepare.js +298 -1
  57. package/dist/task/source-prepare/semantic-intake.js +25 -5
  58. package/dist/task/source-prepare/source-fidelity-pi.js +384 -0
  59. package/dist/task/source-prepare/types.js +22 -0
  60. package/dist/task/source-references.js +22 -1
  61. package/dist/worker/cli.js +18 -2
  62. package/dist/worker/console/chat/chat-event-store.js +62 -4
  63. package/dist/worker/console/chat/context-panel.js +7 -0
  64. package/dist/worker/console/chat/deferred-turn.js +12 -0
  65. package/dist/worker/console/chat/operation-card.js +8 -1
  66. package/dist/worker/console/chat/pi-console-config.js +81 -10
  67. package/dist/worker/console/chat/pi-runtime.js +264 -16
  68. package/dist/worker/console/chat/provider-error.js +98 -0
  69. package/dist/worker/console/chat/routes.js +298 -32
  70. package/dist/worker/console/chat/session-stats.js +179 -0
  71. package/dist/worker/console/chat/session-store.js +93 -63
  72. package/dist/worker/console/chat/todos.js +115 -0
  73. package/dist/worker/console/chat/tool-preview.js +42 -0
  74. package/dist/worker/console/chat/turn-process.js +20 -55
  75. package/dist/worker/console/chat/user-questions.js +266 -0
  76. package/dist/worker/console/chat/workspace-landing.js +14 -0
  77. package/dist/worker/console/console-handoff.js +82 -0
  78. package/dist/worker/console/console-update-and-init.js +115 -0
  79. package/dist/worker/console/console-update-runtime.js +84 -0
  80. package/dist/worker/console/dag-execution-receipt.js +33 -0
  81. package/dist/worker/console/draft-store.js +49 -0
  82. package/dist/worker/console/frontend-human-decision-adapter.js +19 -0
  83. package/dist/worker/console/frontend-split-operation-adapter.js +20 -0
  84. package/dist/worker/console/index.js +3 -0
  85. package/dist/worker/console/inspect-split.js +18 -0
  86. package/dist/worker/console/native-directory-picker.js +13 -0
  87. package/dist/worker/console/operation-run-facts.js +19 -3
  88. package/dist/worker/console/operation-store.js +227 -7
  89. package/dist/worker/console/operator-actions.js +388 -0
  90. package/dist/worker/console/operator-surface-health.js +1 -0
  91. package/dist/worker/console/operator-user-error.js +2 -2
  92. package/dist/worker/console/prd-intake-bridge.js +13 -0
  93. package/dist/worker/console/routes.js +355 -3
  94. package/dist/worker/console/security.js +67 -13
  95. package/dist/worker/console/server.js +609 -164
  96. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4kVYweA.js → abnfDiagram-N423BO3Z-BAsjg7ew.js} +1 -1
  97. package/dist/worker/console/static/assets/{arc-Bj2M8iO5.js → arc-CyIlrwZw.js} +1 -1
  98. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CLlXe4-6.js → architectureDiagram-T3A2C74G-Bo4tobhH.js} +1 -1
  99. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-psEp5xH0.js → blockDiagram-VBNYF7ZC-Dfpow9gu.js} +1 -1
  100. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DeKcOCGJ.js → c4Diagram-5PPSVZJV-DPmzG_B4.js} +1 -1
  101. package/dist/worker/console/static/assets/channel-DSrJAn0-.js +1 -0
  102. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-K0C744p1.js → chunk-2GRJ4B5K-pKQpB1UW.js} +1 -1
  103. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-xbNw4J9-.js → chunk-2Q5K7J3B-5n9YYquZ.js} +1 -1
  104. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLaSDUO.js → chunk-5RXB4S5H-DP3CtA2a.js} +1 -1
  105. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CtiPlKel.js → chunk-5VM5RSS4-DYxMJPUy.js} +1 -1
  106. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DZtolir7.js → chunk-6Q2QTUOP-CS3D50Io.js} +1 -1
  107. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CPcdNeew.js → chunk-GF5L2VYU-gSdDfo5_.js} +1 -1
  108. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-NgHc6V37.js → chunk-JWPE2WC7-A48q0ZNt.js} +1 -1
  109. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DQ_T-hg8.js → chunk-KBJHAD2P-ow8Mm3DY.js} +1 -1
  110. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DgLsxcVP.js → chunk-RYQCIY6F-CMKfLWYE.js} +1 -1
  111. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-UrVoM9z1.js → chunk-XXDRQBXY-BiTBdQPX.js} +1 -1
  112. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DeMmPl0_.js +1 -0
  113. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DeMmPl0_.js +1 -0
  114. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CLkYvTb3.js → cose-bilkent-JH36ORCC-Cm7exRz_.js} +1 -1
  115. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BwT_xKrE.js → cynefin-VYW2F7L2-D9O5qGOu.js} +1 -1
  116. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bi9tuzif.js → cynefinDiagram-MW4NZA55-CkvQnako.js} +1 -1
  117. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BCIqkWBV.js → dagre-VZM6K2ZE-B1rzp7CB.js} +1 -1
  118. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dip6l9_Z.js → diagram-7IWD3JNH-BP07Ipcc.js} +1 -1
  119. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B-xkh_wK.js → diagram-B4RE2ZJO-BlPzweQb.js} +1 -1
  120. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DZO0kTt_.js → diagram-LBJQPF4R-CxPa7n0S.js} +1 -1
  121. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D6ZplNbZ.js → diagram-Q27KOJAE-CMTZs53A.js} +1 -1
  122. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPekbhoS.js → diagram-UB23O5K3-CxG_X52r.js} +1 -1
  123. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BtmaJpK5.js → ebnfDiagram-BXEA7PRR-D8axtJM0.js} +1 -1
  124. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-6DS0Xc44.js → erDiagram-JOGREHBK-DIsGzn7b.js} +1 -1
  125. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CihnROSm.js → flowDiagram-UKHOOZJN-CU9zkDG0.js} +1 -1
  126. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CZC4zOE2.js → ganttDiagram-PKOTCBZU-DCjTmz5P.js} +1 -1
  127. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js → gitGraphDiagram-DS77QQ5N-BksdmFzg.js} +1 -1
  128. package/dist/worker/console/static/assets/index-B-O5ChYq.css +1 -0
  129. package/dist/worker/console/static/assets/index-CfnY8AQx.js +407 -0
  130. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DlSl4BGx.js → infoDiagram-6WML65LV-CGzzzJtL.js} +1 -1
  131. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-snnzFl_U.js → ishikawaDiagram-WSZJBQD7-BdseB80N.js} +1 -1
  132. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BDE7qpMx.js → journeyDiagram-NVQOT4AX-DM9gsgha.js} +1 -1
  133. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CD2Ci04G.js → kanban-definition-27J2QSJJ-3q-_9R2b.js} +1 -1
  134. package/dist/worker/console/static/assets/{linear-DdnapKIH.js → linear-B8N-Mxsd.js} +1 -1
  135. package/dist/worker/console/static/assets/{mermaid.core-BVjAT9b8.js → mermaid.core-EPIHUUsu.js} +5 -5
  136. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-D0yJk-zA.js → mindmap-definition-FAOFIHXS-CxwO12aa.js} +1 -1
  137. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BVo9tFP-.js → pegDiagram-VL7TDLO6-bPqbVX6Y.js} +1 -1
  138. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DnOV-mZ_.js → pieDiagram-7S7Q4E2Y-KzqIvzeX.js} +1 -1
  139. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CzUJo58i.js → quadrantDiagram-CIZ2JOQS-CyofpWiV.js} +1 -1
  140. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Bvikrolh.js → railroadDiagram-AXF67PYL-cTWJvLTP.js} +1 -1
  141. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DtaSVCap.js → requirementDiagram-LRYGKXZP-BK--q27B.js} +1 -1
  142. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-C2c9A0wW.js → sankeyDiagram-W5VNT64P-C0SDOHXK.js} +1 -1
  143. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DmXJcU7r.js → sequenceDiagram-SI44F4Z6-Cong6YIo.js} +1 -1
  144. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4GUFW92.js → sizeCapture-X5ZJPWSS-DUVkgZx8.js} +1 -1
  145. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Dsad2MXf.js → stateDiagram-OKZ733FA-C7XgxuZg.js} +1 -1
  146. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Dzbnd5oX.js +1 -0
  147. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DGq48Fbi.js → swimlanes-SLNWSIFB-D9Ayvwcy.js} +2 -2
  148. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BY0hHk6G.js +8 -0
  149. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CSSi6OFf.js → timeline-definition-Z64GVDOM-Byb000cH.js} +1 -1
  150. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BqyzPrWv.js → vennDiagram-T6HMQDX7-DMLegPt4.js} +1 -1
  151. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DlznVb6S.js → wardleyDiagram-T6FBY63Y-ChMMeOsP.js} +1 -1
  152. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CfBcgJ_K.js → xychartDiagram-ELKLHX3M-bEMEPisW.js} +1 -1
  153. package/dist/worker/console/static/index.html +2 -2
  154. package/dist/worker/console/static-src/active-run-badge.js +17 -0
  155. package/dist/worker/console/static-src/app/console-types.js +68 -4
  156. package/dist/worker/console/static-src/app/useConsoleShell.js +30 -12
  157. package/dist/worker/console/static-src/app/useOperatorActions.js +41 -6
  158. package/dist/worker/console/static-src/app/useRecoveryConsole.js +45 -10
  159. package/dist/worker/console/static-src/app/useRunProgress.js +47 -0
  160. package/dist/worker/console/static-src/app/useTaskWizard.js +74 -2
  161. package/dist/worker/console/static-src/night/useNightBoard.js +8 -5
  162. package/dist/worker/console/static-src/operator-chat/cards/failure-category-advice.js +25 -0
  163. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +140 -8
  164. package/dist/worker/console/static-src/operator-chat/details-dag-actions.js +122 -0
  165. package/dist/worker/console/static-src/operator-chat/input-history.js +76 -0
  166. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
  167. package/dist/worker/console/static-src/operator-chat/refs.js +3 -0
  168. package/dist/worker/console/static-src/operator-chat/runtime-selection-labels.js +27 -0
  169. package/dist/worker/console/static-src/operator-chat/turn-submission.js +16 -3
  170. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +168 -72
  171. package/dist/worker/console/static-src/operator-chat/useChatStream.js +102 -21
  172. package/dist/worker/console/static-src/operator-chat/useChatThread.js +184 -4
  173. package/dist/worker/console/static-src/operator-chat/useComposer.js +32 -0
  174. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +63 -9
  175. package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
  176. package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -3
  177. package/dist/worker/console/static-src/pages/tasks/run-id-resolution.js +79 -0
  178. package/dist/worker/console/static-src/pages/tasks/run-ownership-verify.js +23 -0
  179. package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +110 -0
  180. package/dist/worker/console/static-src/shell/useWorkspaces.js +299 -0
  181. package/dist/worker/console/static-src/shell/workspace-route.js +337 -0
  182. package/dist/worker/console/workspace-context.js +344 -0
  183. package/dist/worker/console/workspace-registry.js +214 -0
  184. package/dist/worker/loop-agent/loop-agent-client.js +7 -3
  185. package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
  186. package/dist/worker/materialize/harness-task-lineage.js +5 -2
  187. package/dist/worker/observability/init-runtime-activity.js +26 -0
  188. package/dist/worker/observability/read-model.js +22 -0
  189. package/dist/worker/observe/node-input.js +160 -3
  190. package/dist/worker/observe/node-process.js +377 -0
  191. package/dist/worker/observe/routes.js +224 -3
  192. package/dist/worker/observe/server.js +4 -0
  193. package/dist/worker/observe/shell-handler-keys.js +34 -0
  194. package/dist/worker/observe/static/api.js +90 -4
  195. package/dist/worker/observe/static/app.js +18 -70
  196. package/dist/worker/observe/static/constants.js +8 -1
  197. package/dist/worker/observe/static/dag-history-labels.js +4 -0
  198. package/dist/worker/observe/static/dag-node-purpose.d.ts +6 -0
  199. package/dist/worker/observe/static/dag-node-purpose.js +208 -0
  200. package/dist/worker/observe/static/format.js +60 -0
  201. package/dist/worker/observe/static/index.html +6 -1
  202. package/dist/worker/observe/static/inspect-workspace.js +307 -0
  203. package/dist/worker/observe/static/markdown-render.js +20 -1
  204. package/dist/worker/observe/static/operator-chrome.css +145 -20
  205. package/dist/worker/observe/static/operator-chrome.d.ts +20 -3
  206. package/dist/worker/observe/static/operator-chrome.js +330 -96
  207. package/dist/worker/observe/static/prompt-restart-candidates.d.ts +18 -0
  208. package/dist/worker/observe/static/prompt-restart-candidates.js +36 -0
  209. package/dist/worker/observe/static/router.d.ts +46 -0
  210. package/dist/worker/observe/static/router.js +25 -1
  211. package/dist/worker/observe/static/state.d.ts +50 -0
  212. package/dist/worker/observe/static/state.js +184 -1
  213. package/dist/worker/observe/static/styles.css +395 -0
  214. package/dist/worker/observe/static/views/dag-graph.js +4 -2
  215. package/dist/worker/observe/static/views/dag-inspector.js +1067 -9
  216. package/dist/worker/observe/static/views/dag.d.ts +13 -0
  217. package/dist/worker/observe/static/views/dag.js +14 -5
  218. package/dist/worker/observe/static/views/dashboard.js +6 -4
  219. package/dist/worker/observe/static/views/failures.js +5 -2
  220. package/dist/worker/observe/static/views/night.js +2 -2
  221. package/dist/worker/observe/static/views/pool.js +1 -0
  222. package/dist/worker/observe/static/views/run.js +3 -1
  223. package/dist/worker/observe/static/views/session-timeline.js +78 -9
  224. package/dist/worker/observe/static/views/task.js +56 -1
  225. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +49 -3
  226. package/dist/workflows/dag/backend-test-markdown-workflow.js +176 -1
  227. package/dist/workflows/dag/backend-test-pytest-collection.js +98 -14
  228. package/dist/workflows/dag/backend-test-writer-completeness.js +10 -5
  229. package/dist/workflows/dag/contract-output-registry.js +15 -0
  230. package/dist/workflows/dag/contract-validator-registrations.js +1 -2
  231. package/dist/workflows/dag/dynamic-runtime/loop-until.js +1 -1
  232. package/dist/workflows/dag/dynamic-runtime/map.js +1 -1
  233. package/dist/workflows/dag/failure-category.js +7 -116
  234. package/dist/workflows/dag/frontend-closeout.js +221 -0
  235. package/dist/workflows/dag/frontend-design-policy.js +400 -0
  236. package/dist/workflows/dag/frontend-human-decision.js +182 -0
  237. package/dist/workflows/dag/frontend-implementation-contract.js +538 -166
  238. package/dist/workflows/dag/frontend-plan-render.js +24 -0
  239. package/dist/workflows/dag/frontend-prewrite-gate.js +388 -392
  240. package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
  241. package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
  242. package/dist/workflows/dag/frontend-recovery-controller.js +202 -0
  243. package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
  244. package/dist/workflows/dag/frontend-recovery-plan.js +17 -10
  245. package/dist/workflows/dag/frontend-recovery-run.js +33 -20
  246. package/dist/workflows/dag/frontend-repair.js +7 -424
  247. package/dist/workflows/dag/frontend-review-context.js +288 -14
  248. package/dist/workflows/dag/frontend-review-findings.js +270 -0
  249. package/dist/workflows/dag/frontend-risk.js +15 -2
  250. package/dist/workflows/dag/frontend-shadow-dual-write.js +814 -0
  251. package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
  252. package/dist/workflows/dag/frontend-shape-facts.js +409 -0
  253. package/dist/workflows/dag/frontend-shape.js +427 -0
  254. package/dist/workflows/dag/frontend-source-fidelity-ledger.js +108 -0
  255. package/dist/workflows/dag/frontend-split-application-service.js +203 -0
  256. package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
  257. package/dist/workflows/dag/frontend-test-case-checklist.js +30 -4
  258. package/dist/workflows/dag/frontend-test-case-manifest.js +11 -4
  259. package/dist/workflows/dag/frontend-test-case-quality.js +11 -16
  260. package/dist/workflows/dag/frontend-test-environment-probe.js +230 -0
  261. package/dist/workflows/dag/frontend-test-html-report.js +3 -1
  262. package/dist/workflows/dag/frontend-test-l5-report.js +3 -1
  263. package/dist/workflows/dag/frontend-test-layout.js +159 -0
  264. package/dist/workflows/dag/frontend-test-markdown.js +61 -0
  265. package/dist/workflows/dag/frontend-test-result-contract.js +188 -42
  266. package/dist/workflows/dag/frontend-test-standard-scenarios.js +70 -0
  267. package/dist/workflows/dag/frontend-typed-event-store.js +452 -0
  268. package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
  269. package/dist/workflows/dag/frontend-verification-trace.js +52 -17
  270. package/dist/workflows/dag/frontend-worktree-diff.js +250 -17
  271. package/dist/workflows/dag/frontend-writer-admission.js +272 -0
  272. package/dist/workflows/dag/frontend-writer-status.js +244 -0
  273. package/dist/workflows/dag/init-hybrid.js +984 -613
  274. package/dist/workflows/dag/interrupt-request.js +10 -3
  275. package/dist/workflows/dag/lifecycle.js +3 -3
  276. package/dist/workflows/dag/node-execution.js +468 -58
  277. package/dist/workflows/dag/prompt.js +44 -19
  278. package/dist/workflows/dag/recovery-lease.js +80 -0
  279. package/dist/workflows/dag/report.js +37 -1
  280. package/dist/workflows/dag/rerun-feedback.js +124 -1
  281. package/dist/workflows/dag/rerun-plan.js +144 -5
  282. package/dist/workflows/dag/rerun-run.js +81 -6
  283. package/dist/workflows/dag/rerun-task.js +29 -0
  284. package/dist/workflows/dag/retry-policy.js +318 -8
  285. package/dist/workflows/dag/runner.js +429 -122
  286. package/dist/workflows/dag/scheduler.js +114 -16
  287. package/dist/workflows/dag/structured-output-repair.js +712 -0
  288. package/dist/workflows/dag/types.js +321 -31
  289. package/dist/workflows/dag/upstream-artifacts.js +0 -4
  290. package/dist/workflows/dag/validate.js +92 -32
  291. package/docs/README.md +2 -0
  292. package/docs/architecture/evolution.md +45 -0
  293. package/docs/init-surface.manifest.json +9 -0
  294. package/docs/operations/README.md +1 -1
  295. package/docs/operations/local-development-environment.md +21 -0
  296. package/docs/templates/README.md +2 -1
  297. package/docs/templates/agent-dag-report.schema.json +8 -2
  298. package/docs/templates/agent-dag.schema.json +45 -1
  299. package/docs/templates/frontend-implementation-contract.schema.json +684 -24
  300. package/docs/templates/frontend-test-case-checklist.md +2 -2
  301. package/docs/templates/frontend-test-dag.json +8 -10
  302. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
  303. package/docs/templates/init-managed-agents.md +13 -3
  304. package/docs/templates/spec-registry.schema.json +45 -0
  305. package/harness.json +3 -3
  306. package/package.json +7 -1
  307. package/scripts/next-info.mjs +356 -0
  308. package/scripts/next-publish-gate.mjs +238 -0
  309. package/scripts/release-source-binding.mjs +251 -0
  310. package/skills/codebase-scout/SKILL.md +1 -1
  311. package/skills/fe-test-ui-scout/SKILL.md +65 -0
  312. package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
  313. package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
  314. package/skills/frontend-bounded-implement/SKILL.md +8 -14
  315. package/skills/frontend-design-review/SKILL.md +20 -41
  316. package/skills/frontend-implementation/SKILL.md +3 -4
  317. package/skills/frontend-implementation/references/design-spec.md +15 -10
  318. package/skills/frontend-implementation/references/node-contracts.md +23 -13
  319. package/skills/frontend-review/SKILL.md +22 -14
  320. package/skills/frontend-review/references/review-findings.md +6 -7
  321. package/skills/loop-agent/references/command-reference.md +12 -4
  322. package/skills/loop-agent/references/hybrid-dag.md +8 -1
  323. package/skills/playwright-cli/SKILL.md +23 -1
  324. package/skills/playwright-cli-case-generator/SKILL.md +29 -8
  325. package/dist/worker/console/static/assets/channel-CPF4N7pf.js +0 -1
  326. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C2DwA_Y1.js +0 -1
  327. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C2DwA_Y1.js +0 -1
  328. package/dist/worker/console/static/assets/index-DTZOKgAn.js +0 -404
  329. package/dist/worker/console/static/assets/index-Ya5FE7cD.css +0 -1
  330. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8FB_cNk.js +0 -1
  331. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CrZYaWfx.js +0 -8
@@ -17,7 +17,7 @@ import { OPERATOR_CHAT_ALLOWED_TOOLS, OPERATOR_CHAT_DENIED_TOOLS, OPERATOR_CHAT_
17
17
  import { buildModelCallableToolSchemas } from "./tool-adapter.js";
18
18
  import { cleanChatTitle, acceptedOperationIdFromResult } from "./session-store.js";
19
19
  import { hashChatTurnPayload, projectChatToolEventPayload, projectCompactSnapshot, } from "./chat-event-store.js";
20
- import { projectDeferredTurnSummary, } from "./deferred-turn.js";
20
+ import { projectDeferredTurnSummary, projectDeferredTurnSummaries, } from "./deferred-turn.js";
21
21
  import { THINKING_LEVELS, RuntimeSnapshotUnavailableError, } from "./pi-runtime.js";
22
22
  import { ResourcePreferencesError, setSkillAutoInvocationPreference, } from "./resource-preferences-store.js";
23
23
  import { ComposerDraftStore } from "./composer-draft-store.js";
@@ -33,9 +33,11 @@ import { dispatchOperatorAction } from "../operator-actions.js";
33
33
  import { makeHumanGateCard, } from "./human-gate-card.js";
34
34
  import { ContractApplyReceiptStore } from "./contract-apply-receipt-store.js";
35
35
  import { projectTaskContext } from "./context-panel.js";
36
+ import { deriveChatSessionTelemetry } from "./session-stats.js";
36
37
  import { contractApplyPayloadHash, issueHumanGateToken, verifyHumanGateToken, } from "../human-gate-token.js";
37
38
  import { sendJson } from "../routes.js";
38
39
  import { openSseResponse, writeSseEvent } from "../operation-sse.js";
40
+ import { buildTodoCompactionInstructions, projectCurrentTodos } from "./todos.js";
39
41
  function turnSummary(turn) {
40
42
  return {
41
43
  turnId: turn.turnId,
@@ -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,19 @@ 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 "error": {
166
+ const content = parseAssistantContent(event.content);
167
+ return {
168
+ kind: "error",
169
+ data: {
170
+ message: String(event.message ?? ""),
171
+ ...(content ? { content } : {}),
172
+ ...(event.providerError && typeof event.providerError === "object"
173
+ ? { providerError: event.providerError }
174
+ : {}),
175
+ },
176
+ };
177
+ }
135
178
  default:
136
179
  return undefined;
137
180
  }
@@ -197,6 +240,13 @@ function ensureAgentEndEvent(deps, sessionId, turnId) {
197
240
  }
198
241
  }
199
242
  async function readJsonBody(req, maxBytes = 2 * 1024 * 1024) {
243
+ // AC-R5-003: the multi-workspace dispatcher (server.ts readBodyOnce)
244
+ // pre-reads and JSON-parses the body for workspaceId resolution, stashing
245
+ // it on req.__preReadBody. Consume it first so the body is parsed exactly
246
+ // once per request; re-reading the consumed stream would yield {}.
247
+ const preRead = req.__preReadBody;
248
+ if (preRead !== undefined)
249
+ return preRead;
200
250
  const chunks = [];
201
251
  let total = 0;
202
252
  for await (const chunk of req) {
@@ -270,6 +320,7 @@ export function parseChatImages(value) {
270
320
  }
271
321
  function gateMutation(req, deps) {
272
322
  const failure = evaluateMutationGate(req, {
323
+ cookieName: deps.bootToken.cookieName,
273
324
  bootToken: deps.bootToken.value,
274
325
  confirmationToken: deps.bootToken.confirmationToken,
275
326
  consoleOrigin: deps.getConsoleOrigin(),
@@ -343,7 +394,10 @@ export async function handleChatCapabilities(_req, res, deps) {
343
394
  hasBash: true,
344
395
  });
345
396
  }
346
- export async function handleCreateChatSession(req, res, deps) {
397
+ export async function handleCreateChatSession(req, res, deps,
398
+ /** Explicit owning workspaceId from the multi-workspace dispatcher
399
+ * (AC-R7-002). The body still parses exactly once via req.__preReadBody. */
400
+ workspaceId) {
347
401
  const gate = gateMutation(req, deps);
348
402
  if (!gate.ok) {
349
403
  sendJson(res, gate.status, {
@@ -421,6 +475,10 @@ export async function handleCreateChatSession(req, res, deps) {
421
475
  sessionFile: handle.sessionFile,
422
476
  modelProvider: handle.model?.provider,
423
477
  modelId: handle.model?.modelId,
478
+ // AC-R7-002: explicit parameter from the dispatcher — the runtime
479
+ // store.create wrapper is gone; body workspaceId (if any) only acts as
480
+ // a fallback for direct (non-dispatcher) handler tests.
481
+ workspaceId: workspaceId ?? (typeof body.workspaceId === "string" && body.workspaceId ? body.workspaceId : undefined),
424
482
  });
425
483
  if (snapshot)
426
484
  deps.events.append(handle.sessionId, `${handle.sessionId}:snapshot`, {
@@ -645,8 +703,10 @@ export async function handleGetChatSession(_req, res, deps, sessionId) {
645
703
  await deps.operationLinker.recoverSession(sessionId);
646
704
  const composerDraft = await new ComposerDraftStore(deps.appData).get(sessionId);
647
705
  const snapshot = deps.events.snapshot(sessionId);
706
+ const telemetry = deriveChatSessionTelemetry(snapshot);
648
707
  const lastEvent = snapshot.at(-1);
649
708
  const activeTurn = deps.events.getActiveTurn(sessionId);
709
+ const pendingQuestion = await deps.questionRegistry?.get(sessionId);
650
710
  sendJson(res, 200, {
651
711
  ok: true,
652
712
  eventEpoch: deps.events.epoch(sessionId),
@@ -659,8 +719,19 @@ export async function handleGetChatSession(_req, res, deps, sessionId) {
659
719
  ? "stopping"
660
720
  : "running"
661
721
  : "idle",
722
+ pendingQuestion: pendingQuestion ?? null,
723
+ todos: projectCurrentTodos(snapshot) ?? [],
662
724
  },
663
725
  hasBash: true,
726
+ sessionStats: telemetry.stats,
727
+ usage: telemetry.usage,
728
+ compactions: snapshot
729
+ .filter((event) => event.kind === "compact")
730
+ .map((event) => ({
731
+ eventId: event.eventId,
732
+ at: event.at,
733
+ data: event.data,
734
+ })),
664
735
  lastEventId: lastEvent?.eventId ?? null,
665
736
  lastEventSeq: lastEvent?.seq ?? 0,
666
737
  });
@@ -727,13 +798,15 @@ export async function handlePutDeferredTurn(req, res, deps, sessionId) {
727
798
  return;
728
799
  }
729
800
  const active = deps.events.getActiveTurn(sessionId);
730
- if (!active || !active.abortRequestedAt || active.turnId !== blockedByTurnId) {
801
+ // Queueing binds to the active Turn (running OR stopping); the durable
802
+ // follow-up auto-dispatches when that Turn reaches a terminal state.
803
+ if (!active || active.turnId !== blockedByTurnId) {
731
804
  sendJson(res, 409, {
732
805
  ok: false,
733
806
  accepted: false,
734
807
  error: {
735
- code: "TURN_NOT_STOPPING",
736
- message: "只有仍在停止中的当前 Turn 可以保存停止后发送消息",
808
+ code: "TURN_NOT_ACTIVE",
809
+ message: "没有匹配的运行中 Turn,无法保存待发送消息",
737
810
  },
738
811
  activeTurn: active ? turnSummary(active) : null,
739
812
  });
@@ -766,11 +839,12 @@ export async function handlePutDeferredTurn(req, res, deps, sessionId) {
766
839
  : "待发送消息无效或会话不存在",
767
840
  },
768
841
  deferredTurn: result.deferredTurn ?? null,
842
+ deferredTurns: projectDeferredTurnSummaries(await deps.store.listDeferredTurns(sessionId)),
769
843
  summary: projectDeferredTurnSummary(result.deferredTurn),
770
844
  });
771
845
  return;
772
846
  }
773
- emitDeferredTurnUpdate(deps, sessionId, blockedByTurnId, result.deferredTurn, result.deferredTurn.revision);
847
+ await emitDeferredTurnUpdate(deps, sessionId, blockedByTurnId, result.deferredTurn, result.deferredTurn.revision);
774
848
  sendJson(res, 200, {
775
849
  ok: true,
776
850
  accepted: true,
@@ -778,6 +852,7 @@ export async function handlePutDeferredTurn(req, res, deps, sessionId) {
778
852
  state: result.deferredTurn.state,
779
853
  revision: result.deferredTurn.revision,
780
854
  deferredTurn: result.deferredTurn,
855
+ deferredTurns: projectDeferredTurnSummaries(await deps.store.listDeferredTurns(sessionId)),
781
856
  summary: projectDeferredTurnSummary(result.deferredTurn),
782
857
  });
783
858
  void dispatchDeferredTurn(deps, sessionId);
@@ -823,18 +898,90 @@ export async function handleDeleteDeferredTurn(req, res, deps, sessionId) {
823
898
  : "待发送消息已变化,请刷新后重试",
824
899
  },
825
900
  deferredTurn: result.deferredTurn ?? null,
901
+ deferredTurns: projectDeferredTurnSummaries(await deps.store.listDeferredTurns(sessionId)),
826
902
  });
827
903
  return;
828
904
  }
829
905
  const clearedRevision = result.deferredTurn.revision + 1;
830
- emitDeferredTurnUpdate(deps, sessionId, result.deferredTurn.blockedByTurnId, undefined, clearedRevision);
906
+ await emitDeferredTurnUpdate(deps, sessionId, result.deferredTurn.blockedByTurnId, undefined, clearedRevision);
831
907
  sendJson(res, 200, {
832
908
  ok: true,
833
909
  cancelled: true,
834
910
  deferredTurn: result.deferredTurn,
911
+ deferredTurns: projectDeferredTurnSummaries(await deps.store.listDeferredTurns(sessionId)),
835
912
  revision: clearedRevision,
836
913
  });
837
914
  }
915
+ /** Move one addressed FIFO item into Pi's live steering queue. */
916
+ export async function handleSteerDeferredTurn(req, res, deps, sessionId, clientRequestId) {
917
+ const gate = gateMutation(req, deps);
918
+ if (!gate.ok) {
919
+ sendJson(res, gate.status, { ok: false, error: { code: gate.code, message: gate.message } });
920
+ return;
921
+ }
922
+ let body = {};
923
+ try {
924
+ body = await readJsonBody(req);
925
+ }
926
+ catch (error) {
927
+ sendJson(res, 400, { ok: false, error: { code: "INVALID_INPUT", message: error instanceof Error ? error.message : String(error) } });
928
+ return;
929
+ }
930
+ const expectedRevision = typeof body.expectedRevision === "number" && Number.isSafeInteger(body.expectedRevision)
931
+ ? body.expectedRevision
932
+ : -1;
933
+ const active = deps.events.getActiveTurn(sessionId);
934
+ if (!active) {
935
+ sendJson(res, 409, { ok: false, error: { code: "TURN_NOT_ACTIVE", message: "当前没有可插话的运行中回合" } });
936
+ return;
937
+ }
938
+ const claim = await deps.store.claimDeferredTurn(sessionId, expectedRevision, clientRequestId);
939
+ if (!claim.ok) {
940
+ sendJson(res, claim.code === "NOT_FOUND" ? 404 : 409, {
941
+ ok: false,
942
+ error: { code: claim.code, message: "排队消息已变化,请刷新后重试" },
943
+ });
944
+ return;
945
+ }
946
+ const item = claim.deferredTurn;
947
+ try {
948
+ await deps.runtime.steer(sessionId, item.text, item.images);
949
+ const userMessage = await deps.store.appendMessage(sessionId, {
950
+ role: "user",
951
+ text: item.text,
952
+ });
953
+ deps.events.append(sessionId, active.turnId, {
954
+ kind: "user_message",
955
+ data: {
956
+ clientRequestId: item.clientRequestId,
957
+ message: { id: userMessage.id, text: userMessage.text, at: userMessage.at },
958
+ },
959
+ });
960
+ await deps.store.clearDeferredTurnForRequest(sessionId, item.clientRequestId);
961
+ await emitDeferredTurnUpdate(deps, sessionId, active.turnId, undefined, item.revision + 2);
962
+ sendJson(res, 200, {
963
+ ok: true,
964
+ accepted: true,
965
+ deferredTurns: projectDeferredTurnSummaries(await deps.store.listDeferredTurns(sessionId)),
966
+ });
967
+ }
968
+ catch (error) {
969
+ const restored = await deps.store.transitionDeferredTurn(sessionId, {
970
+ expectedRevision: item.revision,
971
+ clientRequestId: item.clientRequestId,
972
+ state: "queued",
973
+ });
974
+ if (restored)
975
+ await emitDeferredTurnUpdate(deps, sessionId, active.turnId, restored, restored.revision);
976
+ sendJson(res, 409, {
977
+ ok: false,
978
+ error: {
979
+ code: "STEER_FAILED",
980
+ message: error instanceof Error ? error.message : "插话发送失败",
981
+ },
982
+ });
983
+ }
984
+ }
838
985
  /** Lightweight busy-truth endpoint for the client reconcile loop (pi-web
839
986
  * get_state equivalent): exposes the active turn + the latest turn's durable
840
987
  * outcome WITHOUT the full message payload, so the UI can poll it on an
@@ -862,6 +1009,7 @@ export async function handleGetChatSessionState(_req, res, deps, sessionId) {
862
1009
  isBashRunning: false,
863
1010
  };
864
1011
  const runtimeBusy = Object.values(runtimeFacts).some((value) => value === true);
1012
+ const pendingQuestion = await deps.questionRegistry?.get(sessionId);
865
1013
  const turnPhase = active
866
1014
  ? active.abortRequestedAt
867
1015
  ? "stopping"
@@ -881,10 +1029,60 @@ export async function handleGetChatSessionState(_req, res, deps, sessionId) {
881
1029
  latestTurn: latest ? turnSummary(latest) : null,
882
1030
  runtime: runtimeFacts,
883
1031
  turnPhase,
884
- deferredTurn: projectDeferredTurnSummary(record.deferredTurn),
1032
+ deferredTurn: projectDeferredTurnSummary(record.deferredTurns?.[0]),
1033
+ deferredTurns: projectDeferredTurnSummaries(record.deferredTurns ?? []),
1034
+ pendingQuestion: pendingQuestion ?? null,
1035
+ todos: projectCurrentTodos(deps.events.snapshot(sessionId)) ?? [],
885
1036
  },
886
1037
  });
887
1038
  }
1039
+ export async function handleAnswerUserQuestion(req, res, deps, sessionId, requestId) {
1040
+ const gate = gateMutation(req, deps);
1041
+ if (!gate.ok) {
1042
+ sendJson(res, gate.status, { ok: false, error: { code: gate.code, message: gate.message } });
1043
+ return;
1044
+ }
1045
+ if (!(await requireChatSession(res, deps, sessionId)))
1046
+ return;
1047
+ if (!deps.questionRegistry) {
1048
+ sendJson(res, 503, { ok: false, error: { code: "USER_QUESTIONS_UNAVAILABLE", message: "user question capability is unavailable" } });
1049
+ return;
1050
+ }
1051
+ try {
1052
+ const body = await readJsonBody(req);
1053
+ const question = await deps.questionRegistry.answer(sessionId, requestId, body);
1054
+ sendJson(res, 200, { ok: true, resolved: true, question });
1055
+ }
1056
+ catch (error) {
1057
+ const message = error instanceof Error ? error.message : String(error);
1058
+ const status = message === "USER_QUESTION_NOT_FOUND" ? 404 : message === "USER_QUESTION_STALE" ? 409 : 400;
1059
+ sendJson(res, status, { ok: false, error: { code: message, message } });
1060
+ }
1061
+ }
1062
+ export async function handleCancelUserQuestion(req, res, deps, sessionId, requestId) {
1063
+ const gate = gateMutation(req, deps);
1064
+ if (!gate.ok) {
1065
+ sendJson(res, gate.status, { ok: false, error: { code: gate.code, message: gate.message } });
1066
+ return;
1067
+ }
1068
+ if (!(await requireChatSession(res, deps, sessionId)))
1069
+ return;
1070
+ if (!deps.questionRegistry) {
1071
+ sendJson(res, 503, { ok: false, error: { code: "USER_QUESTIONS_UNAVAILABLE", message: "user question capability is unavailable" } });
1072
+ return;
1073
+ }
1074
+ try {
1075
+ const body = await readJsonBody(req).catch(() => ({}));
1076
+ const reason = typeof body.reason === "string" && body.reason.trim() ? body.reason.trim().slice(0, 120) : "USER_CANCELLED";
1077
+ const question = await deps.questionRegistry.cancel(sessionId, requestId, reason);
1078
+ sendJson(res, 200, { ok: true, cancelled: true, question });
1079
+ }
1080
+ catch (error) {
1081
+ const message = error instanceof Error ? error.message : String(error);
1082
+ const status = message === "USER_QUESTION_NOT_FOUND" ? 404 : 409;
1083
+ sendJson(res, status, { ok: false, error: { code: message, message } });
1084
+ }
1085
+ }
888
1086
  export async function handleListChatSessions(_req, res, deps) {
889
1087
  const sessions = await deps.store.list({ includeArchived: true });
890
1088
  sendJson(res, 200, {
@@ -900,7 +1098,8 @@ export async function handleListChatSessions(_req, res, deps) {
900
1098
  : active
901
1099
  ? "running"
902
1100
  : "idle",
903
- deferredTurn: projectDeferredTurnSummary(session.deferredTurn),
1101
+ deferredTurn: projectDeferredTurnSummary(session.deferredTurns?.[0]),
1102
+ deferredTurns: projectDeferredTurnSummaries(session.deferredTurns ?? []),
904
1103
  };
905
1104
  }),
906
1105
  hasBash: true,
@@ -1147,6 +1346,7 @@ export async function handleChatPrompt(req, res, deps, sessionId) {
1147
1346
  store: deps.store,
1148
1347
  actionContext,
1149
1348
  sessionId,
1349
+ turnId,
1150
1350
  text,
1151
1351
  images,
1152
1352
  signal: ac.signal,
@@ -1442,6 +1642,7 @@ export async function handleCreateChatTurn(req, res, deps, sessionId, trustedPay
1442
1642
  store: deps.store,
1443
1643
  actionContext,
1444
1644
  sessionId,
1645
+ turnId: turn.turnId,
1445
1646
  text,
1446
1647
  images,
1447
1648
  signal: controller.signal,
@@ -1527,12 +1728,16 @@ export async function handleCreateChatTurn(req, res, deps, sessionId, trustedPay
1527
1728
  }
1528
1729
  })();
1529
1730
  }
1530
- function emitDeferredTurnUpdate(deps, sessionId, turnId, deferredTurn, revision) {
1731
+ async function emitDeferredTurnUpdate(deps, sessionId, turnId, _deferredTurn, _revision) {
1732
+ const record = await deps.store.get(sessionId);
1733
+ if (!record)
1734
+ return;
1531
1735
  deps.events.append(sessionId, turnId, {
1532
1736
  kind: "queue_update",
1533
1737
  data: {
1534
- deferredTurn: projectDeferredTurnSummary(deferredTurn),
1535
- revision,
1738
+ deferredTurn: projectDeferredTurnSummary(record.deferredTurns?.[0]),
1739
+ deferredTurns: projectDeferredTurnSummaries(record.deferredTurns ?? []),
1740
+ revision: record.deferredQueueRevision ?? 0,
1536
1741
  },
1537
1742
  });
1538
1743
  }
@@ -1568,7 +1773,7 @@ export async function dispatchDeferredTurn(deps, sessionId) {
1568
1773
  deferredDispatchingSessions.add(sessionId);
1569
1774
  try {
1570
1775
  let record = await deps.store.get(sessionId);
1571
- let deferred = record?.deferredTurn;
1776
+ let deferred = record?.deferredTurns?.find((item) => item.state === "dispatching" || item.state === "queued");
1572
1777
  if (!deferred)
1573
1778
  return;
1574
1779
  if (deferred.state === "accepted")
@@ -1580,17 +1785,19 @@ export async function dispatchDeferredTurn(deps, sessionId) {
1580
1785
  if (existing) {
1581
1786
  const accepted = await deps.store.transitionDeferredTurn(sessionId, {
1582
1787
  expectedRevision: deferred.revision,
1788
+ clientRequestId: deferred.clientRequestId,
1583
1789
  state: "accepted",
1584
1790
  acceptedTurnId: existing.turnId,
1585
1791
  });
1586
1792
  if (accepted)
1587
- emitDeferredTurnUpdate(deps, sessionId, existing.turnId, accepted, accepted.revision);
1793
+ await emitDeferredTurnUpdate(deps, sessionId, existing.turnId, accepted, accepted.revision);
1588
1794
  return;
1589
1795
  }
1590
1796
  if (deps.events.getActiveTurn(sessionId))
1591
1797
  return;
1592
1798
  const requeued = await deps.store.transitionDeferredTurn(sessionId, {
1593
1799
  expectedRevision: deferred.revision,
1800
+ clientRequestId: deferred.clientRequestId,
1594
1801
  state: "queued",
1595
1802
  });
1596
1803
  if (!requeued)
@@ -1601,11 +1808,11 @@ export async function dispatchDeferredTurn(deps, sessionId) {
1601
1808
  const runtimeBusy = Boolean(runtimeFacts && Object.values(runtimeFacts).some((value) => value === true));
1602
1809
  if (deps.events.getActiveTurn(sessionId) || runtimeBusy)
1603
1810
  return;
1604
- const claim = await deps.store.claimDeferredTurn(sessionId, deferred.revision);
1811
+ const claim = await deps.store.claimDeferredTurn(sessionId, deferred.revision, deferred.clientRequestId);
1605
1812
  if (!claim.ok)
1606
1813
  return;
1607
1814
  deferred = claim.deferredTurn;
1608
- emitDeferredTurnUpdate(deps, sessionId, deferred.blockedByTurnId, deferred, deferred.revision);
1815
+ await emitDeferredTurnUpdate(deps, sessionId, deferred.blockedByTurnId, deferred, deferred.revision);
1609
1816
  const started = await startTrustedChatTurn(deps, sessionId, {
1610
1817
  clientRequestId: deferred.clientRequestId,
1611
1818
  text: deferred.text,
@@ -1624,7 +1831,7 @@ export async function dispatchDeferredTurn(deps, sessionId) {
1624
1831
  });
1625
1832
  const cleared = await deps.store.clearDeferredTurnForRequest(sessionId, deferred.clientRequestId);
1626
1833
  if (cleared)
1627
- emitDeferredTurnUpdate(deps, sessionId, turnId, undefined, cleared.clearedRevision);
1834
+ await emitDeferredTurnUpdate(deps, sessionId, turnId, undefined, cleared.clearedRevision);
1628
1835
  },
1629
1836
  });
1630
1837
  const accepted = started.body.accepted === true;
@@ -1634,11 +1841,12 @@ export async function dispatchDeferredTurn(deps, sessionId) {
1634
1841
  if (accepted && turn?.turnId) {
1635
1842
  const next = await deps.store.transitionDeferredTurn(sessionId, {
1636
1843
  expectedRevision: deferred.revision,
1844
+ clientRequestId: deferred.clientRequestId,
1637
1845
  state: "accepted",
1638
1846
  acceptedTurnId: turn.turnId,
1639
1847
  });
1640
1848
  if (next)
1641
- emitDeferredTurnUpdate(deps, sessionId, turn.turnId, next, next.revision);
1849
+ await emitDeferredTurnUpdate(deps, sessionId, turn.turnId, next, next.revision);
1642
1850
  return;
1643
1851
  }
1644
1852
  const error = started.body.error && typeof started.body.error === "object"
@@ -1647,14 +1855,16 @@ export async function dispatchDeferredTurn(deps, sessionId) {
1647
1855
  if (error.code === "TURN_ACTIVE") {
1648
1856
  const next = await deps.store.transitionDeferredTurn(sessionId, {
1649
1857
  expectedRevision: deferred.revision,
1858
+ clientRequestId: deferred.clientRequestId,
1650
1859
  state: "queued",
1651
1860
  });
1652
1861
  if (next)
1653
- emitDeferredTurnUpdate(deps, sessionId, deferred.blockedByTurnId, next, next.revision);
1862
+ await emitDeferredTurnUpdate(deps, sessionId, deferred.blockedByTurnId, next, next.revision);
1654
1863
  return;
1655
1864
  }
1656
1865
  const failed = await deps.store.transitionDeferredTurn(sessionId, {
1657
1866
  expectedRevision: deferred.revision,
1867
+ clientRequestId: deferred.clientRequestId,
1658
1868
  state: "failed",
1659
1869
  lastError: {
1660
1870
  code: error.code ?? `HTTP_${started.status}`,
@@ -1662,17 +1872,18 @@ export async function dispatchDeferredTurn(deps, sessionId) {
1662
1872
  },
1663
1873
  });
1664
1874
  if (failed)
1665
- emitDeferredTurnUpdate(deps, sessionId, deferred.blockedByTurnId, failed, failed.revision);
1875
+ await emitDeferredTurnUpdate(deps, sessionId, deferred.blockedByTurnId, failed, failed.revision);
1666
1876
  }
1667
1877
  catch (error) {
1668
1878
  // Keep fire-and-forget callers safe and make a dispatch failure visible and
1669
1879
  // recoverable from the composer instead of leaving the slot indefinitely
1670
1880
  // in an ambiguous queued/dispatching state.
1671
1881
  try {
1672
- const current = (await deps.store.get(sessionId))?.deferredTurn;
1882
+ const current = (await deps.store.get(sessionId))?.deferredTurns?.find((item) => item.state === "queued" || item.state === "dispatching");
1673
1883
  if (current && (current.state === "queued" || current.state === "dispatching")) {
1674
1884
  const failed = await deps.store.transitionDeferredTurn(sessionId, {
1675
1885
  expectedRevision: current.revision,
1886
+ clientRequestId: current.clientRequestId,
1676
1887
  state: "failed",
1677
1888
  lastError: {
1678
1889
  code: "DEFERRED_TURN_DISPATCH_FAILED",
@@ -1680,7 +1891,7 @@ export async function dispatchDeferredTurn(deps, sessionId) {
1680
1891
  },
1681
1892
  });
1682
1893
  if (failed)
1683
- emitDeferredTurnUpdate(deps, sessionId, failed.blockedByTurnId, failed, failed.revision);
1894
+ await emitDeferredTurnUpdate(deps, sessionId, failed.blockedByTurnId, failed, failed.revision);
1684
1895
  }
1685
1896
  }
1686
1897
  catch {
@@ -1695,12 +1906,9 @@ export async function dispatchDeferredTurn(deps, sessionId) {
1695
1906
  export async function recoverDeferredTurns(deps) {
1696
1907
  const sessions = await deps.store.list({ includeArchived: false });
1697
1908
  await Promise.all(sessions
1698
- .filter((session) => session.deferredTurn
1699
- ? session.deferredTurn.state === "queued" ||
1700
- session.deferredTurn.state === "dispatching"
1701
- : false)
1909
+ .filter((session) => (session.deferredTurns ?? []).some((item) => item.state === "queued" || item.state === "dispatching"))
1702
1910
  .map(async (session) => {
1703
- const deferred = session.deferredTurn;
1911
+ const deferred = session.deferredTurns.find((item) => item.state === "dispatching" || item.state === "queued");
1704
1912
  const active = deps.events.getActiveTurn(session.sessionId);
1705
1913
  // A process restart cannot retain AbortController ownership. When the
1706
1914
  // durable blocker was already stopping and no execution owns it now,
@@ -1768,6 +1976,7 @@ export async function handleAbortChatTurn(req, res, deps, sessionId, turnId) {
1768
1976
  return;
1769
1977
  }
1770
1978
  deps.events.markAbortRequested?.(sessionId, turnId);
1979
+ await deps.questionRegistry?.cancelForSession(sessionId, "TURN_ABORTED").catch(() => undefined);
1771
1980
  if (!execution.controller.signal.aborted)
1772
1981
  execution.controller.abort();
1773
1982
  const waited = await (deps.turnExecutions?.waitForCompletion(sessionId, turnId, DEFAULT_ABORT_WAIT_MS) ?? Promise.resolve({ status: "missing" }));
@@ -1918,6 +2127,8 @@ export async function handleDeleteChatSession(req, res, deps, sessionId) {
1918
2127
  });
1919
2128
  return;
1920
2129
  }
2130
+ await deps.questionRegistry?.cancelForSession(sessionId, "SESSION_DELETED").catch(() => undefined);
2131
+ deps.todoRegistry?.clear(sessionId);
1921
2132
  deps.runtime.dispose(sessionId);
1922
2133
  deps.operationLinker.clearSession(sessionId);
1923
2134
  deps.events.clear(sessionId);
@@ -2592,6 +2803,36 @@ async function handleLinkWorkspaceOperation(req, res, deps, sessionId) {
2592
2803
  });
2593
2804
  }
2594
2805
  }
2806
+ async function handleUnlinkWorkspaceOperation(req, res, deps, sessionId, operationId) {
2807
+ const gate = gateMutation(req, deps);
2808
+ if (!gate.ok) {
2809
+ sendJson(res, gate.status, {
2810
+ ok: false,
2811
+ error: { code: gate.code, message: gate.message },
2812
+ });
2813
+ return;
2814
+ }
2815
+ if (!(await requireChatSession(res, deps, sessionId)))
2816
+ return;
2817
+ // AC-R7-001 (2026-08-23): removing a workspace-linked DAG association is
2818
+ // idempotent — a missing link is already-removed (200), never an error.
2819
+ try {
2820
+ const removed = await deps.operationLinker.unlink({
2821
+ sessionId,
2822
+ operationId,
2823
+ });
2824
+ sendJson(res, 200, { ok: true, removed });
2825
+ }
2826
+ catch (error) {
2827
+ sendJson(res, 400, {
2828
+ ok: false,
2829
+ error: {
2830
+ code: "INVALID_INPUT",
2831
+ message: error instanceof Error ? error.message : String(error),
2832
+ },
2833
+ });
2834
+ }
2835
+ }
2595
2836
  async function handleStartInterview(req, res, deps, sessionId) {
2596
2837
  const gate = gateMutation(req, deps);
2597
2838
  if (!gate.ok) {
@@ -3244,7 +3485,8 @@ export async function handleChatCompact(req, res, deps, sessionId) {
3244
3485
  return;
3245
3486
  }
3246
3487
  const rawInstructions = typeof body.instructions === "string" ? body.instructions : undefined;
3247
- const customInstructions = rawInstructions?.trim() || undefined;
3488
+ const currentTodos = projectCurrentTodos(deps.events.snapshot(sessionId));
3489
+ const customInstructions = buildTodoCompactionInstructions(rawInstructions, currentTodos);
3248
3490
  try {
3249
3491
  const snapshot = await deps.runtime.compact(sessionId, customInstructions);
3250
3492
  const event = deps.events.append(sessionId, deps.events.latestTurnId(sessionId) ?? `${sessionId}:compact`, { kind: "compact", data: snapshot });
@@ -3280,8 +3522,10 @@ export async function handleChatMessages(res, deps, sessionId, url) {
3280
3522
  /**
3281
3523
  * Dispatch a chat HTTP request. Returns true if handled.
3282
3524
  * Mounted under /api/operator/v1/chat/*.
3525
+ * workspaceScope (AC-R7-002): the explicit owning workspaceId for session
3526
+ * create, forwarded to handleCreateChatSession → store.create.
3283
3527
  */
3284
- export async function handleChatRequest(req, res, deps, pathname) {
3528
+ export async function handleChatRequest(req, res, deps, pathname, workspaceScope) {
3285
3529
  if (!deps)
3286
3530
  return false;
3287
3531
  if (!pathname.startsWith("/api/operator/v1/chat/") &&
@@ -3359,6 +3603,16 @@ export async function handleChatRequest(req, res, deps, pathname) {
3359
3603
  await handleStartInterview(req, res, deps, decodeURIComponent(interviewStart[1]));
3360
3604
  return true;
3361
3605
  }
3606
+ const userQuestionAnswer = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/questions\/([^/]+)\/answer$/);
3607
+ if (method === "POST" && userQuestionAnswer) {
3608
+ await handleAnswerUserQuestion(req, res, deps, decodeURIComponent(userQuestionAnswer[1]), decodeURIComponent(userQuestionAnswer[2]));
3609
+ return true;
3610
+ }
3611
+ const userQuestionCancel = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/questions\/([^/]+)\/cancel$/);
3612
+ if (method === "POST" && userQuestionCancel) {
3613
+ await handleCancelUserQuestion(req, res, deps, decodeURIComponent(userQuestionCancel[1]), decodeURIComponent(userQuestionCancel[2]));
3614
+ return true;
3615
+ }
3362
3616
  const interviewAnswer = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/interviews\/([^/]+)\/answers$/);
3363
3617
  if (method === "POST" && interviewAnswer) {
3364
3618
  await handleAnswerInterview(req, res, deps, decodeURIComponent(interviewAnswer[1]), decodeURIComponent(interviewAnswer[2]));
@@ -3465,6 +3719,11 @@ export async function handleChatRequest(req, res, deps, pathname) {
3465
3719
  await handleDeleteDeferredTurn(req, res, deps, decodeURIComponent(deferredTurn[1]));
3466
3720
  return true;
3467
3721
  }
3722
+ const deferredTurnSteer = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/deferred-turn\/([^/]+)\/steer$/);
3723
+ if (method === "POST" && deferredTurnSteer) {
3724
+ await handleSteerDeferredTurn(req, res, deps, decodeURIComponent(deferredTurnSteer[1]), decodeURIComponent(deferredTurnSteer[2]));
3725
+ return true;
3726
+ }
3468
3727
  const sessionTurnAbort = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/turns\/([^/]+)\/abort$/);
3469
3728
  if (method === "POST" && sessionTurnAbort) {
3470
3729
  await handleAbortChatTurn(req, res, deps, decodeURIComponent(sessionTurnAbort[1]), decodeURIComponent(sessionTurnAbort[2]));
@@ -3480,6 +3739,13 @@ export async function handleChatRequest(req, res, deps, pathname) {
3480
3739
  await handleLinkWorkspaceOperation(req, res, deps, decodeURIComponent(operationLink[1]));
3481
3740
  return true;
3482
3741
  }
3742
+ const operationLinkItem = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/operation-links\/([^/]+)$/);
3743
+ if (method === "DELETE" && operationLinkItem) {
3744
+ // AC-R7-001: Details rail DAG remove — real DELETE of the workspace
3745
+ // operation-link association (idempotent).
3746
+ await handleUnlinkWorkspaceOperation(req, res, deps, decodeURIComponent(operationLinkItem[1]), decodeURIComponent(operationLinkItem[2]));
3747
+ return true;
3748
+ }
3483
3749
  const sessionCompact = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/compact$/);
3484
3750
  if (method === "POST" && sessionCompact) {
3485
3751
  await handleChatCompact(req, res, deps, decodeURIComponent(sessionCompact[1]));
@@ -3527,7 +3793,7 @@ export async function handleChatRequest(req, res, deps, pathname) {
3527
3793
  }
3528
3794
  }
3529
3795
  if (method === "POST" && pathname === "/api/operator/v1/chat/sessions") {
3530
- await handleCreateChatSession(req, res, deps);
3796
+ await handleCreateChatSession(req, res, deps, workspaceScope);
3531
3797
  return true;
3532
3798
  }
3533
3799
  if (isMutationMethod(method)) {