@tea-agent/loop-agent 0.40.0-next.9 → 0.41.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 (218) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +130 -3
  3. package/README.md +2 -2
  4. package/dist/adapters/context-transfer/optional-pi-handoff.js +31 -0
  5. package/dist/adapters/context-transfer/pi-session.js +61 -0
  6. package/dist/application/dag/generate-task-dag.js +6 -2
  7. package/dist/build-stamp.json +3 -3
  8. package/dist/cli/command-definitions.js +21 -5
  9. package/dist/cli/program.js +61 -4
  10. package/dist/commands/client-recovery.js +8 -36
  11. package/dist/commands/dag-artifact.js +284 -0
  12. package/dist/commands/dag-context.js +184 -0
  13. package/dist/commands/dag-rerun.js +206 -1
  14. package/dist/commands/init-model-catalog.js +22 -0
  15. package/dist/commands/task-advance.js +23 -1
  16. package/dist/commands/task-source-prepare.js +5 -0
  17. package/dist/commands/task-status.js +1 -0
  18. package/dist/executors/dag-pi-executor.js +39 -12
  19. package/dist/executors/pi-sdk-executor.js +36 -5
  20. package/dist/executors/shell-executor.js +64 -17
  21. package/dist/governance/checks.js +1 -0
  22. package/dist/{worker → infrastructure}/console/app-data.js +4 -0
  23. package/dist/infrastructure/console/artifact-revision-store.js +430 -0
  24. package/dist/infrastructure/console/context-export-store.js +160 -0
  25. package/dist/infrastructure/console/dir-lock.js +132 -0
  26. package/dist/{worker → infrastructure}/console/operation-store.js +28 -0
  27. package/dist/infrastructure/harness/artifact-store.js +10 -1
  28. package/dist/infrastructure/harness/atomic-write.js +12 -2
  29. package/dist/shared/context-transfer/artifact-revision.js +172 -0
  30. package/dist/shared/context-transfer.js +418 -0
  31. package/dist/shared/operator/capabilities.js +23 -1
  32. package/dist/shared/operator/safe-run-summary.js +1 -0
  33. package/dist/shared/path-safety.js +93 -0
  34. package/dist/shared/pi-context-pressure/checkpoint.js +116 -0
  35. package/dist/shared/pi-context-pressure/compaction-policy.js +151 -0
  36. package/dist/shared/pi-context-pressure/env.js +58 -0
  37. package/dist/shared/pi-context-pressure/extension.js +100 -0
  38. package/dist/shared/pi-context-pressure/index.js +7 -0
  39. package/dist/shared/pi-context-pressure/overflow.js +252 -0
  40. package/dist/shared/pi-context-pressure/sift-bridge.js +386 -0
  41. package/dist/shared/pi-context-pressure/telemetry.js +51 -0
  42. package/dist/shared/preview.js +28 -4
  43. package/dist/task/config-types.js +2 -1
  44. package/dist/task/task-demand-routing.js +3 -0
  45. package/dist/worker/console/chat/assistant-content.js +7 -0
  46. package/dist/worker/console/chat/chat-event-store.js +84 -13
  47. package/dist/worker/console/chat/goal-round-driver.js +72 -0
  48. package/dist/worker/console/chat/goals.js +300 -0
  49. package/dist/worker/console/chat/pi-runtime.js +285 -8
  50. package/dist/worker/console/chat/provider-error.js +2 -1
  51. package/dist/worker/console/chat/repo-browser.js +94 -12
  52. package/dist/worker/console/chat/resource-preferences-store.js +1 -1
  53. package/dist/worker/console/chat/routes.js +487 -30
  54. package/dist/worker/console/chat/session-catalog.js +48 -0
  55. package/dist/worker/console/chat/session-stats.js +96 -0
  56. package/dist/worker/console/chat/session-store.js +28 -19
  57. package/dist/worker/console/chat/shortcuts.js +7 -0
  58. package/dist/worker/console/chat/sift-bridge.js +1 -0
  59. package/dist/worker/console/chat/turn-process.js +32 -3
  60. package/dist/worker/console/chat/user-questions.js +1 -1
  61. package/dist/worker/console/console-update-runtime.js +1 -1
  62. package/dist/worker/console/context-transfer-diagnostics.js +198 -0
  63. package/dist/worker/console/dag-confirmation.js +1 -1
  64. package/dist/worker/console/dag-execution-receipt.js +21 -3
  65. package/dist/worker/console/doctor.js +1 -1
  66. package/dist/worker/console/draft-store.js +1 -1
  67. package/dist/worker/console/human-gate-token.js +1 -1
  68. package/dist/worker/console/index.js +3 -3
  69. package/dist/worker/console/interview/assessment.js +1 -1
  70. package/dist/worker/console/interview/session.js +1 -1
  71. package/dist/worker/console/operation-runner.js +45 -4
  72. package/dist/worker/console/operation-sse.js +1 -1
  73. package/dist/worker/console/operation-wait.js +1 -1
  74. package/dist/worker/console/operator-actions.js +124 -8
  75. package/dist/worker/console/operator-selection.js +6 -2
  76. package/dist/worker/console/operator-surface-health.js +1 -1
  77. package/dist/worker/console/operator-user-error.js +169 -0
  78. package/dist/worker/console/pi-plugins.catalog.json +73658 -0
  79. package/dist/worker/console/pi-plugins.js +179 -0
  80. package/dist/worker/console/routes.js +864 -13
  81. package/dist/worker/console/security.js +35 -0
  82. package/dist/worker/console/server.js +177 -44
  83. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Bs-CDAXM.js → abnfDiagram-N423BO3Z-DSVcg1YO.js} +1 -1
  84. package/dist/worker/console/static/assets/{arc-CAkA3We3.js → arc-BtXhIqui.js} +1 -1
  85. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DEmI_zqr.js → architectureDiagram-T3A2C74G-B-nh-ilJ.js} +1 -1
  86. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-uDHANebE.js → blockDiagram-VBNYF7ZC-BIUSVdZI.js} +1 -1
  87. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-Bv97b4_B.js → c4Diagram-5PPSVZJV-Cmw3L4qM.js} +1 -1
  88. package/dist/worker/console/static/assets/channel-BhZFNefx.js +1 -0
  89. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-shmQKsFz.js → chunk-2GRJ4B5K-CGpMMjGm.js} +1 -1
  90. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-BAraNvcN.js → chunk-2Q5K7J3B-BNBkEpEJ.js} +1 -1
  91. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BirhxUop.js → chunk-5RXB4S5H-ubln3eaU.js} +1 -1
  92. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-C9LgUJpR.js → chunk-5VM5RSS4-_XcBTt1i.js} +1 -1
  93. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DrU-s-t7.js → chunk-6Q2QTUOP-Ln30Pgrh.js} +1 -1
  94. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-Tx-H_FA2.js → chunk-GF5L2VYU-ESH3lhSx.js} +1 -1
  95. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BjLtNFrR.js → chunk-JWPE2WC7-DAJt-qXB.js} +1 -1
  96. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-UNLVwSsy.js → chunk-KBJHAD2P-Cx10LIcH.js} +1 -1
  97. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CBEUX8XD.js → chunk-RYQCIY6F-CENeOxoJ.js} +1 -1
  98. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-WTL7IZOq.js → chunk-XXDRQBXY-DXqK-D38.js} +1 -1
  99. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Sbv-NJni.js +1 -0
  100. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Sbv-NJni.js +1 -0
  101. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CkHG6W3s.js → cose-bilkent-JH36ORCC-CE4dmvGY.js} +1 -1
  102. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BRzFqTaL.js → cynefin-VYW2F7L2-CRacN1zK.js} +1 -1
  103. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-2_CFp_i7.js → cynefinDiagram-MW4NZA55-Cneukjrt.js} +1 -1
  104. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BJtsmdkE.js → dagre-VZM6K2ZE-DPFboRoL.js} +1 -1
  105. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-DNEFFo6w.js → diagram-7IWD3JNH-DIAQgcOP.js} +1 -1
  106. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B_IWV7eo.js → diagram-B4RE2ZJO-VrYMKxuy.js} +1 -1
  107. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DvvRx2dz.js → diagram-LBJQPF4R-CLHvBghn.js} +1 -1
  108. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-CsibsWLg.js → diagram-Q27KOJAE-DVCWBif8.js} +1 -1
  109. package/dist/worker/console/static/assets/{diagram-UB23O5K3-les_djBQ.js → diagram-UB23O5K3-BTMmbBzJ.js} +1 -1
  110. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BEm2n7qJ.js → ebnfDiagram-BXEA7PRR-BUZsKJ56.js} +1 -1
  111. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BI7vHHVm.js → erDiagram-JOGREHBK-BsBZ5NXC.js} +1 -1
  112. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CUFCERrB.js → flowDiagram-UKHOOZJN-D7fH_kDa.js} +1 -1
  113. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-wxyLiGWt.js → ganttDiagram-PKOTCBZU-Bza5OohY.js} +1 -1
  114. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Bt897K8Z.js → gitGraphDiagram-DS77QQ5N-DpkoGR_Y.js} +1 -1
  115. package/dist/worker/console/static/assets/index-Ius3vxhf.css +1 -0
  116. package/dist/worker/console/static/assets/index-yASzcbBr.js +451 -0
  117. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DYNx_IPv.js → infoDiagram-6WML65LV-BYm3g6LT.js} +1 -1
  118. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-CzpLNEu6.js → ishikawaDiagram-WSZJBQD7-C2RS_H4v.js} +1 -1
  119. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-97f9owNU.js → journeyDiagram-NVQOT4AX-B4slKpCo.js} +1 -1
  120. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BxM9hyBn.js → kanban-definition-27J2QSJJ-B7orRNpI.js} +1 -1
  121. package/dist/worker/console/static/assets/{linear-ByuxcHvp.js → linear-D1BeBxLQ.js} +1 -1
  122. package/dist/worker/console/static/assets/{mermaid.core-DwPGvpCv.js → mermaid.core-0TtMP8k4.js} +5 -5
  123. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DCoqG9RT.js → mindmap-definition-FAOFIHXS-XL5EbCtT.js} +1 -1
  124. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BLSBmBMx.js → pegDiagram-VL7TDLO6-CwF7uas5.js} +1 -1
  125. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-B0t_0oJW.js → pieDiagram-7S7Q4E2Y-A_Jo-72f.js} +1 -1
  126. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-sTzDk7Nt.js → quadrantDiagram-CIZ2JOQS-CxOa4gKL.js} +1 -1
  127. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-B3jRdATC.js → railroadDiagram-AXF67PYL-DoAoswIH.js} +1 -1
  128. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D-cTtXEN.js → requirementDiagram-LRYGKXZP-C1Lc1MlD.js} +1 -1
  129. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-B25igJRE.js → sankeyDiagram-W5VNT64P-DFXOkq0U.js} +1 -1
  130. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-jW_lM1_D.js → sequenceDiagram-SI44F4Z6-B0ufUCuW.js} +1 -1
  131. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-CrzTgsHE.js → sizeCapture-X5ZJPWSS-iPbW55g3.js} +1 -1
  132. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-B01MvlqE.js → stateDiagram-OKZ733FA-CHiXzEKP.js} +1 -1
  133. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BtMgzo_H.js +1 -0
  134. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-YcF12FiU.js → swimlanes-SLNWSIFB-LyFs7TpX.js} +2 -2
  135. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Cb-OVd9Y.js +8 -0
  136. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-ClnRcg2s.js → timeline-definition-Z64GVDOM-tNkJnb6z.js} +1 -1
  137. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-B6gr_z-q.js → vennDiagram-T6HMQDX7-DFm7Oo8o.js} +1 -1
  138. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CvSgguGH.js → wardleyDiagram-T6FBY63Y-tpblTp9i.js} +1 -1
  139. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-23OtSZCb.js → xychartDiagram-ELKLHX3M-BCDLWR_D.js} +1 -1
  140. package/dist/worker/console/static/index.html +7 -2
  141. package/dist/worker/console/static-src/app/useOperatorActions.js +3 -2
  142. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +95 -25
  143. package/dist/worker/console/static-src/operator-chat/compaction-message.js +35 -1
  144. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +3 -1
  145. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +26 -19
  146. package/dist/worker/console/static-src/operator-chat/useChatStream.js +0 -11
  147. package/dist/worker/console/static-src/operator-chat/useChatThread.js +14 -1
  148. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +34 -4
  149. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +28 -3
  150. package/dist/worker/console/static-src/operator-chat/workspace-reorder.js +18 -0
  151. package/dist/worker/console/static-src/shell/console-update-reload.js +25 -0
  152. package/dist/worker/console/static-src/shell/useWorkspaces.js +150 -34
  153. package/dist/worker/console/static-src/shell/workspace-route.js +3 -1
  154. package/dist/worker/console/workspace-context.js +56 -5
  155. package/dist/worker/console/workspace-registry.js +335 -62
  156. package/dist/worker/continuation/worker-continuation.js +278 -0
  157. package/dist/worker/observe/health.js +1 -1
  158. package/dist/worker/observe/node-transparency.js +572 -0
  159. package/dist/worker/observe/routes.js +51 -1
  160. package/dist/worker/observe/static/api.js +69 -5
  161. package/dist/worker/observe/static/dag-context-reason-labels.d.ts +9 -0
  162. package/dist/worker/observe/static/dag-context-reason-labels.js +120 -0
  163. package/dist/worker/observe/static/inspect-bootstrap.d.ts +8 -0
  164. package/dist/worker/observe/static/inspect-bootstrap.js +57 -0
  165. package/dist/worker/observe/static/inspect-workspace.js +54 -10
  166. package/dist/worker/observe/static/operator-chrome.css +23 -4
  167. package/dist/worker/observe/static/operator-chrome.js +7 -2
  168. package/dist/worker/observe/static/state.js +3 -1
  169. package/dist/worker/observe/static/styles.css +1108 -135
  170. package/dist/worker/observe/static/views/dag-inspector.js +1257 -59
  171. package/dist/worker/observe/static/views/dags.js +7 -5
  172. package/dist/worker/observe/static/views/session-timeline.js +210 -10
  173. package/dist/worker/outcomes/adapters.js +19 -0
  174. package/dist/worker/outcomes/types.js +1 -0
  175. package/dist/worker/pool/attempt-lease.js +97 -66
  176. package/dist/worker/pool/begin-attempt-with-lease.js +1 -0
  177. package/dist/worker/pool/reconcile.js +46 -1
  178. package/dist/worker/pool/run-store.js +2 -0
  179. package/dist/worker/pool/state-projection.js +2 -0
  180. package/dist/worker/task-spec/workflow-routing.js +8 -3
  181. package/dist/workflows/dag/artifact-bindings.js +149 -0
  182. package/dist/workflows/dag/artifact-revision-schema-registry.js +31 -0
  183. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +162 -8
  184. package/dist/workflows/dag/backend-test-pytest-collection.js +70 -2
  185. package/dist/workflows/dag/backend-test-result-contract.js +4 -0
  186. package/dist/workflows/dag/backend-test-scenario-param.js +339 -53
  187. package/dist/workflows/dag/backend-test-writer-completeness.js +11 -0
  188. package/dist/workflows/dag/budget-enforcement.js +10 -0
  189. package/dist/workflows/dag/context-receipt.js +305 -0
  190. package/dist/workflows/dag/context-transfer/context-bundle.js +423 -0
  191. package/dist/workflows/dag/context-transfer/operator-actions.js +61 -0
  192. package/dist/workflows/dag/context-transfer/renderers.js +93 -0
  193. package/dist/workflows/dag/frontend-writer-rollback.js +32 -0
  194. package/dist/workflows/dag/init-hybrid.js +151 -4
  195. package/dist/workflows/dag/node-execution.js +112 -27
  196. package/dist/workflows/dag/path-safety.js +1 -0
  197. package/dist/workflows/dag/prompt.js +29 -4
  198. package/dist/workflows/dag/rerun-plan.js +555 -1
  199. package/dist/workflows/dag/rerun-run.js +601 -15
  200. package/dist/workflows/dag/runner.js +20 -1
  201. package/dist/workflows/dag/skill-snapshot.js +17 -0
  202. package/dist/workflows/dag/types.js +183 -7
  203. package/docs/architecture/runtime-boundaries.md +8 -7
  204. package/docs/architecture/worker-and-feature.md +1 -1
  205. package/docs/templates/agent-dag.schema.json +32 -0
  206. package/docs/templates/backend-test-dag.json +6 -5
  207. package/docs/templates/init-managed-agents.md +1 -1
  208. package/docs/templates/product-line/task.yaml +1 -1
  209. package/package.json +5 -2
  210. package/skills/loop-agent/references/command-reference.md +21 -1
  211. package/dist/worker/console/static/assets/channel-amxUpk7o.js +0 -1
  212. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-bejwBVIz.js +0 -1
  213. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-bejwBVIz.js +0 -1
  214. package/dist/worker/console/static/assets/index-BHhUOTri.css +0 -1
  215. package/dist/worker/console/static/assets/index-DqJbO3-p.js +0 -407
  216. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BvFKEtm2.js +0 -1
  217. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DIB2eVg-.js +0 -8
  218. /package/dist/{worker → infrastructure}/console/repo-fingerprint.js +0 -0
@@ -1,13 +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";
6
7
  import { probePiReadiness } from "./pi-readiness.js";
7
8
  import { buildOperatorSurfaceHealthV1 } from "./operator-surface-health.js";
8
9
  import { ROUTES } from "../observe/routes.js";
9
- import { evaluateMutationGate, injectConfirmationTokenScript, isMutationMethod, } from "./security.js";
10
+ import { evaluateProtectedDownloadGate, evaluateMutationGate, injectConfirmationTokenScript, isMutationMethod, } from "./security.js";
10
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 { installPiPlugin, listPiPluginVersions, listPiPlugins, uninstallPiPlugin, } from "./pi-plugins.js";
11
20
  import { pickNativeWorkspaceDirectory } from "./native-directory-picker.js";
12
21
  export function sendJson(res, status, body, extraHeaders) {
13
22
  const payload = JSON.stringify(body);
@@ -148,7 +157,516 @@ async function handleSessionPing(req, res, ctx) {
148
157
  export const INSPECT_MUTATION_ALLOWED_ACTIONS = new Set([
149
158
  "dagRerunPlan",
150
159
  "dagRerun",
160
+ "contextExport",
161
+ "artifactRevisionStage",
162
+ "artifactRevisionPlan",
163
+ "artifactRevisionExecute",
151
164
  ]);
165
+ function evaluateConsoleCapability(req, ctx) {
166
+ const common = {
167
+ consoleOrigin: ctx.getConsoleOrigin(),
168
+ allowNonLoopbackAccess: ctx.allowNonLoopbackAccess === true,
169
+ };
170
+ const general = evaluateMutationGate(req, {
171
+ ...common,
172
+ cookieName: ctx.bootToken.cookieName,
173
+ bootToken: ctx.bootToken.value,
174
+ confirmationToken: ctx.bootToken.confirmationToken,
175
+ });
176
+ if (!general || !ctx.inspectBootToken)
177
+ return general;
178
+ return evaluateMutationGate(req, {
179
+ ...common,
180
+ cookieName: ctx.inspectBootToken.cookieName,
181
+ bootToken: ctx.inspectBootToken.value,
182
+ confirmationToken: ctx.inspectBootToken.confirmationToken,
183
+ });
184
+ }
185
+ function evaluateConsoleDownloadCapability(req, ctx) {
186
+ const common = {
187
+ consoleOrigin: ctx.getConsoleOrigin(),
188
+ allowNonLoopbackAccess: ctx.allowNonLoopbackAccess === true,
189
+ };
190
+ const general = evaluateProtectedDownloadGate(req, {
191
+ ...common,
192
+ cookieName: ctx.bootToken.cookieName,
193
+ bootToken: ctx.bootToken.value,
194
+ confirmationToken: ctx.bootToken.confirmationToken,
195
+ });
196
+ if (!general || !ctx.inspectBootToken)
197
+ return general;
198
+ return evaluateProtectedDownloadGate(req, {
199
+ ...common,
200
+ cookieName: ctx.inspectBootToken.cookieName,
201
+ bootToken: ctx.inspectBootToken.value,
202
+ confirmationToken: ctx.inspectBootToken.confirmationToken,
203
+ });
204
+ }
205
+ async function acceptContextExportOperation(input) {
206
+ const operator = input.ctx.operator;
207
+ const clientRequestId = input.clientRequestId?.trim();
208
+ if (!clientRequestId) {
209
+ return {
210
+ status: 400,
211
+ body: { ok: false, error: { code: "INVALID_INPUT", message: "clientRequestId is required" } },
212
+ };
213
+ }
214
+ const runId = typeof input.actionParams.runId === "string" ? input.actionParams.runId.trim() : "";
215
+ const nodeId = typeof input.actionParams.nodeId === "string" ? input.actionParams.nodeId.trim() : "";
216
+ const attempt = Number(input.actionParams.attempt ?? 1);
217
+ const profile = input.actionParams.profile === "raw" ? "raw" : "redacted";
218
+ const target = ["generic", "pi-session", "pi-handoff", "readable"].includes(String(input.actionParams.target))
219
+ ? input.actionParams.target
220
+ : "generic";
221
+ if (!runId || !nodeId || !Number.isInteger(attempt) || attempt < 1) {
222
+ return {
223
+ status: 400,
224
+ body: { ok: false, error: { code: "INVALID_INPUT", message: "runId, nodeId and positive attempt are required" } },
225
+ };
226
+ }
227
+ if (profile === "raw" && input.actionParams.rawAcknowledged !== true) {
228
+ return {
229
+ status: 400,
230
+ body: { ok: false, error: { code: "RAW_EXPORT_ACKNOWLEDGEMENT_REQUIRED", message: "raw export requires explicit acknowledgement" } },
231
+ };
232
+ }
233
+ const created = await operator.operations.create({
234
+ clientRequestId,
235
+ action: "contextExport",
236
+ actionParams: { runId, nodeId, attempt, profile, target, rawAcknowledged: profile === "raw" },
237
+ });
238
+ if (created.kind === "conflict") {
239
+ return {
240
+ status: 409,
241
+ body: { ok: false, error: { code: created.code, message: created.message } },
242
+ };
243
+ }
244
+ if (created.kind === "created") {
245
+ void recordContextTransferEvent({
246
+ appData: operator.actionContext.appData,
247
+ event: "context_export.planned",
248
+ fields: {
249
+ runId,
250
+ nodeId,
251
+ profile,
252
+ target,
253
+ operationId: created.operation.operationId,
254
+ },
255
+ }).catch(() => undefined);
256
+ queueMicrotask(() => {
257
+ void (async () => {
258
+ const startedAt = Date.now();
259
+ await operator.operations.update(created.operation.operationId, {
260
+ state: "running",
261
+ startedAt: new Date().toISOString(),
262
+ });
263
+ try {
264
+ const appData = operator.actionContext.appData;
265
+ if (!appData.contextExports)
266
+ throw new Error("console context export root unavailable");
267
+ ensureSecureDir(appData.contextExports);
268
+ const incomingPath = path.join(appData.contextExports, `${randomBytes(16).toString("hex")}.incoming.zip`);
269
+ await rm(incomingPath, { force: true });
270
+ try {
271
+ const kernel = await runKernelCommand({
272
+ ctx: input.ctx,
273
+ args: [
274
+ "dag", "context", "export",
275
+ "--run-id", runId,
276
+ "--node-id", nodeId,
277
+ "--attempt", String(attempt),
278
+ "--target", target,
279
+ "--redaction", profile,
280
+ ...(profile === "raw" ? ["--acknowledge-sensitive-data"] : []),
281
+ "--output", incomingPath,
282
+ "--json",
283
+ ],
284
+ });
285
+ const payload = (kernel.json ?? {});
286
+ if (!kernel.ok || !payload.bundleId || !payload.bundleSha256) {
287
+ throw new Error(kernelFailureMessage(kernel));
288
+ }
289
+ const zipBytes = await readFile(incomingPath);
290
+ const exported = await createContextExport({
291
+ appData,
292
+ bundle: {
293
+ zipBytes,
294
+ manifest: {
295
+ bundleId: payload.bundleId,
296
+ bundleSha256: payload.bundleSha256,
297
+ profile,
298
+ target,
299
+ },
300
+ },
301
+ });
302
+ await recordContextTransferDiagnostic({
303
+ appData: operator.actionContext.appData,
304
+ event: "context-bundle.generate",
305
+ dimensions: {
306
+ outcome: "succeeded",
307
+ target,
308
+ profile,
309
+ bytes: exported.bytes,
310
+ durationMs: Date.now() - startedAt,
311
+ },
312
+ }).catch(() => undefined);
313
+ await recordContextTransferEvent({
314
+ appData: operator.actionContext.appData,
315
+ event: "context_export.ready",
316
+ fields: {
317
+ runId,
318
+ nodeId,
319
+ exportId: exported.exportId,
320
+ profile,
321
+ target,
322
+ bytes: exported.bytes,
323
+ durationMs: Date.now() - startedAt,
324
+ operationId: created.operation.operationId,
325
+ },
326
+ }).catch(() => undefined);
327
+ await operator.operations.update(created.operation.operationId, {
328
+ state: "succeeded",
329
+ finishedAt: new Date().toISOString(),
330
+ result: {
331
+ ok: true,
332
+ exportId: exported.exportId,
333
+ bundleId: exported.bundleId,
334
+ bundleSha256: exported.bundleSha256,
335
+ bytes: exported.bytes,
336
+ expiresAt: exported.expiresAt,
337
+ downloadUrl: `/api/operator/v1/context-exports/${exported.exportId}/download`,
338
+ },
339
+ });
340
+ }
341
+ finally {
342
+ await rm(incomingPath, { force: true }).catch(() => undefined);
343
+ }
344
+ }
345
+ catch (error) {
346
+ await recordContextTransferDiagnostic({
347
+ appData: operator.actionContext.appData,
348
+ event: "context-bundle.generate",
349
+ dimensions: {
350
+ outcome: "failed",
351
+ target,
352
+ profile,
353
+ reasonCode: "CONTEXT_EXPORT_FAILED",
354
+ durationMs: Date.now() - startedAt,
355
+ },
356
+ }).catch(() => undefined);
357
+ await recordContextTransferEvent({
358
+ appData: operator.actionContext.appData,
359
+ event: "context_export.failed",
360
+ fields: {
361
+ runId,
362
+ nodeId,
363
+ profile,
364
+ target,
365
+ reasonCode: "CONTEXT_EXPORT_FAILED",
366
+ durationMs: Date.now() - startedAt,
367
+ operationId: created.operation.operationId,
368
+ },
369
+ }).catch(() => undefined);
370
+ await operator.operations.update(created.operation.operationId, {
371
+ state: "failed",
372
+ finishedAt: new Date().toISOString(),
373
+ errorCode: "CONTEXT_EXPORT_FAILED",
374
+ errorMessage: error instanceof Error ? error.message : String(error),
375
+ });
376
+ }
377
+ })();
378
+ });
379
+ }
380
+ return {
381
+ status: 202,
382
+ body: {
383
+ operationId: created.operation.operationId,
384
+ state: created.operation.state,
385
+ clientRequestId: created.operation.clientRequestId,
386
+ action: created.operation.action,
387
+ },
388
+ };
389
+ }
390
+ async function runKernelCommand(input) {
391
+ const actionContext = input.ctx.operator.actionContext;
392
+ const run = actionContext.runCommand ??
393
+ ((args, options) => actionContext.client.run(args, options));
394
+ return (await run(input.args, {
395
+ cwd: actionContext.repoRoot,
396
+ artifactName: `kernel-${input.args.filter((arg) => !arg.startsWith("--")).slice(0, 3).join("-").replace(/\s+/g, "-")}`,
397
+ expectJson: true,
398
+ env: { ...process.env, LOOP_CONSOLE_APP_DATA: actionContext.appData.root },
399
+ }));
400
+ }
401
+ function kernelFailureMessage(result) {
402
+ const stderr = result.stderr.trim();
403
+ if (stderr) {
404
+ const lastLine = stderr.split("\n").filter(Boolean).pop() ?? stderr;
405
+ return lastLine.slice(0, 300);
406
+ }
407
+ const stdout = result.stdout.trim();
408
+ if (stdout)
409
+ return stdout.slice(0, 300);
410
+ return `kernel command failed with exit code ${result.exitCode}`;
411
+ }
412
+ function decodeBoundedBase64(value) {
413
+ if (typeof value !== "string" || value.length === 0 || value.length > 14_000_000) {
414
+ throw new Error("candidateBase64 is required and exceeds no more than 10 MiB decoded");
415
+ }
416
+ if (!/^[A-Za-z0-9+/]*={0,2}$/.test(value) || value.length % 4 !== 0) {
417
+ throw new Error("candidateBase64 is invalid");
418
+ }
419
+ const bytes = Buffer.from(value, "base64");
420
+ if (bytes.byteLength > 10 * 1024 * 1024)
421
+ throw new Error("candidate exceeds 10 MiB");
422
+ return bytes;
423
+ }
424
+ function revisionProjection(revision) {
425
+ if (!revision)
426
+ return undefined;
427
+ return {
428
+ revisionId: revision.revisionId,
429
+ state: revision.state,
430
+ parent: revision.parent,
431
+ candidate: {
432
+ sha256: revision.candidate.sha256,
433
+ bytes: revision.candidate.bytes,
434
+ mediaType: revision.candidate.mediaType,
435
+ uploadedAt: revision.candidate.uploadedAt,
436
+ },
437
+ validation: revision.validation,
438
+ diff: {
439
+ kind: revision.diff.kind,
440
+ summary: revision.diff.summary,
441
+ truncated: revision.diff.truncated,
442
+ },
443
+ expiresAt: revision.expiresAt,
444
+ };
445
+ }
446
+ async function acceptArtifactRevisionOperation(input) {
447
+ const operator = input.ctx.operator;
448
+ const clientRequestId = input.clientRequestId?.trim();
449
+ if (!clientRequestId) {
450
+ return { status: 400, body: { ok: false, error: { code: "INVALID_INPUT", message: "clientRequestId is required" } } };
451
+ }
452
+ let candidateBytes;
453
+ try {
454
+ if (input.action === "artifactRevisionStage") {
455
+ candidateBytes = decodeBoundedBase64(input.actionParams.candidateBase64);
456
+ }
457
+ }
458
+ catch (error) {
459
+ return { status: 400, body: { ok: false, error: { code: "INVALID_INPUT", message: error instanceof Error ? error.message : String(error) } } };
460
+ }
461
+ const runId = typeof input.actionParams.runId === "string" ? input.actionParams.runId.trim() : "";
462
+ const nodeId = typeof input.actionParams.nodeId === "string" ? input.actionParams.nodeId.trim() : "";
463
+ const artifactId = typeof input.actionParams.artifactId === "string" ? input.actionParams.artifactId.trim() : "";
464
+ const revisionId = typeof input.actionParams.revisionId === "string" ? input.actionParams.revisionId.trim() : "";
465
+ const planHash = typeof input.actionParams.planHash === "string" ? input.actionParams.planHash.trim() : "";
466
+ const reason = typeof input.actionParams.reason === "string" ? input.actionParams.reason.trim() : "";
467
+ const mediaType = typeof input.actionParams.mediaType === "string" ? input.actionParams.mediaType.trim() : "";
468
+ if (!runId || !artifactId || (input.action === "artifactRevisionStage" && (!nodeId || !mediaType))) {
469
+ return { status: 400, body: { ok: false, error: { code: "INVALID_INPUT", message: "runId, artifactId, nodeId and mediaType are required" } } };
470
+ }
471
+ if (input.action !== "artifactRevisionStage" && !revisionId) {
472
+ return { status: 400, body: { ok: false, error: { code: "INVALID_INPUT", message: "revisionId is required" } } };
473
+ }
474
+ if (input.action === "artifactRevisionExecute" && (!planHash || !reason)) {
475
+ return { status: 400, body: { ok: false, error: { code: "INVALID_INPUT", message: "planHash and reason are required" } } };
476
+ }
477
+ let workerContinuation;
478
+ if (input.action !== "artifactRevisionStage") {
479
+ try {
480
+ workerContinuation = await resolveWorkerContinuationAuthority({
481
+ repoRoot: input.ctx.repoRoot,
482
+ parentDagRunId: runId,
483
+ revisionId,
484
+ allowActiveRevision: input.action === "artifactRevisionExecute",
485
+ });
486
+ }
487
+ catch (error) {
488
+ const message = error instanceof Error ? error.message : String(error);
489
+ return {
490
+ status: 422,
491
+ body: {
492
+ ok: false,
493
+ error: {
494
+ code: "WORKER_CONTINUATION_AUTHORITY_MISMATCH",
495
+ message,
496
+ },
497
+ },
498
+ };
499
+ }
500
+ }
501
+ const safeActionParams = {
502
+ runId,
503
+ artifactId,
504
+ ...(nodeId ? { nodeId } : {}),
505
+ ...(revisionId ? { revisionId } : {}),
506
+ ...(planHash ? { planHash } : {}),
507
+ ...(reason ? { reason } : {}),
508
+ ...(mediaType ? { mediaType } : {}),
509
+ ...(candidateBytes
510
+ ? { candidateSha256: createHash("sha256").update(candidateBytes).digest("hex"), candidateBytes: candidateBytes.byteLength }
511
+ : {}),
512
+ ...(workerContinuation
513
+ ? { workerContinuation: workerContinuation.authority }
514
+ : {}),
515
+ };
516
+ const appData = operator.actionContext.appData;
517
+ if (!appData.artifactRevisions) {
518
+ return { status: 503, body: { ok: false, error: { code: "INTERNAL_ERROR", message: "console artifact revision root unavailable" } } };
519
+ }
520
+ let cliArgs;
521
+ if (input.action === "artifactRevisionStage") {
522
+ const candidateSha256 = safeActionParams.candidateSha256;
523
+ cliArgs = [
524
+ "dag", "artifact", "revision", "stage",
525
+ "--run-id", runId,
526
+ "--node-id", nodeId,
527
+ "--artifact-id", artifactId,
528
+ "--candidate-upload", candidateSha256,
529
+ "--request-id", `console-${clientRequestId}`,
530
+ "--json",
531
+ ];
532
+ }
533
+ else if (input.action === "artifactRevisionPlan") {
534
+ cliArgs = [
535
+ "dag", "rerun",
536
+ "--run-id", runId,
537
+ "--artifact-revision", revisionId,
538
+ "--plan", "--json",
539
+ ];
540
+ }
541
+ else {
542
+ cliArgs = [
543
+ "dag", "rerun",
544
+ "--run-id", runId,
545
+ "--artifact-revision", revisionId,
546
+ "--plan-hash", planHash,
547
+ "--request-id", `console-${clientRequestId}`,
548
+ "--reason", reason,
549
+ "--json",
550
+ ];
551
+ }
552
+ if (workerContinuation) {
553
+ cliArgs.push("--worker-continuation", JSON.stringify(workerContinuation.authority));
554
+ }
555
+ let admittedOperationId;
556
+ const accepted = await acceptOperation(operator.actionContext, {
557
+ action: input.action,
558
+ actionParams: safeActionParams,
559
+ clientRequestId,
560
+ cliArgs,
561
+ ...(input.action === "artifactRevisionStage" ||
562
+ (input.action === "artifactRevisionExecute" && workerContinuation)
563
+ ? {
564
+ prepare: async () => {
565
+ if (input.action === "artifactRevisionStage") {
566
+ await writeArtifactRevisionUpload({
567
+ appData,
568
+ candidateSha256: safeActionParams.candidateSha256,
569
+ candidateBytes: candidateBytes,
570
+ });
571
+ }
572
+ else if (workerContinuation && admittedOperationId) {
573
+ await operator.actionContext.operations.update(admittedOperationId, { dagRunId: workerContinuation.authority.childWorkerRunId });
574
+ await beginWorkerContinuation({
575
+ repoRoot: input.ctx.repoRoot,
576
+ resolved: workerContinuation,
577
+ operationId: admittedOperationId,
578
+ client: operator.actionContext.client,
579
+ });
580
+ }
581
+ else {
582
+ throw new Error("Worker continuation operation identity is unavailable");
583
+ }
584
+ },
585
+ prepareFailure: (error) => {
586
+ const message = error instanceof Error ? error.message : String(error);
587
+ if (input.action === "artifactRevisionExecute") {
588
+ const conflict = message.includes("attempt-lease-conflict") || message.includes("state-conflict");
589
+ return {
590
+ status: conflict ? 409 : 422,
591
+ errorCode: conflict
592
+ ? "WORKER_CONTINUATION_LEASE_CONFLICT"
593
+ : "WORKER_CONTINUATION_AUTHORITY_MISMATCH",
594
+ body: {
595
+ ok: false,
596
+ error: {
597
+ code: conflict
598
+ ? "WORKER_CONTINUATION_LEASE_CONFLICT"
599
+ : "WORKER_CONTINUATION_AUTHORITY_MISMATCH",
600
+ message,
601
+ },
602
+ },
603
+ };
604
+ }
605
+ const stableCode = [
606
+ "ARTIFACT_REVISION_QUOTA_EXCEEDED",
607
+ "ARTIFACT_REVISION_DRIFT",
608
+ "ARTIFACT_REVISION_PATH_INVALID",
609
+ ].find((code) => message.startsWith(code));
610
+ void recordContextTransferEvent({
611
+ appData,
612
+ event: "artifact_revision.rejected",
613
+ fields: {
614
+ runId,
615
+ nodeId,
616
+ reasonCode: stableCode ?? "INVALID_INPUT",
617
+ },
618
+ }).catch(() => undefined);
619
+ return {
620
+ status: stableCode ? 422 : 400,
621
+ errorCode: stableCode ?? "INVALID_INPUT",
622
+ body: { ok: false, error: { code: stableCode ?? "INVALID_INPUT", message } },
623
+ };
624
+ },
625
+ }
626
+ : {}),
627
+ onAdmitted: (operation) => {
628
+ admittedOperationId = operation.operationId;
629
+ if (input.action === "artifactRevisionStage") {
630
+ return recordContextTransferEvent({
631
+ appData,
632
+ event: "artifact_revision.upload_started",
633
+ fields: {
634
+ runId,
635
+ nodeId,
636
+ operationId: operation.operationId,
637
+ },
638
+ }).catch(() => undefined);
639
+ }
640
+ if (input.action === "artifactRevisionExecute") {
641
+ return recordContextTransferEvent({
642
+ appData,
643
+ event: "artifact_rerun.execute_requested",
644
+ fields: {
645
+ runId,
646
+ revisionId,
647
+ operationId: operation.operationId,
648
+ },
649
+ }).catch(() => undefined);
650
+ }
651
+ return undefined;
652
+ },
653
+ ...(input.action === "artifactRevisionExecute" && workerContinuation
654
+ ? {
655
+ beforeTerminal: (operation, state, info) => settleWorkerContinuation({
656
+ repoRoot: input.ctx.repoRoot,
657
+ resolved: workerContinuation,
658
+ operation,
659
+ state,
660
+ info,
661
+ }),
662
+ }
663
+ : {}),
664
+ });
665
+ if (accepted.kind === "error") {
666
+ return { status: accepted.status, body: accepted.body };
667
+ }
668
+ return { status: accepted.status, body: accepted.body };
669
+ }
152
670
  async function handleCreateOperation(req, res, ctx) {
153
671
  if (!ctx.operator) {
154
672
  sendJson(res, 503, {
@@ -194,7 +712,7 @@ async function handleCreateOperation(req, res, ctx) {
194
712
  }
195
713
  let body;
196
714
  try {
197
- body = (await readJsonBody(req));
715
+ body = (await readJsonBody(req, 15 * 1024 * 1024));
198
716
  }
199
717
  catch (error) {
200
718
  sendJson(res, 400, {
@@ -221,6 +739,27 @@ async function handleCreateOperation(req, res, ctx) {
221
739
  });
222
740
  return;
223
741
  }
742
+ if (action === "contextExport") {
743
+ const accepted = await acceptContextExportOperation({
744
+ ctx,
745
+ actionParams,
746
+ clientRequestId,
747
+ });
748
+ sendJson(res, accepted.status, accepted.body);
749
+ return;
750
+ }
751
+ if (action === "artifactRevisionStage" ||
752
+ action === "artifactRevisionPlan" ||
753
+ action === "artifactRevisionExecute") {
754
+ const accepted = await acceptArtifactRevisionOperation({
755
+ ctx,
756
+ action,
757
+ actionParams,
758
+ clientRequestId,
759
+ });
760
+ sendJson(res, accepted.status, accepted.body);
761
+ return;
762
+ }
224
763
  const result = await dispatchOperatorAction(ctx.operator.actionContext, {
225
764
  action,
226
765
  actionParams,
@@ -364,6 +903,34 @@ async function handleOperationEvents(req, res, ctx, operationId) {
364
903
  * Path-escape-safe static file serve for Console SPA.
365
904
  * Sets boot capability cookie on HTML responses.
366
905
  */
906
+ /**
907
+ * Inject the general Console confirmation token into HTML, and additionally
908
+ * the scoped inspect mutation token when the experimental prompt-override
909
+ * switch is enabled and inspect credentials are available. The standalone
910
+ * /inspect/ document canonicalizes into the Console SPA (/#/inspect...), so
911
+ * the SPA must carry the scoped inspect token for the export/rerun tab to be
912
+ * rendered by the mounted inspect workspace.
913
+ */
914
+ function injectScopedInspectTokens(raw, ctx) {
915
+ let content = injectConfirmationTokenScript(raw, ctx.bootToken.confirmationToken);
916
+ if (isDagRerunPromptOverrideEnabled() && ctx.inspectBootToken) {
917
+ content = injectConfirmationTokenScript(content, ctx.inspectBootToken.confirmationToken, "__INSPECT_MUTATION_TOKEN__");
918
+ }
919
+ return content;
920
+ }
921
+ /**
922
+ * HTML response Set-Cookie headers for the Console SPA. The general boot
923
+ * cookie is always set; when the experimental prompt-override switch is
924
+ * enabled the scoped inspect cookie is set alongside so the hosted inspect
925
+ * workspace (mounted inside the SPA) can authenticate its allowlisted
926
+ * mutation requests.
927
+ */
928
+ function scopedSetCookieHeaders(ctx) {
929
+ if (isDagRerunPromptOverrideEnabled() && ctx.inspectBootToken) {
930
+ return [ctx.bootToken.setCookieHeader, ctx.inspectBootToken.setCookieHeader];
931
+ }
932
+ return ctx.bootToken.setCookieHeader;
933
+ }
367
934
  export async function serveConsoleStatic(req, res, ctx) {
368
935
  const url = new URL(req.url ?? "/", "http://localhost");
369
936
  let pathname = decodeURIComponent(url.pathname);
@@ -382,11 +949,14 @@ export async function serveConsoleStatic(req, res, ctx) {
382
949
  // SPA fallback for non-file routes
383
950
  const indexPath = path.join(resolvedStatic, "index.html");
384
951
  if (existsSync(indexPath) && !pathname.includes(".")) {
385
- const html = injectConfirmationTokenScript(await readFile(indexPath), ctx.bootToken.confirmationToken);
952
+ const html = injectScopedInspectTokens(await readFile(indexPath), ctx);
386
953
  res.writeHead(200, {
387
954
  "Content-Type": "text/html; charset=utf-8",
388
955
  "Content-Length": html.byteLength,
389
- "Set-Cookie": ctx.bootToken.setCookieHeader,
956
+ // Boot capability tokens are baked into the document; a cached
957
+ // page from a previous Console process makes every mutation 403.
958
+ "Cache-Control": "no-store",
959
+ "Set-Cookie": scopedSetCookieHeaders(ctx),
390
960
  });
391
961
  res.end(html);
392
962
  return;
@@ -397,14 +967,15 @@ export async function serveConsoleStatic(req, res, ctx) {
397
967
  const raw = await readFile(resolvedFile);
398
968
  const contentType = contentTypeFor(pathname);
399
969
  const content = contentType.startsWith("text/html")
400
- ? injectConfirmationTokenScript(raw, ctx.bootToken.confirmationToken)
970
+ ? injectScopedInspectTokens(raw, ctx)
401
971
  : raw;
402
972
  const headers = {
403
973
  "Content-Type": contentType,
404
974
  "Content-Length": content.byteLength,
405
975
  };
406
976
  if (contentType.startsWith("text/html")) {
407
- headers["Set-Cookie"] = ctx.bootToken.setCookieHeader;
977
+ headers["Cache-Control"] = "no-store";
978
+ headers["Set-Cookie"] = scopedSetCookieHeaders(ctx);
408
979
  }
409
980
  res.writeHead(200, headers);
410
981
  res.end(content);
@@ -462,6 +1033,95 @@ async function handleNightAuxTickerPost(req, res, ctx) {
462
1033
  const status = ticker.setEnabled(body.enabled);
463
1034
  sendJson(res, 200, { ok: true, ticker: status });
464
1035
  }
1036
+ async function handlePiPluginInstall(req, res) {
1037
+ let body;
1038
+ try {
1039
+ body = (await readJsonBody(req));
1040
+ }
1041
+ catch (error) {
1042
+ sendJson(res, 400, {
1043
+ ok: false,
1044
+ error: {
1045
+ code: "INVALID_INPUT",
1046
+ message: error instanceof Error ? error.message : String(error),
1047
+ },
1048
+ });
1049
+ return;
1050
+ }
1051
+ const name = typeof body.name === "string" ? body.name.trim() : "";
1052
+ if (!name) {
1053
+ sendJson(res, 400, {
1054
+ ok: false,
1055
+ error: { code: "INVALID_INPUT", message: "missing package name" },
1056
+ });
1057
+ return;
1058
+ }
1059
+ const version = typeof body.version === "string" ? body.version.trim() : undefined;
1060
+ try {
1061
+ const result = await installPiPlugin(name, version);
1062
+ if (!result.ok) {
1063
+ sendJson(res, 400, {
1064
+ ok: false,
1065
+ error: { code: "INSTALL_FAILED", message: result.error },
1066
+ });
1067
+ return;
1068
+ }
1069
+ sendJson(res, 200, { ok: true });
1070
+ }
1071
+ catch (error) {
1072
+ sendJson(res, 500, {
1073
+ ok: false,
1074
+ error: {
1075
+ code: "INTERNAL_ERROR",
1076
+ message: error instanceof Error ? error.message : String(error),
1077
+ },
1078
+ });
1079
+ }
1080
+ }
1081
+ async function handlePiPluginUninstall(req, res) {
1082
+ let body;
1083
+ try {
1084
+ body = (await readJsonBody(req));
1085
+ }
1086
+ catch (error) {
1087
+ sendJson(res, 400, {
1088
+ ok: false,
1089
+ error: {
1090
+ code: "INVALID_INPUT",
1091
+ message: error instanceof Error ? error.message : String(error),
1092
+ },
1093
+ });
1094
+ return;
1095
+ }
1096
+ const name = typeof body.name === "string" ? body.name.trim() : "";
1097
+ if (!name) {
1098
+ sendJson(res, 400, {
1099
+ ok: false,
1100
+ error: { code: "INVALID_INPUT", message: "missing package name" },
1101
+ });
1102
+ return;
1103
+ }
1104
+ try {
1105
+ const result = await uninstallPiPlugin(name);
1106
+ if (!result.ok) {
1107
+ sendJson(res, 400, {
1108
+ ok: false,
1109
+ error: { code: "UNINSTALL_FAILED", message: result.error },
1110
+ });
1111
+ return;
1112
+ }
1113
+ sendJson(res, 200, { ok: true });
1114
+ }
1115
+ catch (error) {
1116
+ sendJson(res, 500, {
1117
+ ok: false,
1118
+ error: {
1119
+ code: "INTERNAL_ERROR",
1120
+ message: error instanceof Error ? error.message : String(error),
1121
+ },
1122
+ });
1123
+ }
1124
+ }
465
1125
  export async function handleConsoleOperatorRequest(req, res, ctx) {
466
1126
  const method = (req.method ?? "GET").toUpperCase();
467
1127
  const url = new URL(req.url ?? "/", "http://localhost");
@@ -491,10 +1151,79 @@ export async function handleConsoleOperatorRequest(req, res, ctx) {
491
1151
  await handleListOperations(req, res, ctx);
492
1152
  return true;
493
1153
  }
1154
+ const contextDownloadMatch = pathname.match(/^\/api\/operator\/v1\/context-exports\/([a-f0-9]{32})\/download$/);
1155
+ if (method === "GET" && contextDownloadMatch) {
1156
+ if (!ctx.operator) {
1157
+ sendJson(res, 503, { ok: false, error: { code: "INTERNAL_ERROR", message: "operator runtime unavailable" } });
1158
+ return true;
1159
+ }
1160
+ const failure = evaluateConsoleDownloadCapability(req, ctx);
1161
+ if (failure) {
1162
+ sendJson(res, failure.status, { ok: false, error: { code: failure.code, message: failure.message } });
1163
+ return true;
1164
+ }
1165
+ try {
1166
+ const exportId = contextDownloadMatch[1];
1167
+ const leased = await acquireContextExportLease({
1168
+ appData: ctx.operator.actionContext.appData,
1169
+ exportId,
1170
+ });
1171
+ res.writeHead(200, {
1172
+ "Content-Type": "application/zip",
1173
+ "Content-Length": leased.bytes.byteLength,
1174
+ "Content-Disposition": `attachment; filename="context-${leased.record.bundleId}.zip"`,
1175
+ "Cache-Control": "no-store",
1176
+ "X-Content-Type-Options": "nosniff",
1177
+ });
1178
+ res.end(leased.bytes);
1179
+ await releaseContextExportLease({
1180
+ appData: ctx.operator.actionContext.appData,
1181
+ exportId,
1182
+ leaseId: leased.leaseId,
1183
+ }).catch(() => undefined);
1184
+ }
1185
+ catch (error) {
1186
+ if (res.headersSent)
1187
+ return true;
1188
+ sendJson(res, 404, {
1189
+ ok: false,
1190
+ error: { code: "CONTEXT_EXPORT_UNAVAILABLE", message: error instanceof Error ? error.message : String(error) },
1191
+ });
1192
+ }
1193
+ return true;
1194
+ }
494
1195
  if (method === "GET" && pathname === "/api/operator/v1/night-aux-ticker") {
495
1196
  await handleNightAuxTickerGet(req, res, ctx);
496
1197
  return true;
497
1198
  }
1199
+ if (method === "GET" && pathname === "/api/operator/v1/pi-plugins") {
1200
+ sendJson(res, 200, { ok: true, data: listPiPlugins() });
1201
+ return true;
1202
+ }
1203
+ if (method === "GET" &&
1204
+ pathname === "/api/operator/v1/pi-plugins/versions") {
1205
+ const name = url.searchParams.get("name")?.trim() ?? "";
1206
+ if (!name) {
1207
+ sendJson(res, 400, {
1208
+ ok: false,
1209
+ error: { code: "INVALID_INPUT", message: "missing name" },
1210
+ });
1211
+ return true;
1212
+ }
1213
+ const result = await listPiPluginVersions(name);
1214
+ if (!result.ok) {
1215
+ sendJson(res, 400, {
1216
+ ok: false,
1217
+ error: { code: "FETCH_FAILED", message: result.error },
1218
+ });
1219
+ return true;
1220
+ }
1221
+ sendJson(res, 200, {
1222
+ ok: true,
1223
+ data: { versions: result.versions, latest: result.latest },
1224
+ });
1225
+ return true;
1226
+ }
498
1227
  const opMatch = pathname.match(/^\/api\/operator\/v1\/operations\/([^/]+)(?:\/(events))?$/);
499
1228
  if (opMatch) {
500
1229
  const operationId = decodeURIComponent(opMatch[1]);
@@ -526,6 +1255,14 @@ export async function handleConsoleOperatorRequest(req, res, ctx) {
526
1255
  await handleNightAuxTickerPost(req, res, ctx);
527
1256
  return true;
528
1257
  }
1258
+ if (method === "POST" && pathname === "/api/operator/v1/pi-plugins/install") {
1259
+ await handlePiPluginInstall(req, res);
1260
+ return true;
1261
+ }
1262
+ if (method === "POST" && pathname === "/api/operator/v1/pi-plugins/uninstall") {
1263
+ await handlePiPluginUninstall(req, res);
1264
+ return true;
1265
+ }
529
1266
  // Allow other operator POSTs only under known routes; unknown → 404
530
1267
  sendJson(res, 404, {
531
1268
  ok: false,
@@ -587,9 +1324,11 @@ function pendingReadinessSummary(inspectReady) {
587
1324
  * POST /api/operator/v1/workspaces/pick — host native folder chooser
588
1325
  * POST /api/operator/v1/workspaces — body { "name": "<label>",
589
1326
  * "directoryPath": "<dir>" }
1327
+ * PATCH /api/operator/v1/workspaces/:workspaceId — body { "beforeWorkspaceId":
1328
+ * "<id>" | null }
590
1329
  * DELETE /api/operator/v1/workspaces/:workspaceId — soft remove (restore on re-add)
591
1330
  *
592
- * POST/DELETE are mutations and pass the Console mutation gate. GET carries no
1331
+ * POST/PATCH/DELETE are mutations and pass the Console mutation gate. GET carries no
593
1332
  * workspace selection parameter.
594
1333
  *
595
1334
  * AC-006 (2026-08-23): GET is NON-BLOCKING — registry + process-level
@@ -598,8 +1337,53 @@ function pendingReadinessSummary(inspectReady) {
598
1337
  * requests. A hanging probe can never delay the 200 list.
599
1338
  */
600
1339
  export async function handleWorkspaceRoutes(req, res, registry, deps, url, pathname, method, readinessDeps) {
1340
+ if (method === "GET" &&
1341
+ pathname === "/api/operator/v1/workspaces/events") {
1342
+ res.writeHead(200, {
1343
+ "Content-Type": "text/event-stream; charset=utf-8",
1344
+ "Cache-Control": "no-cache, no-transform",
1345
+ Connection: "keep-alive",
1346
+ "X-Accel-Buffering": "no",
1347
+ });
1348
+ const sendRevision = (event, revision) => {
1349
+ if (res.destroyed || res.writableEnded)
1350
+ return;
1351
+ res.write(`id: ${revision}\nevent: ${event}\ndata: ${JSON.stringify({ schemaVersion: 1, revision })}\n\n`);
1352
+ };
1353
+ let baselineSent = false;
1354
+ let pendingRevision = 0;
1355
+ const unsubscribe = registry.subscribe((revision) => {
1356
+ if (!baselineSent)
1357
+ pendingRevision = Math.max(pendingRevision, revision);
1358
+ else
1359
+ sendRevision("workspace_registry_changed", revision);
1360
+ });
1361
+ const initial = await registry.snapshotVisible();
1362
+ sendRevision("workspace_registry_ready", initial.revision);
1363
+ baselineSent = true;
1364
+ if (pendingRevision > initial.revision) {
1365
+ sendRevision("workspace_registry_changed", pendingRevision);
1366
+ }
1367
+ const heartbeat = setInterval(() => {
1368
+ if (!res.destroyed && !res.writableEnded)
1369
+ res.write(": keepalive\n\n");
1370
+ }, 15_000);
1371
+ heartbeat.unref();
1372
+ let closed = false;
1373
+ const close = () => {
1374
+ if (closed)
1375
+ return;
1376
+ closed = true;
1377
+ clearInterval(heartbeat);
1378
+ unsubscribe();
1379
+ };
1380
+ req.once("aborted", close);
1381
+ res.once("close", close);
1382
+ return;
1383
+ }
601
1384
  if (method === "GET" && pathname === "/api/operator/v1/workspaces") {
602
- const records = await registry.listVisible();
1385
+ const snapshot = await registry.snapshotVisible();
1386
+ const records = snapshot.workspaces;
603
1387
  const inspectReady = readinessDeps.inspectReady;
604
1388
  const readinessCache = readinessDeps.readinessCache ?? defaultWorkspaceReadinessCache;
605
1389
  const workspaces = records.map((record) => {
@@ -611,6 +1395,7 @@ export async function handleWorkspaceRoutes(req, res, registry, deps, url, pathn
611
1395
  repoFingerprint: record.repoFingerprint,
612
1396
  createdAt: record.createdAt,
613
1397
  lastAccessedAt: record.lastAccessedAt,
1398
+ sessionIds: record.sessionIds,
614
1399
  readiness: cached
615
1400
  ? {
616
1401
  state: cached.state,
@@ -622,7 +1407,8 @@ export async function handleWorkspaceRoutes(req, res, registry, deps, url, pathn
622
1407
  // Respond FIRST, then refresh readiness in the background (AC-006).
623
1408
  sendJson(res, 200, {
624
1409
  ok: true,
625
- schemaVersion: 1,
1410
+ schemaVersion: 2,
1411
+ revision: snapshot.revision,
626
1412
  workspaces,
627
1413
  });
628
1414
  void Promise.all(records.map(async (record) => {
@@ -789,8 +1575,73 @@ export async function handleWorkspaceRoutes(req, res, registry, deps, url, pathn
789
1575
  });
790
1576
  return;
791
1577
  }
792
- const removeMatch = pathname.match(/^\/api\/operator\/v1\/workspaces\/([^/]+)$/);
793
- if (method === "DELETE" && removeMatch) {
1578
+ const workspaceMatch = pathname.match(/^\/api\/operator\/v1\/workspaces\/([^/]+)$/);
1579
+ if (method === "PATCH" && workspaceMatch) {
1580
+ const failure = evaluateMutationGate(req, {
1581
+ cookieName: deps.bootToken.cookieName,
1582
+ bootToken: deps.bootToken.value,
1583
+ confirmationToken: deps.bootToken.confirmationToken,
1584
+ consoleOrigin: deps.getConsoleOrigin(),
1585
+ allowNonLoopbackAccess: deps.allowNonLoopbackAccess,
1586
+ });
1587
+ if (failure) {
1588
+ sendJson(res, failure.status, {
1589
+ ok: false,
1590
+ error: { code: failure.code, message: failure.message },
1591
+ });
1592
+ return;
1593
+ }
1594
+ let body;
1595
+ try {
1596
+ body = (await readJsonBody(req));
1597
+ }
1598
+ catch (error) {
1599
+ sendJson(res, 400, {
1600
+ ok: false,
1601
+ error: {
1602
+ code: "INVALID_INPUT",
1603
+ message: error instanceof Error ? error.message : String(error),
1604
+ },
1605
+ });
1606
+ return;
1607
+ }
1608
+ if (body.beforeWorkspaceId !== null &&
1609
+ typeof body.beforeWorkspaceId !== "string") {
1610
+ sendJson(res, 400, {
1611
+ ok: false,
1612
+ error: {
1613
+ code: "INVALID_INPUT",
1614
+ message: "beforeWorkspaceId must be a workspace id or null",
1615
+ },
1616
+ });
1617
+ return;
1618
+ }
1619
+ const workspaceId = decodeURIComponent(workspaceMatch[1]);
1620
+ const beforeWorkspaceId = typeof body.beforeWorkspaceId === "string"
1621
+ ? body.beforeWorkspaceId
1622
+ : null;
1623
+ const reordered = await registry.insertBefore(workspaceId, beforeWorkspaceId);
1624
+ if (!reordered.ok) {
1625
+ sendJson(res, 404, {
1626
+ ok: false,
1627
+ error: {
1628
+ code: reordered.code,
1629
+ message: reordered.code === "ANCHOR_NOT_FOUND"
1630
+ ? `unknown workspace anchor: ${beforeWorkspaceId}`
1631
+ : `unknown workspace: ${workspaceId}`,
1632
+ },
1633
+ });
1634
+ return;
1635
+ }
1636
+ sendJson(res, 200, {
1637
+ ok: true,
1638
+ workspaceId,
1639
+ beforeWorkspaceId,
1640
+ changed: reordered.changed,
1641
+ });
1642
+ return;
1643
+ }
1644
+ if (method === "DELETE" && workspaceMatch) {
794
1645
  const failure = evaluateMutationGate(req, {
795
1646
  cookieName: deps.bootToken.cookieName,
796
1647
  bootToken: deps.bootToken.value,
@@ -805,7 +1656,7 @@ export async function handleWorkspaceRoutes(req, res, registry, deps, url, pathn
805
1656
  });
806
1657
  return;
807
1658
  }
808
- const workspaceId = decodeURIComponent(removeMatch[1]);
1659
+ const workspaceId = decodeURIComponent(workspaceMatch[1]);
809
1660
  const removed = await registry.remove(workspaceId);
810
1661
  if (!removed) {
811
1662
  sendJson(res, 404, {