@tea-agent/loop-agent 0.39.3 → 0.40.0-beta.1

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 (217) hide show
  1. package/CHANGELOG.md +133 -92
  2. package/dist/adapters/context-transfer/optional-pi-handoff.js +31 -0
  3. package/dist/adapters/context-transfer/pi-session.js +61 -0
  4. package/dist/build-stamp.json +3 -3
  5. package/dist/cli/command-definitions.js +12 -0
  6. package/dist/cli/program.js +23 -1
  7. package/dist/commands/dag-artifact.js +284 -0
  8. package/dist/commands/dag-context.js +184 -0
  9. package/dist/commands/dag-rerun.js +206 -1
  10. package/dist/executors/dag-pi-executor.js +108 -19
  11. package/dist/executors/pi-executor.js +4 -2
  12. package/dist/executors/pi-sdk-executor.js +49 -1
  13. package/dist/executors/pi-writer-tool-policy.js +57 -1
  14. package/dist/executors/shell-executor.js +42 -2
  15. package/dist/executors/workspace-write-snapshot.js +68 -0
  16. package/dist/{worker → infrastructure}/console/app-data.js +4 -0
  17. package/dist/infrastructure/console/artifact-revision-store.js +430 -0
  18. package/dist/infrastructure/console/context-export-store.js +160 -0
  19. package/dist/infrastructure/console/dir-lock.js +132 -0
  20. package/dist/infrastructure/harness/artifact-store.js +10 -1
  21. package/dist/infrastructure/harness/atomic-write.js +35 -2
  22. package/dist/shared/context-transfer/artifact-revision.js +172 -0
  23. package/dist/shared/context-transfer.js +418 -0
  24. package/dist/shared/operator/capabilities.js +14 -0
  25. package/dist/shared/operator/safe-run-summary.js +1 -0
  26. package/dist/shared/path-safety.js +93 -0
  27. package/dist/shared/preview.js +28 -4
  28. package/dist/task/config-types.js +10 -0
  29. package/dist/task/source-prepare/fragment-inventory.js +1 -14
  30. package/dist/worker/console/chat/chat-event-store.js +77 -7
  31. package/dist/worker/console/chat/context-panel.js +7 -0
  32. package/dist/worker/console/chat/deferred-turn.js +12 -0
  33. package/dist/worker/console/chat/pi-runtime.js +331 -18
  34. package/dist/worker/console/chat/provider-error.js +98 -0
  35. package/dist/worker/console/chat/resource-preferences-store.js +1 -1
  36. package/dist/worker/console/chat/routes.js +389 -56
  37. package/dist/worker/console/chat/session-stats.js +179 -0
  38. package/dist/worker/console/chat/session-store.js +94 -64
  39. package/dist/worker/console/chat/todos.js +115 -0
  40. package/dist/worker/console/chat/tool-preview.js +42 -0
  41. package/dist/worker/console/chat/turn-process.js +20 -55
  42. package/dist/worker/console/chat/user-questions.js +266 -0
  43. package/dist/worker/console/chat/workspace-landing.js +79 -0
  44. package/dist/worker/console/console-update-runtime.js +1 -1
  45. package/dist/worker/console/context-transfer-diagnostics.js +198 -0
  46. package/dist/worker/console/dag-confirmation.js +1 -1
  47. package/dist/worker/console/dag-execution-receipt.js +1 -1
  48. package/dist/worker/console/doctor.js +1 -1
  49. package/dist/worker/console/draft-store.js +1 -1
  50. package/dist/worker/console/human-gate-token.js +1 -1
  51. package/dist/worker/console/index.js +3 -3
  52. package/dist/worker/console/interview/assessment.js +1 -1
  53. package/dist/worker/console/interview/session.js +1 -1
  54. package/dist/worker/console/native-directory-picker.js +13 -0
  55. package/dist/worker/console/operation-runner.js +45 -4
  56. package/dist/worker/console/operation-sse.js +1 -1
  57. package/dist/worker/console/operation-wait.js +1 -1
  58. package/dist/worker/console/operator-actions.js +104 -3
  59. package/dist/worker/console/operator-surface-health.js +1 -1
  60. package/dist/worker/console/operator-user-error.js +128 -0
  61. package/dist/worker/console/routes.js +927 -8
  62. package/dist/worker/console/security.js +39 -4
  63. package/dist/worker/console/server.js +532 -187
  64. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-CTx1TrNa.js → abnfDiagram-N423BO3Z-CpWvwARc.js} +1 -1
  65. package/dist/worker/console/static/assets/{arc-B4i9cVxX.js → arc-DctXgO-g.js} +1 -1
  66. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BAXE8_Rl.js → architectureDiagram-T3A2C74G-D4mIkoQl.js} +1 -1
  67. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-C5Xh59at.js → blockDiagram-VBNYF7ZC-ClG0--KO.js} +1 -1
  68. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B_asOzAu.js → c4Diagram-5PPSVZJV-CwdLk78T.js} +1 -1
  69. package/dist/worker/console/static/assets/channel-C9VQZ-ru.js +1 -0
  70. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-wRO_iRNc.js → chunk-2GRJ4B5K-DKczcu-O.js} +1 -1
  71. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-ByOaSh_G.js → chunk-2Q5K7J3B-BV6F-vBI.js} +1 -1
  72. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLzudan.js → chunk-5RXB4S5H-DtojVnvA.js} +1 -1
  73. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-JCNGkXYT.js → chunk-5VM5RSS4-BjDQRGnO.js} +1 -1
  74. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BhrPXXMZ.js → chunk-6Q2QTUOP-I0fV6ngU.js} +1 -1
  75. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-TJ5JuEBT.js → chunk-GF5L2VYU-BM8C-mmC.js} +1 -1
  76. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-VVthn_mD.js → chunk-JWPE2WC7-BEK-a15e.js} +1 -1
  77. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-D60-Mny4.js → chunk-KBJHAD2P-CQsg_fSR.js} +1 -1
  78. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-zXv7aVye.js → chunk-RYQCIY6F-B-BcaUm3.js} +1 -1
  79. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-By9VQoi1.js → chunk-XXDRQBXY-D1Bu08fg.js} +1 -1
  80. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BsCkw5MF.js +1 -0
  81. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BsCkw5MF.js +1 -0
  82. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-C4zQLe-M.js → cose-bilkent-JH36ORCC-q8nwtxBR.js} +1 -1
  83. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-wh5N2ReN.js → cynefin-VYW2F7L2-CruhpURw.js} +1 -1
  84. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Dlu5wIqs.js → cynefinDiagram-MW4NZA55-CJne6iYF.js} +1 -1
  85. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CrDgthc6.js → dagre-VZM6K2ZE-CbeLvBU5.js} +1 -1
  86. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-zN1clg27.js → diagram-7IWD3JNH-DmCxH1Pe.js} +1 -1
  87. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BN-o7J7-.js → diagram-B4RE2ZJO-DJ5OYhtC.js} +1 -1
  88. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-C8SuPt0d.js → diagram-LBJQPF4R-B-UN-hln.js} +1 -1
  89. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BoizuHbZ.js → diagram-Q27KOJAE-CcEro8Ey.js} +1 -1
  90. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BzUzXixG.js → diagram-UB23O5K3-BBBzDden.js} +1 -1
  91. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Ev3lA4Cf.js → ebnfDiagram-BXEA7PRR-xKEyH-oo.js} +1 -1
  92. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-GALUzRbs.js → erDiagram-JOGREHBK-C7xl63VC.js} +1 -1
  93. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CZJN65Sb.js → flowDiagram-UKHOOZJN-nthQQ9nR.js} +1 -1
  94. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-JLB9OESl.js → ganttDiagram-PKOTCBZU-CWN5mE33.js} +1 -1
  95. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CcvCr4GD.js → gitGraphDiagram-DS77QQ5N-Wn86tQ6N.js} +1 -1
  96. package/dist/worker/console/static/assets/index-D-0NVyMk.css +1 -0
  97. package/dist/worker/console/static/assets/index-gwyOxQiS.js +407 -0
  98. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-HG5z7aVg.js → infoDiagram-6WML65LV-_WQymGtW.js} +1 -1
  99. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BHEu9chR.js → ishikawaDiagram-WSZJBQD7-COBeivBm.js} +1 -1
  100. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-Cp_Up8-a.js → journeyDiagram-NVQOT4AX-CwGOJl3Q.js} +1 -1
  101. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-DvVeY-em.js → kanban-definition-27J2QSJJ-Ci0CJ5Vi.js} +1 -1
  102. package/dist/worker/console/static/assets/{linear-aGmcjgTn.js → linear-CPjMq6vN.js} +1 -1
  103. package/dist/worker/console/static/assets/{mermaid.core-Biyo-tqA.js → mermaid.core-Z0wGoe0b.js} +5 -5
  104. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DyxY2pIb.js → mindmap-definition-FAOFIHXS-dISked0d.js} +1 -1
  105. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CkdGuc50.js → pegDiagram-VL7TDLO6-BJxgof7Y.js} +1 -1
  106. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-NNQI9xVR.js → pieDiagram-7S7Q4E2Y-CIblU8eW.js} +1 -1
  107. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-PAJkuz_G.js → quadrantDiagram-CIZ2JOQS-vwYiqh8E.js} +1 -1
  108. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-ZwrQod0O.js → railroadDiagram-AXF67PYL-_iOsVeFm.js} +1 -1
  109. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D3ZfrzM2.js → requirementDiagram-LRYGKXZP-Dk0MLxO_.js} +1 -1
  110. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-DDGFmWk5.js → sankeyDiagram-W5VNT64P-Dx7HtLVW.js} +1 -1
  111. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DO6mmXTA.js → sequenceDiagram-SI44F4Z6-DRuWjlpL.js} +1 -1
  112. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DMbCQiJt.js → sizeCapture-X5ZJPWSS-Bhy9DQAA.js} +1 -1
  113. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Di_YUEfC.js → stateDiagram-OKZ733FA-DAx9twnU.js} +1 -1
  114. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Dmdf8cGy.js +1 -0
  115. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-Bn2PphNJ.js → swimlanes-SLNWSIFB-okMlvVrM.js} +2 -2
  116. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-W1IcAFff.js +8 -0
  117. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DPh8pRsp.js → timeline-definition-Z64GVDOM-DXqCKe37.js} +1 -1
  118. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-GMfl26pW.js → vennDiagram-T6HMQDX7-CeIUkwBh.js} +1 -1
  119. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DTlmVrmF.js → wardleyDiagram-T6FBY63Y-BP76BKTN.js} +1 -1
  120. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-YGBjKAS5.js → xychartDiagram-ELKLHX3M-BpVI0OpF.js} +1 -1
  121. package/dist/worker/console/static/index.html +7 -2
  122. package/dist/worker/console/static-src/app/console-types.js +29 -4
  123. package/dist/worker/console/static-src/app/useConsoleShell.js +30 -12
  124. package/dist/worker/console/static-src/app/useOperatorActions.js +36 -6
  125. package/dist/worker/console/static-src/app/useRecoveryConsole.js +10 -11
  126. package/dist/worker/console/static-src/app/useRunProgress.js +2 -1
  127. package/dist/worker/console/static-src/app/useTaskWizard.js +2 -2
  128. package/dist/worker/console/static-src/night/useNightBoard.js +8 -5
  129. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +141 -8
  130. package/dist/worker/console/static-src/operator-chat/compaction-message.js +79 -0
  131. package/dist/worker/console/static-src/operator-chat/details-dag-actions.js +122 -0
  132. package/dist/worker/console/static-src/operator-chat/refs.js +3 -0
  133. package/dist/worker/console/static-src/operator-chat/runtime-selection-labels.js +27 -0
  134. package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +1 -2
  135. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +25 -0
  136. package/dist/worker/console/static-src/operator-chat/turn-submission.js +3 -2
  137. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +181 -87
  138. package/dist/worker/console/static-src/operator-chat/useChatStream.js +159 -44
  139. package/dist/worker/console/static-src/operator-chat/useChatThread.js +177 -8
  140. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +63 -9
  141. package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
  142. package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -3
  143. package/dist/worker/console/static-src/shell/useWorkspaces.js +299 -0
  144. package/dist/worker/console/static-src/shell/workspace-route.js +339 -0
  145. package/dist/worker/console/workspace-context.js +260 -0
  146. package/dist/worker/console/workspace-registry.js +214 -0
  147. package/dist/worker/continuation/worker-continuation.js +278 -0
  148. package/dist/worker/observe/health.js +1 -1
  149. package/dist/worker/observe/node-transparency.js +572 -0
  150. package/dist/worker/observe/routes.js +51 -1
  151. package/dist/worker/observe/server.js +4 -0
  152. package/dist/worker/observe/static/api.js +94 -10
  153. package/dist/worker/observe/static/app.js +18 -70
  154. package/dist/worker/observe/static/dag-context-reason-labels.d.ts +9 -0
  155. package/dist/worker/observe/static/dag-context-reason-labels.js +120 -0
  156. package/dist/worker/observe/static/index.html +1 -1
  157. package/dist/worker/observe/static/inspect-workspace.js +307 -0
  158. package/dist/worker/observe/static/operator-chrome.css +145 -20
  159. package/dist/worker/observe/static/operator-chrome.d.ts +20 -3
  160. package/dist/worker/observe/static/operator-chrome.js +330 -100
  161. package/dist/worker/observe/static/router.d.ts +46 -0
  162. package/dist/worker/observe/static/router.js +25 -1
  163. package/dist/worker/observe/static/state.d.ts +50 -0
  164. package/dist/worker/observe/static/state.js +175 -1
  165. package/dist/worker/observe/static/styles.css +1039 -128
  166. package/dist/worker/observe/static/views/dag-inspector.js +1218 -60
  167. package/dist/worker/observe/static/views/dag.js +2 -1
  168. package/dist/worker/observe/static/views/dashboard.js +6 -4
  169. package/dist/worker/observe/static/views/night.js +2 -2
  170. package/dist/worker/observe/static/views/pool.js +1 -0
  171. package/dist/worker/observe/static/views/run.js +3 -1
  172. package/dist/worker/observe/static/views/session-timeline.js +75 -10
  173. package/dist/worker/observe/static/views/task.js +1 -1
  174. package/dist/worker/pool/attempt-lease.js +97 -66
  175. package/dist/worker/pool/begin-attempt-with-lease.js +1 -0
  176. package/dist/worker/pool/reconcile.js +46 -1
  177. package/dist/worker/pool/run-store.js +2 -0
  178. package/dist/worker/pool/state-projection.js +2 -0
  179. package/dist/workflows/dag/artifact-bindings.js +149 -0
  180. package/dist/workflows/dag/artifact-revision-schema-registry.js +31 -0
  181. package/dist/workflows/dag/backend-test-pytest-collection.js +11 -10
  182. package/dist/workflows/dag/backend-test-writer-completeness.js +7 -12
  183. package/dist/workflows/dag/context-receipt.js +305 -0
  184. package/dist/workflows/dag/context-transfer/context-bundle.js +423 -0
  185. package/dist/workflows/dag/context-transfer/operator-actions.js +61 -0
  186. package/dist/workflows/dag/context-transfer/renderers.js +93 -0
  187. package/dist/workflows/dag/dynamic-runtime/map.js +1 -0
  188. package/dist/workflows/dag/frontend-implementation-contract.js +1 -89
  189. package/dist/workflows/dag/init-hybrid.js +178 -47
  190. package/dist/workflows/dag/interrupt-request.js +3 -3
  191. package/dist/workflows/dag/lifecycle.js +3 -3
  192. package/dist/workflows/dag/node-execution.js +112 -27
  193. package/dist/workflows/dag/path-safety.js +1 -0
  194. package/dist/workflows/dag/prompt.js +29 -4
  195. package/dist/workflows/dag/rerun-plan.js +272 -0
  196. package/dist/workflows/dag/rerun-run.js +390 -12
  197. package/dist/workflows/dag/skill-snapshot.js +1 -0
  198. package/dist/workflows/dag/types.js +203 -8
  199. package/docs/README.md +1 -0
  200. package/docs/architecture/runtime-boundaries.md +6 -6
  201. package/docs/operations/local-development-environment.md +21 -0
  202. package/docs/templates/agent-dag.schema.json +50 -0
  203. package/docs/templates/backend-test-dag.json +265 -237
  204. package/harness.json +3 -3
  205. package/package.json +5 -1
  206. package/skills/loop-agent/references/command-reference.md +6 -0
  207. package/dist/worker/console/static/assets/channel-Dj_4Moei.js +0 -1
  208. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C0YYAyCa.js +0 -1
  209. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C0YYAyCa.js +0 -1
  210. package/dist/worker/console/static/assets/index-CW88Fjbw.js +0 -406
  211. package/dist/worker/console/static/assets/index-DP0ZJl39.css +0 -1
  212. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DrdDvela.js +0 -1
  213. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DPkEAULl.js +0 -8
  214. package/dist/worker/console/static-src/operator-chat/activity-rail-presentation.js +0 -73
  215. package/dist/worker/console/static-src/operator-chat/useActivityRailTransition.js +0 -59
  216. /package/dist/{worker → infrastructure}/console/operation-store.js +0 -0
  217. /package/dist/{worker → infrastructure}/console/repo-fingerprint.js +0 -0
@@ -1,12 +1,22 @@
1
+ import { createHash, randomBytes } from "node:crypto";
1
2
  import { existsSync } from "node:fs";
2
- import { readFile } from "node:fs/promises";
3
+ import { readFile, rm } from "node:fs/promises";
3
4
  import path from "node:path";
4
5
  import { dispatchOperatorAction } from "./operator-actions.js";
5
6
  import { openSseResponse, parseLastEventId, writeSseEvent, } from "./operation-sse.js";
7
+ import { probePiReadiness } from "./pi-readiness.js";
6
8
  import { buildOperatorSurfaceHealthV1 } from "./operator-surface-health.js";
7
9
  import { ROUTES } from "../observe/routes.js";
8
- import { evaluateMutationGate, injectConfirmationTokenScript, isMutationMethod, } from "./security.js";
10
+ import { evaluateProtectedDownloadGate, evaluateMutationGate, injectConfirmationTokenScript, isMutationMethod, } from "./security.js";
9
11
  import { listDagRunsForRecovery } from "./resolve-dag-run-for-task.js";
12
+ import { writeArtifactRevisionUpload } from "../../infrastructure/console/artifact-revision-store.js";
13
+ import { ensureSecureDir } from "../../infrastructure/console/app-data.js";
14
+ import { acquireContextExportLease, createContextExport, releaseContextExportLease, } from "../../infrastructure/console/context-export-store.js";
15
+ import { acceptOperation } from "./operator-actions.js";
16
+ import { recordContextTransferDiagnostic, recordContextTransferEvent, } from "./context-transfer-diagnostics.js";
17
+ import { isDagRerunPromptOverrideEnabled } from "../../shared/dag-prompt-override.js";
18
+ import { beginWorkerContinuation, resolveWorkerContinuationAuthority, settleWorkerContinuation, } from "../continuation/worker-continuation.js";
19
+ import { pickNativeWorkspaceDirectory } from "./native-directory-picker.js";
10
20
  export function sendJson(res, status, body, extraHeaders) {
11
21
  const payload = JSON.stringify(body);
12
22
  res.writeHead(status, {
@@ -38,6 +48,10 @@ function contentTypeFor(pathname) {
38
48
  return "application/octet-stream";
39
49
  }
40
50
  async function readJsonBody(req, maxBytes = 10 * 1024 * 1024) {
51
+ // Body pre-read by the multi-workspace dispatcher (workspaceId resolution).
52
+ const preRead = req.__preReadBody;
53
+ if (preRead !== undefined)
54
+ return preRead;
41
55
  const chunks = [];
42
56
  let total = 0;
43
57
  for await (const chunk of req) {
@@ -142,7 +156,516 @@ async function handleSessionPing(req, res, ctx) {
142
156
  export const INSPECT_MUTATION_ALLOWED_ACTIONS = new Set([
143
157
  "dagRerunPlan",
144
158
  "dagRerun",
159
+ "contextExport",
160
+ "artifactRevisionStage",
161
+ "artifactRevisionPlan",
162
+ "artifactRevisionExecute",
145
163
  ]);
164
+ function evaluateConsoleCapability(req, ctx) {
165
+ const common = {
166
+ consoleOrigin: ctx.getConsoleOrigin(),
167
+ allowNonLoopbackAccess: ctx.allowNonLoopbackAccess === true,
168
+ };
169
+ const general = evaluateMutationGate(req, {
170
+ ...common,
171
+ cookieName: ctx.bootToken.cookieName,
172
+ bootToken: ctx.bootToken.value,
173
+ confirmationToken: ctx.bootToken.confirmationToken,
174
+ });
175
+ if (!general || !ctx.inspectBootToken)
176
+ return general;
177
+ return evaluateMutationGate(req, {
178
+ ...common,
179
+ cookieName: ctx.inspectBootToken.cookieName,
180
+ bootToken: ctx.inspectBootToken.value,
181
+ confirmationToken: ctx.inspectBootToken.confirmationToken,
182
+ });
183
+ }
184
+ function evaluateConsoleDownloadCapability(req, ctx) {
185
+ const common = {
186
+ consoleOrigin: ctx.getConsoleOrigin(),
187
+ allowNonLoopbackAccess: ctx.allowNonLoopbackAccess === true,
188
+ };
189
+ const general = evaluateProtectedDownloadGate(req, {
190
+ ...common,
191
+ cookieName: ctx.bootToken.cookieName,
192
+ bootToken: ctx.bootToken.value,
193
+ confirmationToken: ctx.bootToken.confirmationToken,
194
+ });
195
+ if (!general || !ctx.inspectBootToken)
196
+ return general;
197
+ return evaluateProtectedDownloadGate(req, {
198
+ ...common,
199
+ cookieName: ctx.inspectBootToken.cookieName,
200
+ bootToken: ctx.inspectBootToken.value,
201
+ confirmationToken: ctx.inspectBootToken.confirmationToken,
202
+ });
203
+ }
204
+ async function acceptContextExportOperation(input) {
205
+ const operator = input.ctx.operator;
206
+ const clientRequestId = input.clientRequestId?.trim();
207
+ if (!clientRequestId) {
208
+ return {
209
+ status: 400,
210
+ body: { ok: false, error: { code: "INVALID_INPUT", message: "clientRequestId is required" } },
211
+ };
212
+ }
213
+ const runId = typeof input.actionParams.runId === "string" ? input.actionParams.runId.trim() : "";
214
+ const nodeId = typeof input.actionParams.nodeId === "string" ? input.actionParams.nodeId.trim() : "";
215
+ const attempt = Number(input.actionParams.attempt ?? 1);
216
+ const profile = input.actionParams.profile === "raw" ? "raw" : "redacted";
217
+ const target = ["generic", "pi-session", "pi-handoff", "readable"].includes(String(input.actionParams.target))
218
+ ? input.actionParams.target
219
+ : "generic";
220
+ if (!runId || !nodeId || !Number.isInteger(attempt) || attempt < 1) {
221
+ return {
222
+ status: 400,
223
+ body: { ok: false, error: { code: "INVALID_INPUT", message: "runId, nodeId and positive attempt are required" } },
224
+ };
225
+ }
226
+ if (profile === "raw" && input.actionParams.rawAcknowledged !== true) {
227
+ return {
228
+ status: 400,
229
+ body: { ok: false, error: { code: "RAW_EXPORT_ACKNOWLEDGEMENT_REQUIRED", message: "raw export requires explicit acknowledgement" } },
230
+ };
231
+ }
232
+ const created = await operator.operations.create({
233
+ clientRequestId,
234
+ action: "contextExport",
235
+ actionParams: { runId, nodeId, attempt, profile, target, rawAcknowledged: profile === "raw" },
236
+ });
237
+ if (created.kind === "conflict") {
238
+ return {
239
+ status: 409,
240
+ body: { ok: false, error: { code: created.code, message: created.message } },
241
+ };
242
+ }
243
+ if (created.kind === "created") {
244
+ void recordContextTransferEvent({
245
+ appData: operator.actionContext.appData,
246
+ event: "context_export.planned",
247
+ fields: {
248
+ runId,
249
+ nodeId,
250
+ profile,
251
+ target,
252
+ operationId: created.operation.operationId,
253
+ },
254
+ }).catch(() => undefined);
255
+ queueMicrotask(() => {
256
+ void (async () => {
257
+ const startedAt = Date.now();
258
+ await operator.operations.update(created.operation.operationId, {
259
+ state: "running",
260
+ startedAt: new Date().toISOString(),
261
+ });
262
+ try {
263
+ const appData = operator.actionContext.appData;
264
+ if (!appData.contextExports)
265
+ throw new Error("console context export root unavailable");
266
+ ensureSecureDir(appData.contextExports);
267
+ const incomingPath = path.join(appData.contextExports, `${randomBytes(16).toString("hex")}.incoming.zip`);
268
+ await rm(incomingPath, { force: true });
269
+ try {
270
+ const kernel = await runKernelCommand({
271
+ ctx: input.ctx,
272
+ args: [
273
+ "dag", "context", "export",
274
+ "--run-id", runId,
275
+ "--node-id", nodeId,
276
+ "--attempt", String(attempt),
277
+ "--target", target,
278
+ "--redaction", profile,
279
+ ...(profile === "raw" ? ["--acknowledge-sensitive-data"] : []),
280
+ "--output", incomingPath,
281
+ "--json",
282
+ ],
283
+ });
284
+ const payload = (kernel.json ?? {});
285
+ if (!kernel.ok || !payload.bundleId || !payload.bundleSha256) {
286
+ throw new Error(kernelFailureMessage(kernel));
287
+ }
288
+ const zipBytes = await readFile(incomingPath);
289
+ const exported = await createContextExport({
290
+ appData,
291
+ bundle: {
292
+ zipBytes,
293
+ manifest: {
294
+ bundleId: payload.bundleId,
295
+ bundleSha256: payload.bundleSha256,
296
+ profile,
297
+ target,
298
+ },
299
+ },
300
+ });
301
+ await recordContextTransferDiagnostic({
302
+ appData: operator.actionContext.appData,
303
+ event: "context-bundle.generate",
304
+ dimensions: {
305
+ outcome: "succeeded",
306
+ target,
307
+ profile,
308
+ bytes: exported.bytes,
309
+ durationMs: Date.now() - startedAt,
310
+ },
311
+ }).catch(() => undefined);
312
+ await recordContextTransferEvent({
313
+ appData: operator.actionContext.appData,
314
+ event: "context_export.ready",
315
+ fields: {
316
+ runId,
317
+ nodeId,
318
+ exportId: exported.exportId,
319
+ profile,
320
+ target,
321
+ bytes: exported.bytes,
322
+ durationMs: Date.now() - startedAt,
323
+ operationId: created.operation.operationId,
324
+ },
325
+ }).catch(() => undefined);
326
+ await operator.operations.update(created.operation.operationId, {
327
+ state: "succeeded",
328
+ finishedAt: new Date().toISOString(),
329
+ result: {
330
+ ok: true,
331
+ exportId: exported.exportId,
332
+ bundleId: exported.bundleId,
333
+ bundleSha256: exported.bundleSha256,
334
+ bytes: exported.bytes,
335
+ expiresAt: exported.expiresAt,
336
+ downloadUrl: `/api/operator/v1/context-exports/${exported.exportId}/download`,
337
+ },
338
+ });
339
+ }
340
+ finally {
341
+ await rm(incomingPath, { force: true }).catch(() => undefined);
342
+ }
343
+ }
344
+ catch (error) {
345
+ await recordContextTransferDiagnostic({
346
+ appData: operator.actionContext.appData,
347
+ event: "context-bundle.generate",
348
+ dimensions: {
349
+ outcome: "failed",
350
+ target,
351
+ profile,
352
+ reasonCode: "CONTEXT_EXPORT_FAILED",
353
+ durationMs: Date.now() - startedAt,
354
+ },
355
+ }).catch(() => undefined);
356
+ await recordContextTransferEvent({
357
+ appData: operator.actionContext.appData,
358
+ event: "context_export.failed",
359
+ fields: {
360
+ runId,
361
+ nodeId,
362
+ profile,
363
+ target,
364
+ reasonCode: "CONTEXT_EXPORT_FAILED",
365
+ durationMs: Date.now() - startedAt,
366
+ operationId: created.operation.operationId,
367
+ },
368
+ }).catch(() => undefined);
369
+ await operator.operations.update(created.operation.operationId, {
370
+ state: "failed",
371
+ finishedAt: new Date().toISOString(),
372
+ errorCode: "CONTEXT_EXPORT_FAILED",
373
+ errorMessage: error instanceof Error ? error.message : String(error),
374
+ });
375
+ }
376
+ })();
377
+ });
378
+ }
379
+ return {
380
+ status: 202,
381
+ body: {
382
+ operationId: created.operation.operationId,
383
+ state: created.operation.state,
384
+ clientRequestId: created.operation.clientRequestId,
385
+ action: created.operation.action,
386
+ },
387
+ };
388
+ }
389
+ async function runKernelCommand(input) {
390
+ const actionContext = input.ctx.operator.actionContext;
391
+ const run = actionContext.runCommand ??
392
+ ((args, options) => actionContext.client.run(args, options));
393
+ return (await run(input.args, {
394
+ cwd: actionContext.repoRoot,
395
+ artifactName: `kernel-${input.args.filter((arg) => !arg.startsWith("--")).slice(0, 3).join("-").replace(/\s+/g, "-")}`,
396
+ expectJson: true,
397
+ env: { ...process.env, LOOP_CONSOLE_APP_DATA: actionContext.appData.root },
398
+ }));
399
+ }
400
+ function kernelFailureMessage(result) {
401
+ const stderr = result.stderr.trim();
402
+ if (stderr) {
403
+ const lastLine = stderr.split("\n").filter(Boolean).pop() ?? stderr;
404
+ return lastLine.slice(0, 300);
405
+ }
406
+ const stdout = result.stdout.trim();
407
+ if (stdout)
408
+ return stdout.slice(0, 300);
409
+ return `kernel command failed with exit code ${result.exitCode}`;
410
+ }
411
+ function decodeBoundedBase64(value) {
412
+ if (typeof value !== "string" || value.length === 0 || value.length > 14_000_000) {
413
+ throw new Error("candidateBase64 is required and exceeds no more than 10 MiB decoded");
414
+ }
415
+ if (!/^[A-Za-z0-9+/]*={0,2}$/.test(value) || value.length % 4 !== 0) {
416
+ throw new Error("candidateBase64 is invalid");
417
+ }
418
+ const bytes = Buffer.from(value, "base64");
419
+ if (bytes.byteLength > 10 * 1024 * 1024)
420
+ throw new Error("candidate exceeds 10 MiB");
421
+ return bytes;
422
+ }
423
+ function revisionProjection(revision) {
424
+ if (!revision)
425
+ return undefined;
426
+ return {
427
+ revisionId: revision.revisionId,
428
+ state: revision.state,
429
+ parent: revision.parent,
430
+ candidate: {
431
+ sha256: revision.candidate.sha256,
432
+ bytes: revision.candidate.bytes,
433
+ mediaType: revision.candidate.mediaType,
434
+ uploadedAt: revision.candidate.uploadedAt,
435
+ },
436
+ validation: revision.validation,
437
+ diff: {
438
+ kind: revision.diff.kind,
439
+ summary: revision.diff.summary,
440
+ truncated: revision.diff.truncated,
441
+ },
442
+ expiresAt: revision.expiresAt,
443
+ };
444
+ }
445
+ async function acceptArtifactRevisionOperation(input) {
446
+ const operator = input.ctx.operator;
447
+ const clientRequestId = input.clientRequestId?.trim();
448
+ if (!clientRequestId) {
449
+ return { status: 400, body: { ok: false, error: { code: "INVALID_INPUT", message: "clientRequestId is required" } } };
450
+ }
451
+ let candidateBytes;
452
+ try {
453
+ if (input.action === "artifactRevisionStage") {
454
+ candidateBytes = decodeBoundedBase64(input.actionParams.candidateBase64);
455
+ }
456
+ }
457
+ catch (error) {
458
+ return { status: 400, body: { ok: false, error: { code: "INVALID_INPUT", message: error instanceof Error ? error.message : String(error) } } };
459
+ }
460
+ const runId = typeof input.actionParams.runId === "string" ? input.actionParams.runId.trim() : "";
461
+ const nodeId = typeof input.actionParams.nodeId === "string" ? input.actionParams.nodeId.trim() : "";
462
+ const artifactId = typeof input.actionParams.artifactId === "string" ? input.actionParams.artifactId.trim() : "";
463
+ const revisionId = typeof input.actionParams.revisionId === "string" ? input.actionParams.revisionId.trim() : "";
464
+ const planHash = typeof input.actionParams.planHash === "string" ? input.actionParams.planHash.trim() : "";
465
+ const reason = typeof input.actionParams.reason === "string" ? input.actionParams.reason.trim() : "";
466
+ const mediaType = typeof input.actionParams.mediaType === "string" ? input.actionParams.mediaType.trim() : "";
467
+ if (!runId || !artifactId || (input.action === "artifactRevisionStage" && (!nodeId || !mediaType))) {
468
+ return { status: 400, body: { ok: false, error: { code: "INVALID_INPUT", message: "runId, artifactId, nodeId and mediaType are required" } } };
469
+ }
470
+ if (input.action !== "artifactRevisionStage" && !revisionId) {
471
+ return { status: 400, body: { ok: false, error: { code: "INVALID_INPUT", message: "revisionId is required" } } };
472
+ }
473
+ if (input.action === "artifactRevisionExecute" && (!planHash || !reason)) {
474
+ return { status: 400, body: { ok: false, error: { code: "INVALID_INPUT", message: "planHash and reason are required" } } };
475
+ }
476
+ let workerContinuation;
477
+ if (input.action !== "artifactRevisionStage") {
478
+ try {
479
+ workerContinuation = await resolveWorkerContinuationAuthority({
480
+ repoRoot: input.ctx.repoRoot,
481
+ parentDagRunId: runId,
482
+ revisionId,
483
+ allowActiveRevision: input.action === "artifactRevisionExecute",
484
+ });
485
+ }
486
+ catch (error) {
487
+ const message = error instanceof Error ? error.message : String(error);
488
+ return {
489
+ status: 422,
490
+ body: {
491
+ ok: false,
492
+ error: {
493
+ code: "WORKER_CONTINUATION_AUTHORITY_MISMATCH",
494
+ message,
495
+ },
496
+ },
497
+ };
498
+ }
499
+ }
500
+ const safeActionParams = {
501
+ runId,
502
+ artifactId,
503
+ ...(nodeId ? { nodeId } : {}),
504
+ ...(revisionId ? { revisionId } : {}),
505
+ ...(planHash ? { planHash } : {}),
506
+ ...(reason ? { reason } : {}),
507
+ ...(mediaType ? { mediaType } : {}),
508
+ ...(candidateBytes
509
+ ? { candidateSha256: createHash("sha256").update(candidateBytes).digest("hex"), candidateBytes: candidateBytes.byteLength }
510
+ : {}),
511
+ ...(workerContinuation
512
+ ? { workerContinuation: workerContinuation.authority }
513
+ : {}),
514
+ };
515
+ const appData = operator.actionContext.appData;
516
+ if (!appData.artifactRevisions) {
517
+ return { status: 503, body: { ok: false, error: { code: "INTERNAL_ERROR", message: "console artifact revision root unavailable" } } };
518
+ }
519
+ let cliArgs;
520
+ if (input.action === "artifactRevisionStage") {
521
+ const candidateSha256 = safeActionParams.candidateSha256;
522
+ cliArgs = [
523
+ "dag", "artifact", "revision", "stage",
524
+ "--run-id", runId,
525
+ "--node-id", nodeId,
526
+ "--artifact-id", artifactId,
527
+ "--candidate-upload", candidateSha256,
528
+ "--request-id", `console-${clientRequestId}`,
529
+ "--json",
530
+ ];
531
+ }
532
+ else if (input.action === "artifactRevisionPlan") {
533
+ cliArgs = [
534
+ "dag", "rerun",
535
+ "--run-id", runId,
536
+ "--artifact-revision", revisionId,
537
+ "--plan", "--json",
538
+ ];
539
+ }
540
+ else {
541
+ cliArgs = [
542
+ "dag", "rerun",
543
+ "--run-id", runId,
544
+ "--artifact-revision", revisionId,
545
+ "--plan-hash", planHash,
546
+ "--request-id", `console-${clientRequestId}`,
547
+ "--reason", reason,
548
+ "--json",
549
+ ];
550
+ }
551
+ if (workerContinuation) {
552
+ cliArgs.push("--worker-continuation", JSON.stringify(workerContinuation.authority));
553
+ }
554
+ let admittedOperationId;
555
+ const accepted = await acceptOperation(operator.actionContext, {
556
+ action: input.action,
557
+ actionParams: safeActionParams,
558
+ clientRequestId,
559
+ cliArgs,
560
+ ...(input.action === "artifactRevisionStage" ||
561
+ (input.action === "artifactRevisionExecute" && workerContinuation)
562
+ ? {
563
+ prepare: async () => {
564
+ if (input.action === "artifactRevisionStage") {
565
+ await writeArtifactRevisionUpload({
566
+ appData,
567
+ candidateSha256: safeActionParams.candidateSha256,
568
+ candidateBytes: candidateBytes,
569
+ });
570
+ }
571
+ else if (workerContinuation && admittedOperationId) {
572
+ await operator.actionContext.operations.update(admittedOperationId, { dagRunId: workerContinuation.authority.childWorkerRunId });
573
+ await beginWorkerContinuation({
574
+ repoRoot: input.ctx.repoRoot,
575
+ resolved: workerContinuation,
576
+ operationId: admittedOperationId,
577
+ client: operator.actionContext.client,
578
+ });
579
+ }
580
+ else {
581
+ throw new Error("Worker continuation operation identity is unavailable");
582
+ }
583
+ },
584
+ prepareFailure: (error) => {
585
+ const message = error instanceof Error ? error.message : String(error);
586
+ if (input.action === "artifactRevisionExecute") {
587
+ const conflict = message.includes("attempt-lease-conflict") || message.includes("state-conflict");
588
+ return {
589
+ status: conflict ? 409 : 422,
590
+ errorCode: conflict
591
+ ? "WORKER_CONTINUATION_LEASE_CONFLICT"
592
+ : "WORKER_CONTINUATION_AUTHORITY_MISMATCH",
593
+ body: {
594
+ ok: false,
595
+ error: {
596
+ code: conflict
597
+ ? "WORKER_CONTINUATION_LEASE_CONFLICT"
598
+ : "WORKER_CONTINUATION_AUTHORITY_MISMATCH",
599
+ message,
600
+ },
601
+ },
602
+ };
603
+ }
604
+ const stableCode = [
605
+ "ARTIFACT_REVISION_QUOTA_EXCEEDED",
606
+ "ARTIFACT_REVISION_DRIFT",
607
+ "ARTIFACT_REVISION_PATH_INVALID",
608
+ ].find((code) => message.startsWith(code));
609
+ void recordContextTransferEvent({
610
+ appData,
611
+ event: "artifact_revision.rejected",
612
+ fields: {
613
+ runId,
614
+ nodeId,
615
+ reasonCode: stableCode ?? "INVALID_INPUT",
616
+ },
617
+ }).catch(() => undefined);
618
+ return {
619
+ status: stableCode ? 422 : 400,
620
+ errorCode: stableCode ?? "INVALID_INPUT",
621
+ body: { ok: false, error: { code: stableCode ?? "INVALID_INPUT", message } },
622
+ };
623
+ },
624
+ }
625
+ : {}),
626
+ onAdmitted: (operation) => {
627
+ admittedOperationId = operation.operationId;
628
+ if (input.action === "artifactRevisionStage") {
629
+ return recordContextTransferEvent({
630
+ appData,
631
+ event: "artifact_revision.upload_started",
632
+ fields: {
633
+ runId,
634
+ nodeId,
635
+ operationId: operation.operationId,
636
+ },
637
+ }).catch(() => undefined);
638
+ }
639
+ if (input.action === "artifactRevisionExecute") {
640
+ return recordContextTransferEvent({
641
+ appData,
642
+ event: "artifact_rerun.execute_requested",
643
+ fields: {
644
+ runId,
645
+ revisionId,
646
+ operationId: operation.operationId,
647
+ },
648
+ }).catch(() => undefined);
649
+ }
650
+ return undefined;
651
+ },
652
+ ...(input.action === "artifactRevisionExecute" && workerContinuation
653
+ ? {
654
+ beforeTerminal: (operation, state, info) => settleWorkerContinuation({
655
+ repoRoot: input.ctx.repoRoot,
656
+ resolved: workerContinuation,
657
+ operation,
658
+ state,
659
+ info,
660
+ }),
661
+ }
662
+ : {}),
663
+ });
664
+ if (accepted.kind === "error") {
665
+ return { status: accepted.status, body: accepted.body };
666
+ }
667
+ return { status: accepted.status, body: accepted.body };
668
+ }
146
669
  async function handleCreateOperation(req, res, ctx) {
147
670
  if (!ctx.operator) {
148
671
  sendJson(res, 503, {
@@ -188,7 +711,7 @@ async function handleCreateOperation(req, res, ctx) {
188
711
  }
189
712
  let body;
190
713
  try {
191
- body = (await readJsonBody(req));
714
+ body = (await readJsonBody(req, 15 * 1024 * 1024));
192
715
  }
193
716
  catch (error) {
194
717
  sendJson(res, 400, {
@@ -215,6 +738,27 @@ async function handleCreateOperation(req, res, ctx) {
215
738
  });
216
739
  return;
217
740
  }
741
+ if (action === "contextExport") {
742
+ const accepted = await acceptContextExportOperation({
743
+ ctx,
744
+ actionParams,
745
+ clientRequestId,
746
+ });
747
+ sendJson(res, accepted.status, accepted.body);
748
+ return;
749
+ }
750
+ if (action === "artifactRevisionStage" ||
751
+ action === "artifactRevisionPlan" ||
752
+ action === "artifactRevisionExecute") {
753
+ const accepted = await acceptArtifactRevisionOperation({
754
+ ctx,
755
+ action,
756
+ actionParams,
757
+ clientRequestId,
758
+ });
759
+ sendJson(res, accepted.status, accepted.body);
760
+ return;
761
+ }
218
762
  const result = await dispatchOperatorAction(ctx.operator.actionContext, {
219
763
  action,
220
764
  actionParams,
@@ -245,7 +789,35 @@ async function handleGetOperation(_req, res, ctx, operationId) {
245
789
  });
246
790
  return;
247
791
  }
248
- sendJson(res, 200, { ok: true, operation: op });
792
+ sendJson(res, 200, {
793
+ ok: true,
794
+ workspaceId: ctx.workspace?.workspaceId,
795
+ operation: { ...op, workspaceId: ctx.workspace?.workspaceId },
796
+ });
797
+ }
798
+ /**
799
+ * GET /api/operator/v1/operations?workspaceId= — NEW per-workspace list route
800
+ * (multi-workspace foundation). Returns only the resolved Context's Operations
801
+ * and injects the owning workspaceId. operation-store.ts is unchanged.
802
+ */
803
+ async function handleListOperations(_req, res, ctx) {
804
+ if (!ctx.operator) {
805
+ sendJson(res, 503, {
806
+ ok: false,
807
+ error: {
808
+ code: "INTERNAL_ERROR",
809
+ message: "operator runtime unavailable",
810
+ },
811
+ });
812
+ return;
813
+ }
814
+ const operations = await ctx.operator.operations.list();
815
+ sendJson(res, 200, {
816
+ ok: true,
817
+ schemaVersion: 1,
818
+ workspaceId: ctx.workspace?.workspaceId,
819
+ ops: operations.map((op) => ({ ...op, workspaceId: ctx.workspace?.workspaceId })),
820
+ });
249
821
  }
250
822
  async function handleOperationEvents(req, res, ctx, operationId) {
251
823
  if (!ctx.operator) {
@@ -330,6 +902,34 @@ async function handleOperationEvents(req, res, ctx, operationId) {
330
902
  * Path-escape-safe static file serve for Console SPA.
331
903
  * Sets boot capability cookie on HTML responses.
332
904
  */
905
+ /**
906
+ * Inject the general Console confirmation token into HTML, and additionally
907
+ * the scoped inspect mutation token when the experimental prompt-override
908
+ * switch is enabled and inspect credentials are available. The standalone
909
+ * /inspect/ document canonicalizes into the Console SPA (/#/inspect...), so
910
+ * the SPA must carry the scoped inspect token for the export/rerun tab to be
911
+ * rendered by the mounted inspect workspace.
912
+ */
913
+ function injectScopedInspectTokens(raw, ctx) {
914
+ let content = injectConfirmationTokenScript(raw, ctx.bootToken.confirmationToken);
915
+ if (isDagRerunPromptOverrideEnabled() && ctx.inspectBootToken) {
916
+ content = injectConfirmationTokenScript(content, ctx.inspectBootToken.confirmationToken, "__INSPECT_MUTATION_TOKEN__");
917
+ }
918
+ return content;
919
+ }
920
+ /**
921
+ * HTML response Set-Cookie headers for the Console SPA. The general boot
922
+ * cookie is always set; when the experimental prompt-override switch is
923
+ * enabled the scoped inspect cookie is set alongside so the hosted inspect
924
+ * workspace (mounted inside the SPA) can authenticate its allowlisted
925
+ * mutation requests.
926
+ */
927
+ function scopedSetCookieHeaders(ctx) {
928
+ if (isDagRerunPromptOverrideEnabled() && ctx.inspectBootToken) {
929
+ return [ctx.bootToken.setCookieHeader, ctx.inspectBootToken.setCookieHeader];
930
+ }
931
+ return ctx.bootToken.setCookieHeader;
932
+ }
333
933
  export async function serveConsoleStatic(req, res, ctx) {
334
934
  const url = new URL(req.url ?? "/", "http://localhost");
335
935
  let pathname = decodeURIComponent(url.pathname);
@@ -348,11 +948,14 @@ export async function serveConsoleStatic(req, res, ctx) {
348
948
  // SPA fallback for non-file routes
349
949
  const indexPath = path.join(resolvedStatic, "index.html");
350
950
  if (existsSync(indexPath) && !pathname.includes(".")) {
351
- const html = injectConfirmationTokenScript(await readFile(indexPath), ctx.bootToken.confirmationToken);
951
+ const html = injectScopedInspectTokens(await readFile(indexPath), ctx);
352
952
  res.writeHead(200, {
353
953
  "Content-Type": "text/html; charset=utf-8",
354
954
  "Content-Length": html.byteLength,
355
- "Set-Cookie": ctx.bootToken.setCookieHeader,
955
+ // Boot capability tokens are baked into the document; a cached
956
+ // page from a previous Console process makes every mutation 403.
957
+ "Cache-Control": "no-store",
958
+ "Set-Cookie": scopedSetCookieHeaders(ctx),
356
959
  });
357
960
  res.end(html);
358
961
  return;
@@ -363,14 +966,15 @@ export async function serveConsoleStatic(req, res, ctx) {
363
966
  const raw = await readFile(resolvedFile);
364
967
  const contentType = contentTypeFor(pathname);
365
968
  const content = contentType.startsWith("text/html")
366
- ? injectConfirmationTokenScript(raw, ctx.bootToken.confirmationToken)
969
+ ? injectScopedInspectTokens(raw, ctx)
367
970
  : raw;
368
971
  const headers = {
369
972
  "Content-Type": contentType,
370
973
  "Content-Length": content.byteLength,
371
974
  };
372
975
  if (contentType.startsWith("text/html")) {
373
- headers["Set-Cookie"] = ctx.bootToken.setCookieHeader;
976
+ headers["Cache-Control"] = "no-store";
977
+ headers["Set-Cookie"] = scopedSetCookieHeaders(ctx);
374
978
  }
375
979
  res.writeHead(200, headers);
376
980
  res.end(content);
@@ -453,6 +1057,51 @@ export async function handleConsoleOperatorRequest(req, res, ctx) {
453
1057
  await handleCreateOperation(req, res, ctx);
454
1058
  return true;
455
1059
  }
1060
+ if (method === "GET" && pathname === "/api/operator/v1/operations") {
1061
+ await handleListOperations(req, res, ctx);
1062
+ return true;
1063
+ }
1064
+ const contextDownloadMatch = pathname.match(/^\/api\/operator\/v1\/context-exports\/([a-f0-9]{32})\/download$/);
1065
+ if (method === "GET" && contextDownloadMatch) {
1066
+ if (!ctx.operator) {
1067
+ sendJson(res, 503, { ok: false, error: { code: "INTERNAL_ERROR", message: "operator runtime unavailable" } });
1068
+ return true;
1069
+ }
1070
+ const failure = evaluateConsoleDownloadCapability(req, ctx);
1071
+ if (failure) {
1072
+ sendJson(res, failure.status, { ok: false, error: { code: failure.code, message: failure.message } });
1073
+ return true;
1074
+ }
1075
+ try {
1076
+ const exportId = contextDownloadMatch[1];
1077
+ const leased = await acquireContextExportLease({
1078
+ appData: ctx.operator.actionContext.appData,
1079
+ exportId,
1080
+ });
1081
+ res.writeHead(200, {
1082
+ "Content-Type": "application/zip",
1083
+ "Content-Length": leased.bytes.byteLength,
1084
+ "Content-Disposition": `attachment; filename="context-${leased.record.bundleId}.zip"`,
1085
+ "Cache-Control": "no-store",
1086
+ "X-Content-Type-Options": "nosniff",
1087
+ });
1088
+ res.end(leased.bytes);
1089
+ await releaseContextExportLease({
1090
+ appData: ctx.operator.actionContext.appData,
1091
+ exportId,
1092
+ leaseId: leased.leaseId,
1093
+ }).catch(() => undefined);
1094
+ }
1095
+ catch (error) {
1096
+ if (res.headersSent)
1097
+ return true;
1098
+ sendJson(res, 404, {
1099
+ ok: false,
1100
+ error: { code: "CONTEXT_EXPORT_UNAVAILABLE", message: error instanceof Error ? error.message : String(error) },
1101
+ });
1102
+ }
1103
+ return true;
1104
+ }
456
1105
  if (method === "GET" && pathname === "/api/operator/v1/night-aux-ticker") {
457
1106
  await handleNightAuxTickerGet(req, res, ctx);
458
1107
  return true;
@@ -517,3 +1166,273 @@ export async function handleConsoleRequest(req, res, ctx) {
517
1166
  }
518
1167
  return false;
519
1168
  }
1169
+ export function createWorkspaceReadinessCache() {
1170
+ const entries = new Map();
1171
+ return {
1172
+ get: (repoRoot) => entries.get(repoRoot),
1173
+ set: (repoRoot, entry) => {
1174
+ entries.set(repoRoot, entry);
1175
+ },
1176
+ };
1177
+ }
1178
+ const defaultWorkspaceReadinessCache = createWorkspaceReadinessCache();
1179
+ const READY_STATE_TTL_MS = 60_000;
1180
+ const NON_READY_STATE_TTL_MS = 15_000;
1181
+ function isCacheFresh(entry, now) {
1182
+ const ttl = entry.state === "ready" ? READY_STATE_TTL_MS : NON_READY_STATE_TTL_MS;
1183
+ return now - entry.probedAt < ttl;
1184
+ }
1185
+ /** Readiness placeholder for a never-probed workspace (AC-006): the list is
1186
+ * still returned immediately with a pending marker, refreshed by the
1187
+ * background probe. */
1188
+ function pendingReadinessSummary(inspectReady) {
1189
+ return {
1190
+ state: "pending",
1191
+ summary: `pi=pending; inspect=${inspectReady ? "ready" : "unavailable"}`,
1192
+ };
1193
+ }
1194
+ /**
1195
+ * Process-level Workspace Registry HTTP surface (2026-08-22 multi-workspace
1196
+ * foundation):
1197
+ * GET /api/operator/v1/workspaces — visible list
1198
+ * POST /api/operator/v1/workspaces/pick — host native folder chooser
1199
+ * POST /api/operator/v1/workspaces — body { "name": "<label>",
1200
+ * "directoryPath": "<dir>" }
1201
+ * DELETE /api/operator/v1/workspaces/:workspaceId — soft remove (restore on re-add)
1202
+ *
1203
+ * POST/DELETE are mutations and pass the Console mutation gate. GET carries no
1204
+ * workspace selection parameter.
1205
+ *
1206
+ * AC-006 (2026-08-23): GET is NON-BLOCKING — registry + process-level
1207
+ * readiness cache answer immediately (first sight may be a pending
1208
+ * placeholder); probes refresh the cache in the background for subsequent
1209
+ * requests. A hanging probe can never delay the 200 list.
1210
+ */
1211
+ export async function handleWorkspaceRoutes(req, res, registry, deps, url, pathname, method, readinessDeps) {
1212
+ if (method === "GET" && pathname === "/api/operator/v1/workspaces") {
1213
+ const records = await registry.listVisible();
1214
+ const inspectReady = readinessDeps.inspectReady;
1215
+ const readinessCache = readinessDeps.readinessCache ?? defaultWorkspaceReadinessCache;
1216
+ const workspaces = records.map((record) => {
1217
+ const cached = readinessCache.get(record.repoRoot);
1218
+ return {
1219
+ workspaceId: record.workspaceId,
1220
+ name: record.name,
1221
+ displayPath: record.displayPath,
1222
+ repoFingerprint: record.repoFingerprint,
1223
+ createdAt: record.createdAt,
1224
+ lastAccessedAt: record.lastAccessedAt,
1225
+ readiness: cached
1226
+ ? {
1227
+ state: cached.state,
1228
+ summary: `pi=${cached.state}; inspect=${inspectReady ? "ready" : "unavailable"}`,
1229
+ }
1230
+ : pendingReadinessSummary(inspectReady),
1231
+ };
1232
+ });
1233
+ // Respond FIRST, then refresh readiness in the background (AC-006).
1234
+ sendJson(res, 200, {
1235
+ ok: true,
1236
+ schemaVersion: 1,
1237
+ workspaces,
1238
+ });
1239
+ void Promise.all(records.map(async (record) => {
1240
+ const cached = readinessCache.get(record.repoRoot);
1241
+ if (cached && isCacheFresh(cached, Date.now()))
1242
+ return;
1243
+ let state;
1244
+ try {
1245
+ const report = await probePiReadiness({
1246
+ override: deps.piReadinessProbe,
1247
+ repoRoot: record.repoRoot,
1248
+ });
1249
+ state = report.state;
1250
+ }
1251
+ catch {
1252
+ state = "setup-required";
1253
+ }
1254
+ readinessCache.set(record.repoRoot, {
1255
+ state,
1256
+ probedAt: Date.now(),
1257
+ });
1258
+ })).catch(() => undefined);
1259
+ return;
1260
+ }
1261
+ if (method === "POST" && pathname === "/api/operator/v1/workspaces/pick") {
1262
+ const failure = evaluateMutationGate(req, {
1263
+ cookieName: deps.bootToken.cookieName,
1264
+ bootToken: deps.bootToken.value,
1265
+ confirmationToken: deps.bootToken.confirmationToken,
1266
+ consoleOrigin: deps.getConsoleOrigin(),
1267
+ allowNonLoopbackAccess: deps.allowNonLoopbackAccess,
1268
+ });
1269
+ if (failure) {
1270
+ sendJson(res, failure.status, {
1271
+ ok: false,
1272
+ error: { code: failure.code, message: failure.message },
1273
+ });
1274
+ return;
1275
+ }
1276
+ if (deps.allowNonLoopbackAccess) {
1277
+ sendJson(res, 409, {
1278
+ ok: false,
1279
+ error: {
1280
+ code: "NATIVE_PICKER_UNAVAILABLE",
1281
+ message: "原生目录选择仅在本机回环地址启动的 Console 中可用。",
1282
+ },
1283
+ });
1284
+ return;
1285
+ }
1286
+ const abortController = new AbortController();
1287
+ const abort = () => abortController.abort();
1288
+ req.once("aborted", abort);
1289
+ res.once("close", abort);
1290
+ try {
1291
+ const directoryPath = await (deps.pickDirectory ?? pickNativeWorkspaceDirectory)(abortController.signal);
1292
+ if (!res.writableEnded && !abortController.signal.aborted) {
1293
+ sendJson(res, 200, { ok: true, directoryPath });
1294
+ }
1295
+ }
1296
+ catch (error) {
1297
+ if (!res.writableEnded && !abortController.signal.aborted) {
1298
+ sendJson(res, 500, {
1299
+ ok: false,
1300
+ error: {
1301
+ code: "DIRECTORY_PICKER_FAILED",
1302
+ message: error instanceof Error ? error.message : String(error),
1303
+ },
1304
+ });
1305
+ }
1306
+ }
1307
+ finally {
1308
+ req.off("aborted", abort);
1309
+ res.off("close", abort);
1310
+ }
1311
+ return;
1312
+ }
1313
+ if (method === "POST" && pathname === "/api/operator/v1/workspaces") {
1314
+ const failure = evaluateMutationGate(req, {
1315
+ cookieName: deps.bootToken.cookieName,
1316
+ bootToken: deps.bootToken.value,
1317
+ confirmationToken: deps.bootToken.confirmationToken,
1318
+ consoleOrigin: deps.getConsoleOrigin(),
1319
+ allowNonLoopbackAccess: deps.allowNonLoopbackAccess,
1320
+ });
1321
+ if (failure) {
1322
+ sendJson(res, failure.status, {
1323
+ ok: false,
1324
+ error: { code: failure.code, message: failure.message },
1325
+ });
1326
+ return;
1327
+ }
1328
+ let body;
1329
+ try {
1330
+ body = (await readJsonBody(req));
1331
+ }
1332
+ catch (error) {
1333
+ sendJson(res, 400, {
1334
+ ok: false,
1335
+ error: {
1336
+ code: "INVALID_INPUT",
1337
+ message: error instanceof Error ? error.message : String(error),
1338
+ },
1339
+ });
1340
+ return;
1341
+ }
1342
+ const directoryPath = typeof body.directoryPath === "string"
1343
+ ? body.directoryPath
1344
+ : typeof body.path === "string"
1345
+ ? body.path
1346
+ : "";
1347
+ if (!directoryPath.trim()) {
1348
+ sendJson(res, 400, {
1349
+ ok: false,
1350
+ error: {
1351
+ code: "INVALID_INPUT",
1352
+ message: "directoryPath (string) is required",
1353
+ },
1354
+ });
1355
+ return;
1356
+ }
1357
+ if (body.name !== undefined &&
1358
+ (typeof body.name !== "string" || !body.name.trim())) {
1359
+ sendJson(res, 400, {
1360
+ ok: false,
1361
+ error: {
1362
+ code: "INVALID_INPUT",
1363
+ message: "name must be a non-empty string when provided",
1364
+ },
1365
+ });
1366
+ return;
1367
+ }
1368
+ const name = typeof body.name === "string" ? body.name : undefined;
1369
+ const result = await registry.add(directoryPath, name ? { name } : {});
1370
+ if (!result.ok) {
1371
+ const status = result.code === "DUPLICATE"
1372
+ ? 409
1373
+ : result.code === "INVALID_PATH"
1374
+ ? 400
1375
+ : 400;
1376
+ sendJson(res, status, {
1377
+ ok: false,
1378
+ error: {
1379
+ code: result.code,
1380
+ message: result.message,
1381
+ },
1382
+ });
1383
+ return;
1384
+ }
1385
+ const record = result.record;
1386
+ sendJson(res, 201, {
1387
+ ok: true,
1388
+ workspace: {
1389
+ workspaceId: record.workspaceId,
1390
+ name: record.name,
1391
+ displayPath: record.displayPath,
1392
+ repoFingerprint: record.repoFingerprint,
1393
+ createdAt: record.createdAt,
1394
+ lastAccessedAt: record.lastAccessedAt,
1395
+ readiness: {
1396
+ state: "setup-required",
1397
+ summary: `pi=unknown; inspect=${readinessDeps.inspectReady ? "ready" : "unavailable"}`,
1398
+ },
1399
+ },
1400
+ });
1401
+ return;
1402
+ }
1403
+ const removeMatch = pathname.match(/^\/api\/operator\/v1\/workspaces\/([^/]+)$/);
1404
+ if (method === "DELETE" && removeMatch) {
1405
+ const failure = evaluateMutationGate(req, {
1406
+ cookieName: deps.bootToken.cookieName,
1407
+ bootToken: deps.bootToken.value,
1408
+ confirmationToken: deps.bootToken.confirmationToken,
1409
+ consoleOrigin: deps.getConsoleOrigin(),
1410
+ allowNonLoopbackAccess: deps.allowNonLoopbackAccess,
1411
+ });
1412
+ if (failure) {
1413
+ sendJson(res, failure.status, {
1414
+ ok: false,
1415
+ error: { code: failure.code, message: failure.message },
1416
+ });
1417
+ return;
1418
+ }
1419
+ const workspaceId = decodeURIComponent(removeMatch[1]);
1420
+ const removed = await registry.remove(workspaceId);
1421
+ if (!removed) {
1422
+ sendJson(res, 404, {
1423
+ ok: false,
1424
+ error: {
1425
+ code: "WORKSPACE_NOT_FOUND",
1426
+ message: `unknown workspace: ${workspaceId}`,
1427
+ },
1428
+ });
1429
+ return;
1430
+ }
1431
+ sendJson(res, 200, { ok: true, workspaceId, removed: true });
1432
+ return;
1433
+ }
1434
+ sendJson(res, 404, {
1435
+ ok: false,
1436
+ error: { code: "NOT_FOUND", message: `unknown workspace route: ${pathname}` },
1437
+ });
1438
+ }