@tea-agent/loop-agent 0.39.0-beta.1 → 0.39.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -1
- package/CHANGELOG.md +248 -95
- package/README.md +9 -5
- package/bin/loop-agent.js +7 -3
- package/dist/application/task-lifecycle/advance.js +12 -0
- package/dist/application/task-lifecycle/recommendations.js +13 -2
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +23 -5
- package/dist/cli/program.js +21 -2
- package/dist/cli/update/notifier.js +2 -2
- package/dist/cli/update/runtime-activity.js +1 -29
- package/dist/cli.js +2 -1
- package/dist/commands/client-recovery.js +3 -0
- package/dist/commands/dag-follow-up.js +138 -0
- package/dist/commands/dag-rerun.js +55 -1
- package/dist/commands/init-model-catalog.js +464 -0
- package/dist/commands/init-upgrade.js +265 -97
- package/dist/commands/init.js +455 -28
- package/dist/commands/inspect-next.js +8 -0
- package/dist/commands/task-advance.js +19 -0
- package/dist/executors/dag-pi-executor.js +2306 -61
- package/dist/executors/pi-executor.js +7 -0
- package/dist/executors/pi-extension-resolver.js +233 -0
- package/dist/executors/pi-playwright-cli-tool.js +74 -28
- package/dist/executors/pi-read-budget-policy.js +239 -0
- package/dist/executors/pi-sdk-executor.js +180 -33
- package/dist/executors/shell-executor.js +1249 -224
- package/dist/executors/shell-write-guard.js +14 -0
- package/dist/executors/workspace-write-snapshot.js +68 -0
- package/dist/infrastructure/harness/atomic-write.js +23 -0
- package/dist/shared/dag-failure-category.js +150 -0
- package/dist/shared/dag-prompt-override.js +27 -0
- package/dist/shared/openspec-spec.js +70 -4
- package/dist/shared/operator/capabilities.js +39 -0
- package/dist/shared/playwright-cli-command-policy.js +15 -0
- package/dist/shared/update/console-notifier.js +100 -0
- package/dist/{cli → shared}/update/npm-client.js +70 -15
- package/dist/{cli → shared}/update/state.js +41 -13
- package/dist/task/config-types.js +50 -5
- package/dist/task/contract/apply.js +11 -1
- package/dist/task/contract/constants.js +2 -0
- package/dist/task/contract/hash.js +3 -0
- package/dist/task/contract/observe.js +25 -4
- package/dist/task/contract/paths.js +2 -1
- package/dist/task/contract/project.js +18 -1
- package/dist/task/contract/schema.js +4 -1
- package/dist/task/contract/transaction.js +12 -0
- package/dist/task/frontend-project-capability.js +199 -18
- package/dist/task/source-prepare/completeness.js +188 -0
- package/dist/task/source-prepare/fragment-inventory.js +477 -0
- package/dist/task/source-prepare/index.js +3 -0
- package/dist/task/source-prepare/ledger-reconciliation.js +127 -0
- package/dist/task/source-prepare/ledger-review.js +214 -0
- package/dist/task/source-prepare/ledger.js +545 -0
- package/dist/task/source-prepare/parse-intent.js +24 -4
- package/dist/task/source-prepare/prepare.js +298 -1
- package/dist/task/source-prepare/semantic-intake.js +25 -5
- package/dist/task/source-prepare/source-fidelity-pi.js +384 -0
- package/dist/task/source-prepare/types.js +22 -0
- package/dist/task/source-references.js +22 -1
- package/dist/worker/cli.js +18 -2
- package/dist/worker/console/chat/chat-event-store.js +62 -4
- package/dist/worker/console/chat/context-panel.js +7 -0
- package/dist/worker/console/chat/deferred-turn.js +12 -0
- package/dist/worker/console/chat/operation-card.js +8 -1
- package/dist/worker/console/chat/pi-console-config.js +81 -10
- package/dist/worker/console/chat/pi-runtime.js +264 -16
- package/dist/worker/console/chat/provider-error.js +98 -0
- package/dist/worker/console/chat/routes.js +298 -32
- package/dist/worker/console/chat/session-stats.js +179 -0
- package/dist/worker/console/chat/session-store.js +93 -63
- package/dist/worker/console/chat/todos.js +115 -0
- package/dist/worker/console/chat/tool-preview.js +42 -0
- package/dist/worker/console/chat/turn-process.js +20 -55
- package/dist/worker/console/chat/user-questions.js +266 -0
- package/dist/worker/console/chat/workspace-landing.js +14 -0
- package/dist/worker/console/console-handoff.js +82 -0
- package/dist/worker/console/console-update-and-init.js +115 -0
- package/dist/worker/console/console-update-runtime.js +84 -0
- package/dist/worker/console/dag-execution-receipt.js +33 -0
- package/dist/worker/console/draft-store.js +49 -0
- package/dist/worker/console/frontend-human-decision-adapter.js +19 -0
- package/dist/worker/console/frontend-split-operation-adapter.js +20 -0
- package/dist/worker/console/index.js +3 -0
- package/dist/worker/console/inspect-split.js +18 -0
- package/dist/worker/console/native-directory-picker.js +13 -0
- package/dist/worker/console/operation-run-facts.js +19 -3
- package/dist/worker/console/operation-store.js +227 -7
- package/dist/worker/console/operator-actions.js +388 -0
- package/dist/worker/console/operator-surface-health.js +1 -0
- package/dist/worker/console/operator-user-error.js +2 -2
- package/dist/worker/console/prd-intake-bridge.js +13 -0
- package/dist/worker/console/routes.js +355 -3
- package/dist/worker/console/security.js +67 -13
- package/dist/worker/console/server.js +609 -164
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4kVYweA.js → abnfDiagram-N423BO3Z-BAsjg7ew.js} +1 -1
- package/dist/worker/console/static/assets/{arc-Bj2M8iO5.js → arc-CyIlrwZw.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CLlXe4-6.js → architectureDiagram-T3A2C74G-Bo4tobhH.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-psEp5xH0.js → blockDiagram-VBNYF7ZC-Dfpow9gu.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DeKcOCGJ.js → c4Diagram-5PPSVZJV-DPmzG_B4.js} +1 -1
- package/dist/worker/console/static/assets/channel-DSrJAn0-.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-K0C744p1.js → chunk-2GRJ4B5K-pKQpB1UW.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-xbNw4J9-.js → chunk-2Q5K7J3B-5n9YYquZ.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLaSDUO.js → chunk-5RXB4S5H-DP3CtA2a.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CtiPlKel.js → chunk-5VM5RSS4-DYxMJPUy.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DZtolir7.js → chunk-6Q2QTUOP-CS3D50Io.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CPcdNeew.js → chunk-GF5L2VYU-gSdDfo5_.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-NgHc6V37.js → chunk-JWPE2WC7-A48q0ZNt.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DQ_T-hg8.js → chunk-KBJHAD2P-ow8Mm3DY.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DgLsxcVP.js → chunk-RYQCIY6F-CMKfLWYE.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-UrVoM9z1.js → chunk-XXDRQBXY-BiTBdQPX.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DeMmPl0_.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DeMmPl0_.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CLkYvTb3.js → cose-bilkent-JH36ORCC-Cm7exRz_.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BwT_xKrE.js → cynefin-VYW2F7L2-D9O5qGOu.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bi9tuzif.js → cynefinDiagram-MW4NZA55-CkvQnako.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BCIqkWBV.js → dagre-VZM6K2ZE-B1rzp7CB.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dip6l9_Z.js → diagram-7IWD3JNH-BP07Ipcc.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B-xkh_wK.js → diagram-B4RE2ZJO-BlPzweQb.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DZO0kTt_.js → diagram-LBJQPF4R-CxPa7n0S.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D6ZplNbZ.js → diagram-Q27KOJAE-CMTZs53A.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPekbhoS.js → diagram-UB23O5K3-CxG_X52r.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BtmaJpK5.js → ebnfDiagram-BXEA7PRR-D8axtJM0.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-6DS0Xc44.js → erDiagram-JOGREHBK-DIsGzn7b.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CihnROSm.js → flowDiagram-UKHOOZJN-CU9zkDG0.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CZC4zOE2.js → ganttDiagram-PKOTCBZU-DCjTmz5P.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js → gitGraphDiagram-DS77QQ5N-BksdmFzg.js} +1 -1
- package/dist/worker/console/static/assets/index-B-O5ChYq.css +1 -0
- package/dist/worker/console/static/assets/index-CfnY8AQx.js +407 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DlSl4BGx.js → infoDiagram-6WML65LV-CGzzzJtL.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-snnzFl_U.js → ishikawaDiagram-WSZJBQD7-BdseB80N.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BDE7qpMx.js → journeyDiagram-NVQOT4AX-DM9gsgha.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CD2Ci04G.js → kanban-definition-27J2QSJJ-3q-_9R2b.js} +1 -1
- package/dist/worker/console/static/assets/{linear-DdnapKIH.js → linear-B8N-Mxsd.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-BVjAT9b8.js → mermaid.core-EPIHUUsu.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-D0yJk-zA.js → mindmap-definition-FAOFIHXS-CxwO12aa.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BVo9tFP-.js → pegDiagram-VL7TDLO6-bPqbVX6Y.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DnOV-mZ_.js → pieDiagram-7S7Q4E2Y-KzqIvzeX.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CzUJo58i.js → quadrantDiagram-CIZ2JOQS-CyofpWiV.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Bvikrolh.js → railroadDiagram-AXF67PYL-cTWJvLTP.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DtaSVCap.js → requirementDiagram-LRYGKXZP-BK--q27B.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-C2c9A0wW.js → sankeyDiagram-W5VNT64P-C0SDOHXK.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DmXJcU7r.js → sequenceDiagram-SI44F4Z6-Cong6YIo.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4GUFW92.js → sizeCapture-X5ZJPWSS-DUVkgZx8.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Dsad2MXf.js → stateDiagram-OKZ733FA-C7XgxuZg.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Dzbnd5oX.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DGq48Fbi.js → swimlanes-SLNWSIFB-D9Ayvwcy.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BY0hHk6G.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CSSi6OFf.js → timeline-definition-Z64GVDOM-Byb000cH.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BqyzPrWv.js → vennDiagram-T6HMQDX7-DMLegPt4.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DlznVb6S.js → wardleyDiagram-T6FBY63Y-ChMMeOsP.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CfBcgJ_K.js → xychartDiagram-ELKLHX3M-bEMEPisW.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/active-run-badge.js +17 -0
- package/dist/worker/console/static-src/app/console-types.js +68 -4
- package/dist/worker/console/static-src/app/useConsoleShell.js +30 -12
- package/dist/worker/console/static-src/app/useOperatorActions.js +41 -6
- package/dist/worker/console/static-src/app/useRecoveryConsole.js +45 -10
- package/dist/worker/console/static-src/app/useRunProgress.js +47 -0
- package/dist/worker/console/static-src/app/useTaskWizard.js +74 -2
- package/dist/worker/console/static-src/night/useNightBoard.js +8 -5
- package/dist/worker/console/static-src/operator-chat/cards/failure-category-advice.js +25 -0
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +140 -8
- package/dist/worker/console/static-src/operator-chat/details-dag-actions.js +122 -0
- package/dist/worker/console/static-src/operator-chat/input-history.js +76 -0
- package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
- package/dist/worker/console/static-src/operator-chat/refs.js +3 -0
- package/dist/worker/console/static-src/operator-chat/runtime-selection-labels.js +27 -0
- package/dist/worker/console/static-src/operator-chat/turn-submission.js +16 -3
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +168 -72
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +102 -21
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +184 -4
- package/dist/worker/console/static-src/operator-chat/useComposer.js +32 -0
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +63 -9
- package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
- package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -3
- package/dist/worker/console/static-src/pages/tasks/run-id-resolution.js +79 -0
- package/dist/worker/console/static-src/pages/tasks/run-ownership-verify.js +23 -0
- package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +110 -0
- package/dist/worker/console/static-src/shell/useWorkspaces.js +299 -0
- package/dist/worker/console/static-src/shell/workspace-route.js +337 -0
- package/dist/worker/console/workspace-context.js +344 -0
- package/dist/worker/console/workspace-registry.js +214 -0
- package/dist/worker/loop-agent/loop-agent-client.js +7 -3
- package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
- package/dist/worker/materialize/harness-task-lineage.js +5 -2
- package/dist/worker/observability/init-runtime-activity.js +26 -0
- package/dist/worker/observability/read-model.js +22 -0
- package/dist/worker/observe/node-input.js +160 -3
- package/dist/worker/observe/node-process.js +377 -0
- package/dist/worker/observe/routes.js +224 -3
- package/dist/worker/observe/server.js +4 -0
- package/dist/worker/observe/shell-handler-keys.js +34 -0
- package/dist/worker/observe/static/api.js +90 -4
- package/dist/worker/observe/static/app.js +18 -70
- package/dist/worker/observe/static/constants.js +8 -1
- package/dist/worker/observe/static/dag-history-labels.js +4 -0
- package/dist/worker/observe/static/dag-node-purpose.d.ts +6 -0
- package/dist/worker/observe/static/dag-node-purpose.js +208 -0
- package/dist/worker/observe/static/format.js +60 -0
- package/dist/worker/observe/static/index.html +6 -1
- package/dist/worker/observe/static/inspect-workspace.js +307 -0
- package/dist/worker/observe/static/markdown-render.js +20 -1
- package/dist/worker/observe/static/operator-chrome.css +145 -20
- package/dist/worker/observe/static/operator-chrome.d.ts +20 -3
- package/dist/worker/observe/static/operator-chrome.js +330 -96
- package/dist/worker/observe/static/prompt-restart-candidates.d.ts +18 -0
- package/dist/worker/observe/static/prompt-restart-candidates.js +36 -0
- package/dist/worker/observe/static/router.d.ts +46 -0
- package/dist/worker/observe/static/router.js +25 -1
- package/dist/worker/observe/static/state.d.ts +50 -0
- package/dist/worker/observe/static/state.js +184 -1
- package/dist/worker/observe/static/styles.css +395 -0
- package/dist/worker/observe/static/views/dag-graph.js +4 -2
- package/dist/worker/observe/static/views/dag-inspector.js +1067 -9
- package/dist/worker/observe/static/views/dag.d.ts +13 -0
- package/dist/worker/observe/static/views/dag.js +14 -5
- package/dist/worker/observe/static/views/dashboard.js +6 -4
- package/dist/worker/observe/static/views/failures.js +5 -2
- package/dist/worker/observe/static/views/night.js +2 -2
- package/dist/worker/observe/static/views/pool.js +1 -0
- package/dist/worker/observe/static/views/run.js +3 -1
- package/dist/worker/observe/static/views/session-timeline.js +78 -9
- package/dist/worker/observe/static/views/task.js +56 -1
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +49 -3
- package/dist/workflows/dag/backend-test-markdown-workflow.js +176 -1
- package/dist/workflows/dag/backend-test-pytest-collection.js +98 -14
- package/dist/workflows/dag/backend-test-writer-completeness.js +10 -5
- package/dist/workflows/dag/contract-output-registry.js +15 -0
- package/dist/workflows/dag/contract-validator-registrations.js +1 -2
- package/dist/workflows/dag/dynamic-runtime/loop-until.js +1 -1
- package/dist/workflows/dag/dynamic-runtime/map.js +1 -1
- package/dist/workflows/dag/failure-category.js +7 -116
- package/dist/workflows/dag/frontend-closeout.js +221 -0
- package/dist/workflows/dag/frontend-design-policy.js +400 -0
- package/dist/workflows/dag/frontend-human-decision.js +182 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +538 -166
- package/dist/workflows/dag/frontend-plan-render.js +24 -0
- package/dist/workflows/dag/frontend-prewrite-gate.js +388 -392
- package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
- package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
- package/dist/workflows/dag/frontend-recovery-controller.js +202 -0
- package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
- package/dist/workflows/dag/frontend-recovery-plan.js +17 -10
- package/dist/workflows/dag/frontend-recovery-run.js +33 -20
- package/dist/workflows/dag/frontend-repair.js +7 -424
- package/dist/workflows/dag/frontend-review-context.js +288 -14
- package/dist/workflows/dag/frontend-review-findings.js +270 -0
- package/dist/workflows/dag/frontend-risk.js +15 -2
- package/dist/workflows/dag/frontend-shadow-dual-write.js +814 -0
- package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
- package/dist/workflows/dag/frontend-shape-facts.js +409 -0
- package/dist/workflows/dag/frontend-shape.js +427 -0
- package/dist/workflows/dag/frontend-source-fidelity-ledger.js +108 -0
- package/dist/workflows/dag/frontend-split-application-service.js +203 -0
- package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
- package/dist/workflows/dag/frontend-test-case-checklist.js +30 -4
- package/dist/workflows/dag/frontend-test-case-manifest.js +11 -4
- package/dist/workflows/dag/frontend-test-case-quality.js +11 -16
- package/dist/workflows/dag/frontend-test-environment-probe.js +230 -0
- package/dist/workflows/dag/frontend-test-html-report.js +3 -1
- package/dist/workflows/dag/frontend-test-l5-report.js +3 -1
- package/dist/workflows/dag/frontend-test-layout.js +159 -0
- package/dist/workflows/dag/frontend-test-markdown.js +61 -0
- package/dist/workflows/dag/frontend-test-result-contract.js +188 -42
- package/dist/workflows/dag/frontend-test-standard-scenarios.js +70 -0
- package/dist/workflows/dag/frontend-typed-event-store.js +452 -0
- package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
- package/dist/workflows/dag/frontend-verification-trace.js +52 -17
- package/dist/workflows/dag/frontend-worktree-diff.js +250 -17
- package/dist/workflows/dag/frontend-writer-admission.js +272 -0
- package/dist/workflows/dag/frontend-writer-status.js +244 -0
- package/dist/workflows/dag/init-hybrid.js +984 -613
- package/dist/workflows/dag/interrupt-request.js +10 -3
- package/dist/workflows/dag/lifecycle.js +3 -3
- package/dist/workflows/dag/node-execution.js +468 -58
- package/dist/workflows/dag/prompt.js +44 -19
- package/dist/workflows/dag/recovery-lease.js +80 -0
- package/dist/workflows/dag/report.js +37 -1
- package/dist/workflows/dag/rerun-feedback.js +124 -1
- package/dist/workflows/dag/rerun-plan.js +144 -5
- package/dist/workflows/dag/rerun-run.js +81 -6
- package/dist/workflows/dag/rerun-task.js +29 -0
- package/dist/workflows/dag/retry-policy.js +318 -8
- package/dist/workflows/dag/runner.js +429 -122
- package/dist/workflows/dag/scheduler.js +114 -16
- package/dist/workflows/dag/structured-output-repair.js +712 -0
- package/dist/workflows/dag/types.js +321 -31
- package/dist/workflows/dag/upstream-artifacts.js +0 -4
- package/dist/workflows/dag/validate.js +92 -32
- package/docs/README.md +2 -0
- package/docs/architecture/evolution.md +45 -0
- package/docs/init-surface.manifest.json +9 -0
- package/docs/operations/README.md +1 -1
- package/docs/operations/local-development-environment.md +21 -0
- package/docs/templates/README.md +2 -1
- package/docs/templates/agent-dag-report.schema.json +8 -2
- package/docs/templates/agent-dag.schema.json +45 -1
- package/docs/templates/frontend-implementation-contract.schema.json +684 -24
- package/docs/templates/frontend-test-case-checklist.md +2 -2
- package/docs/templates/frontend-test-dag.json +8 -10
- package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
- package/docs/templates/init-managed-agents.md +13 -3
- package/docs/templates/spec-registry.schema.json +45 -0
- package/harness.json +3 -3
- package/package.json +7 -1
- package/scripts/next-info.mjs +356 -0
- package/scripts/next-publish-gate.mjs +238 -0
- package/scripts/release-source-binding.mjs +251 -0
- package/skills/codebase-scout/SKILL.md +1 -1
- package/skills/fe-test-ui-scout/SKILL.md +65 -0
- package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
- package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
- package/skills/frontend-bounded-implement/SKILL.md +8 -14
- package/skills/frontend-design-review/SKILL.md +20 -41
- package/skills/frontend-implementation/SKILL.md +3 -4
- package/skills/frontend-implementation/references/design-spec.md +15 -10
- package/skills/frontend-implementation/references/node-contracts.md +23 -13
- package/skills/frontend-review/SKILL.md +22 -14
- package/skills/frontend-review/references/review-findings.md +6 -7
- package/skills/loop-agent/references/command-reference.md +12 -4
- package/skills/loop-agent/references/hybrid-dag.md +8 -1
- package/skills/playwright-cli/SKILL.md +23 -1
- package/skills/playwright-cli-case-generator/SKILL.md +29 -8
- package/dist/worker/console/static/assets/channel-CPF4N7pf.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C2DwA_Y1.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C2DwA_Y1.js +0 -1
- package/dist/worker/console/static/assets/index-DTZOKgAn.js +0 -404
- package/dist/worker/console/static/assets/index-Ya5FE7cD.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8FB_cNk.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CrZYaWfx.js +0 -8
|
@@ -1,31 +1,23 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
2
3
|
import { existsSync } from "node:fs";
|
|
3
4
|
import { createServer } from "node:http";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import {
|
|
7
|
+
import { readPackageVersion } from "../../shared/package-metadata.js";
|
|
7
8
|
import { DAG_ABSOLUTE_MAX_WALL_CLOCK_MS } from "../../shared/timeout-policy.js";
|
|
8
9
|
import { openConsoleAppData } from "./app-data.js";
|
|
9
|
-
import { DagConfirmationStore } from "./dag-confirmation.js";
|
|
10
|
-
import { DagExecutionReceiptStore } from "./dag-execution-receipt.js";
|
|
11
|
-
import { DraftStore } from "./draft-store.js";
|
|
12
|
-
import { AssessmentStore } from "./interview/assessment.js";
|
|
13
|
-
import { InterviewSessionStore } from "./interview/session.js";
|
|
14
10
|
import { isLoopbackHost } from "./loopback.js";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import { createOperationEventStore, } from "./operation-sse.js";
|
|
21
|
-
import { OperationStore } from "./operation-store.js";
|
|
22
|
-
import { probePiReadiness, } from "./pi-readiness.js";
|
|
23
|
-
import { handleConsoleHealthRequest, handleConsoleOperatorRequest, sendJson, serveConsoleStatic, } from "./routes.js";
|
|
24
|
-
import { issueBootCapabilityToken } from "./security.js";
|
|
25
|
-
import { resolveSiblingLoopAgentBin } from "./sibling-controller.js";
|
|
26
|
-
import { NightAuxTicker } from "./night-aux-ticker.js";
|
|
11
|
+
import { handleConsoleHealthRequest, handleWorkspaceRoutes, sendJson, serveConsoleStatic, } from "./routes.js";
|
|
12
|
+
import { issueBootCapabilityToken, injectConfirmationTokenScript, } from "./security.js";
|
|
13
|
+
import { resolvePackageRootFromConsole } from "./sibling-controller.js";
|
|
14
|
+
import { isDagRerunPromptOverrideEnabled } from "../../shared/dag-prompt-override.js";
|
|
15
|
+
import { defaultObserveStaticDir, handleMatchedObserveRoute, matchObserveGetRoute, ROUTES, serveObserveStatic, } from "../observe/routes.js";
|
|
27
16
|
import { deriveObserveRouteCapabilities } from "../observe/health.js";
|
|
28
|
-
import {
|
|
17
|
+
import { WorkspaceRegistry, } from "./workspace-registry.js";
|
|
18
|
+
import { createWorkspaceContext, } from "./workspace-context.js";
|
|
19
|
+
import { createConsoleUpdateExecutor } from "./console-update-runtime.js";
|
|
20
|
+
import { createConsoleHandoff } from "./console-handoff.js";
|
|
29
21
|
/**
|
|
30
22
|
* Default hard timeout for the Console-owned LoopAgentClient.
|
|
31
23
|
*
|
|
@@ -66,143 +58,161 @@ export async function createConsoleServer(options) {
|
|
|
66
58
|
process.stderr.write(`console: binding non-loopback host ${host} (Operator mutations enabled; no network auth — expose only on trusted networks)\n`);
|
|
67
59
|
}
|
|
68
60
|
const repoRoot = path.resolve(options.repoRoot);
|
|
69
|
-
const
|
|
61
|
+
const configuredPort = options.port ?? 8790;
|
|
70
62
|
const debug = options.debug === true;
|
|
71
63
|
const keepAliveTimeoutMs = options.keepAliveTimeoutMs ?? 65_000;
|
|
72
64
|
const staticDir = options.staticDir ?? defaultConsoleStaticDir();
|
|
73
65
|
const inspectStaticDir = options.inspectStaticDir ?? defaultObserveStaticDir();
|
|
74
|
-
const observeRouteContext = createObserveRouteContext(repoRoot, {
|
|
75
|
-
staticDir: inspectStaticDir,
|
|
76
|
-
});
|
|
77
|
-
const bootToken = issueBootCapabilityToken();
|
|
78
66
|
const operatorSessionId = `sess_${randomBytes(16).toString("hex")}`;
|
|
79
|
-
const
|
|
67
|
+
const consoleInstanceId = options.consoleInstanceId ?? `console_${randomBytes(16).toString("hex")}`;
|
|
68
|
+
// User-level registry root shares the user app-data root directory layout
|
|
69
|
+
// (workspaces.json lives at <root>/workspaces.json, per-workspace records
|
|
70
|
+
// carry fingerprint-isolated namespaces under <root>/repos/<fingerprint>/).
|
|
71
|
+
const bootstrapAppData = openConsoleAppData({
|
|
80
72
|
repoRoot,
|
|
81
73
|
appDataRoot: options.appDataRoot,
|
|
82
74
|
});
|
|
83
|
-
const
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const assessments = new AssessmentStore(appData);
|
|
92
|
-
const readiness = await probePiReadiness({
|
|
93
|
-
override: options.piReadinessProbe,
|
|
94
|
-
repoRoot,
|
|
75
|
+
const registry = new WorkspaceRegistry(bootstrapAppData);
|
|
76
|
+
const startupRecord = await registry.ensureStartupWorkspace(repoRoot);
|
|
77
|
+
const instanceNonce = configuredPort === 0 ? randomBytes(16).toString("hex") : undefined;
|
|
78
|
+
const bootToken = issueBootCapabilityToken({
|
|
79
|
+
repoFingerprint: startupRecord.repoFingerprint,
|
|
80
|
+
bindHost: host,
|
|
81
|
+
configuredPort,
|
|
82
|
+
...(instanceNonce ? { instanceNonce } : {}),
|
|
95
83
|
});
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
84
|
+
// Scoped credentials for the experimental /inspect prompt-override rerun:
|
|
85
|
+
// separate cookie name + confirmation token so the read-only inspect page
|
|
86
|
+
// can never replay them against the general Console mutation surface.
|
|
87
|
+
const inspectBootToken = issueBootCapabilityToken({
|
|
88
|
+
repoFingerprint: startupRecord.repoFingerprint,
|
|
89
|
+
bindHost: host,
|
|
90
|
+
configuredPort,
|
|
91
|
+
...(instanceNonce ? { instanceNonce } : {}),
|
|
92
|
+
}, { variant: "inspect" });
|
|
93
|
+
let boundHost = host;
|
|
94
|
+
let boundPort = configuredPort;
|
|
95
|
+
const getConsoleOrigin = () => `http://${boundHost}:${boundPort}`;
|
|
96
|
+
// ---- Per-workspace lazily-created contexts -------------------------------
|
|
97
|
+
const workspaceContexts = new Map();
|
|
98
|
+
let sharedConsoleUpdateExecutor = options.consoleUpdateExecutor;
|
|
99
|
+
const resolveContext = async (workspaceId) => {
|
|
100
|
+
const id = workspaceId?.trim();
|
|
101
|
+
if (!id) {
|
|
102
|
+
return {
|
|
103
|
+
ok: false,
|
|
104
|
+
status: 400,
|
|
105
|
+
code: "WORKSPACE_REQUIRED",
|
|
106
|
+
message: "workspaceId is required",
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
const existing = workspaceContexts.get(id);
|
|
110
|
+
if (existing)
|
|
111
|
+
return { ok: true, context: existing };
|
|
112
|
+
const record = await registry.getVisible(id);
|
|
113
|
+
if (!record) {
|
|
114
|
+
return {
|
|
115
|
+
ok: false,
|
|
116
|
+
status: 404,
|
|
117
|
+
code: "WORKSPACE_NOT_FOUND",
|
|
118
|
+
message: `unknown workspace: ${id}`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const context = await createWorkspaceContext(record, {
|
|
122
|
+
appDataRoot: options.appDataRoot,
|
|
123
|
+
staticDir,
|
|
124
|
+
inspectStaticDir,
|
|
125
|
+
bootToken,
|
|
126
|
+
operatorSessionId,
|
|
127
|
+
getConsoleOrigin,
|
|
128
|
+
allowNonLoopbackAccess: !loopbackBind,
|
|
129
|
+
observeBaseUrl: options.observeBaseUrl,
|
|
130
|
+
fetchImpl: options.fetchImpl,
|
|
131
|
+
piReadinessProbe: options.piReadinessProbe,
|
|
132
|
+
createClient: options.createClient,
|
|
133
|
+
defaultTimeoutMs: options.defaultTimeoutMs,
|
|
134
|
+
skipReconcile: options.skipReconcile,
|
|
106
135
|
});
|
|
107
|
-
|
|
108
|
-
|
|
136
|
+
if (sharedConsoleUpdateExecutor) {
|
|
137
|
+
context.actionContext.consoleUpdateExecutor = sharedConsoleUpdateExecutor;
|
|
138
|
+
}
|
|
139
|
+
workspaceContexts.set(id, context);
|
|
140
|
+
void context.recoverDeferredTurns().catch(() => undefined);
|
|
141
|
+
return { ok: true, context };
|
|
142
|
+
};
|
|
143
|
+
const startupResolution = await resolveContext(startupRecord.workspaceId);
|
|
144
|
+
if (!startupResolution.ok) {
|
|
145
|
+
throw new Error(`console serve: failed to create startup workspace context: ${startupResolution.message}`);
|
|
109
146
|
}
|
|
110
|
-
const
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
147
|
+
const startup = startupResolution.context;
|
|
148
|
+
const requestConsoleHandoff = async (targetVersion) => {
|
|
149
|
+
const packageRoot = resolvePackageRootFromConsole();
|
|
150
|
+
const agentWorkerBin = path.join(packageRoot, "bin", "agent-worker.js");
|
|
151
|
+
if (readPackageVersion(packageRoot) !== targetVersion ||
|
|
152
|
+
!existsSync(agentWorkerBin)) {
|
|
153
|
+
return {
|
|
154
|
+
ok: false,
|
|
155
|
+
message: "verified global package no longer contains the target Console worker",
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
const { handoff, env } = createConsoleHandoff({
|
|
159
|
+
oldPid: process.pid,
|
|
114
160
|
repoRoot,
|
|
161
|
+
host: boundHost,
|
|
162
|
+
port: boundPort,
|
|
163
|
+
...(options.appDataRoot ? { appDataRoot: options.appDataRoot } : {}),
|
|
164
|
+
targetVersion,
|
|
115
165
|
});
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const chatStore = new ChatSessionStore(appData);
|
|
146
|
-
// M5.1: every Console boot owns a fresh event-log generation. A browser
|
|
147
|
-
// reconnecting with an old SSE cursor must reconcile from snapshot rather
|
|
148
|
-
// than silently consume a different process' event namespace.
|
|
149
|
-
const chatEventEpoch = randomBytes(12).toString("hex");
|
|
150
|
-
const chatEvents = createChatEventStore({ appData, epoch: chatEventEpoch });
|
|
151
|
-
const chatTurnExecutions = createChatTurnExecutionRegistry();
|
|
152
|
-
const chatOperationLinker = createChatOperationLinker({
|
|
153
|
-
chatEvents,
|
|
154
|
-
operations,
|
|
155
|
-
operationEvents: events,
|
|
156
|
-
});
|
|
157
|
-
const chatRuntime = new ConsolePiRuntime({
|
|
158
|
-
cwd: repoRoot,
|
|
159
|
-
sessionDir: appData.chatSessions,
|
|
160
|
-
resourcePreferencesPath: appData.resourcePreferences,
|
|
161
|
-
actionContext,
|
|
162
|
-
});
|
|
163
|
-
// chatRouteDeps is a plain object literal — it cannot throw. The previous
|
|
164
|
-
// try/catch was dead code that created a false "handled failure" impression.
|
|
165
|
-
// If a future field can throw, scope it explicitly instead of wrapping the
|
|
166
|
-
// literal.
|
|
167
|
-
const chatRouteDeps = {
|
|
168
|
-
runtime: chatRuntime,
|
|
169
|
-
store: chatStore,
|
|
170
|
-
events: chatEvents,
|
|
171
|
-
turnExecutions: chatTurnExecutions,
|
|
172
|
-
operationLinker: chatOperationLinker,
|
|
173
|
-
appData,
|
|
174
|
-
bootToken,
|
|
175
|
-
getConsoleOrigin,
|
|
176
|
-
allowNonLoopbackAccess: !loopbackBind,
|
|
166
|
+
try {
|
|
167
|
+
const child = spawn(process.execPath, [
|
|
168
|
+
agentWorkerBin,
|
|
169
|
+
"console",
|
|
170
|
+
"--repo",
|
|
171
|
+
repoRoot,
|
|
172
|
+
"--host",
|
|
173
|
+
boundHost,
|
|
174
|
+
"--port",
|
|
175
|
+
String(boundPort),
|
|
176
|
+
...(options.appDataRoot
|
|
177
|
+
? ["--app-data", options.appDataRoot]
|
|
178
|
+
: []),
|
|
179
|
+
"--no-open",
|
|
180
|
+
], { detached: true, stdio: "ignore", env: { ...process.env, ...env } });
|
|
181
|
+
await new Promise((resolve, reject) => {
|
|
182
|
+
child.once("spawn", resolve);
|
|
183
|
+
child.once("error", reject);
|
|
184
|
+
});
|
|
185
|
+
child.unref();
|
|
186
|
+
setTimeout(() => process.kill(process.pid, "SIGTERM"), 100).unref();
|
|
187
|
+
return { ok: true, consoleInstanceId: handoff.consoleInstanceId };
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
return {
|
|
191
|
+
ok: false,
|
|
192
|
+
message: error instanceof Error ? error.message : String(error),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
177
195
|
};
|
|
178
|
-
|
|
179
|
-
// when no browser currently has the Session open.
|
|
180
|
-
void recoverDeferredTurns(chatRouteDeps).catch((error) => {
|
|
181
|
-
console.error(`[console] deferred Turn recovery failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
182
|
-
});
|
|
183
|
-
const nightAuxTicker = new NightAuxTicker({
|
|
196
|
+
sharedConsoleUpdateExecutor ??= createConsoleUpdateExecutor({
|
|
184
197
|
repoRoot,
|
|
185
|
-
|
|
198
|
+
packageRoot: resolvePackageRootFromConsole(),
|
|
199
|
+
client: startup.client,
|
|
200
|
+
operations: startup.operations,
|
|
201
|
+
activeChatTurnCount: () => [...workspaceContexts.values()].reduce((total, context) => total + (context.chatRouteDeps.turnExecutions?.size() ?? 0), 0),
|
|
202
|
+
requestHandoff: requestConsoleHandoff,
|
|
186
203
|
});
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
operations,
|
|
200
|
-
events,
|
|
201
|
-
getReadiness,
|
|
202
|
-
chat: chatRouteDeps,
|
|
203
|
-
nightAuxTicker,
|
|
204
|
-
},
|
|
205
|
-
};
|
|
204
|
+
for (const context of workspaceContexts.values()) {
|
|
205
|
+
context.actionContext.consoleUpdateExecutor = sharedConsoleUpdateExecutor;
|
|
206
|
+
}
|
|
207
|
+
// Legacy surface readiness: the boot readiness report of the startup
|
|
208
|
+
// workspace. Kept as a process-level report for /api/health projections.
|
|
209
|
+
const bootReadiness = startup.getReadiness();
|
|
210
|
+
// Durable follow-up recovery is per-context and runs at context creation
|
|
211
|
+
// (see resolveContext); the startup context is created eagerly below, so its
|
|
212
|
+
// recovery has already been scheduled.
|
|
213
|
+
// Readiness is shared per-context; the legacy getReadiness() helper keeps
|
|
214
|
+
// pointing at the startup context report.
|
|
215
|
+
const getReadiness = () => bootReadiness;
|
|
206
216
|
return new Promise((resolve, reject) => {
|
|
207
217
|
const server = createServer((req, res) => {
|
|
208
218
|
const startedAt = Date.now();
|
|
@@ -250,35 +260,92 @@ export async function createConsoleServer(options) {
|
|
|
250
260
|
}
|
|
251
261
|
});
|
|
252
262
|
async function dispatch(req, res) {
|
|
253
|
-
// Refresh readiness pointer on each request surface
|
|
254
|
-
actionContext.readiness = getReadiness();
|
|
255
263
|
const method = (req.method ?? "GET").toUpperCase();
|
|
256
|
-
const
|
|
264
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
265
|
+
const pathname = decodeURIComponent(url.pathname);
|
|
257
266
|
if (method === "GET" && pathname === "/api/health") {
|
|
258
|
-
await handleConsoleHealthRequest(req, res,
|
|
267
|
+
await handleConsoleHealthRequest(req, res, {
|
|
268
|
+
repoRoot,
|
|
269
|
+
staticDir,
|
|
270
|
+
bootToken,
|
|
271
|
+
getConsoleOrigin,
|
|
272
|
+
allowNonLoopbackAccess: !loopbackBind,
|
|
273
|
+
inspect: {
|
|
274
|
+
ready: existsSync(path.join(inspectStaticDir, "index.html")),
|
|
275
|
+
routeCapabilities: deriveObserveRouteCapabilities(ROUTES),
|
|
276
|
+
},
|
|
277
|
+
consoleInstanceId,
|
|
278
|
+
registry,
|
|
279
|
+
getStartupReadiness: getReadiness,
|
|
280
|
+
});
|
|
259
281
|
return;
|
|
260
282
|
}
|
|
261
283
|
if (pathname.startsWith("/api/operator/v1/") ||
|
|
262
284
|
pathname.startsWith("/api/session/")) {
|
|
263
|
-
//
|
|
285
|
+
// Workspace Registry routes are process-level (no workspaceId).
|
|
286
|
+
if (pathname === "/api/operator/v1/workspaces" ||
|
|
287
|
+
pathname.startsWith("/api/operator/v1/workspaces/")) {
|
|
288
|
+
await handleWorkspaceRoutes(req, res, registry, {
|
|
289
|
+
bootToken,
|
|
290
|
+
getConsoleOrigin,
|
|
291
|
+
allowNonLoopbackAccess: !loopbackBind,
|
|
292
|
+
...(options.piReadinessProbe
|
|
293
|
+
? { piReadinessProbe: options.piReadinessProbe }
|
|
294
|
+
: {}),
|
|
295
|
+
...(options.pickDirectory
|
|
296
|
+
? { pickDirectory: options.pickDirectory }
|
|
297
|
+
: {}),
|
|
298
|
+
}, url, pathname, method, {
|
|
299
|
+
inspectReady: existsSync(path.join(inspectStaticDir, "index.html")),
|
|
300
|
+
});
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
264
303
|
if (pathname.startsWith("/api/operator/v1/chat/") ||
|
|
265
304
|
pathname.startsWith("/api/operator/v1/pi/")) {
|
|
266
|
-
await
|
|
305
|
+
await handleChatDispatch(req, res, pathname, url, method);
|
|
267
306
|
return;
|
|
268
307
|
}
|
|
269
|
-
await
|
|
308
|
+
await handleOperatorDispatch(req, res, pathname, url, method);
|
|
270
309
|
return;
|
|
271
310
|
}
|
|
272
311
|
if (method === "GET" && pathname.startsWith("/api/")) {
|
|
273
312
|
const match = matchObserveGetRoute(pathname);
|
|
274
313
|
if (match) {
|
|
275
|
-
|
|
314
|
+
const requestedWorkspaceId = url.searchParams.get("workspaceId")?.trim() || undefined;
|
|
315
|
+
// Every Observe GET is workspace data. Missing, blank, and unknown
|
|
316
|
+
// workspace ids fail closed; only /api/health remains process-level.
|
|
317
|
+
const resolution = await resolveContext(requestedWorkspaceId);
|
|
318
|
+
if (!resolution.ok) {
|
|
319
|
+
sendJson(res, resolution.status, {
|
|
320
|
+
ok: false,
|
|
321
|
+
error: {
|
|
322
|
+
code: resolution.code,
|
|
323
|
+
message: resolution.message,
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
// Refresh readiness pointer per request surface.
|
|
329
|
+
resolution.context.actionContext.readiness =
|
|
330
|
+
resolution.context.getReadiness();
|
|
331
|
+
await handleMatchedObserveRoute(req, res, match, resolution.context.observeRouteContext);
|
|
276
332
|
return;
|
|
277
333
|
}
|
|
278
334
|
}
|
|
279
335
|
if (pathname.startsWith("/inspect")) {
|
|
336
|
+
// Prompt-override rerun is enabled by default. Unless the emergency
|
|
337
|
+
// kill switch disables it, hand the inspect page SCOPED mutation
|
|
338
|
+
// credentials (separate cookie + token, only strong enough for
|
|
339
|
+
// INSPECT_MUTATION_ALLOWED_ACTIONS — see routes.ts).
|
|
340
|
+
const htmlEnhancer = isDagRerunPromptOverrideEnabled()
|
|
341
|
+
? (html) => ({
|
|
342
|
+
content: injectConfirmationTokenScript(html, inspectBootToken.confirmationToken, "__INSPECT_MUTATION_TOKEN__"),
|
|
343
|
+
setCookie: inspectBootToken.setCookieHeader,
|
|
344
|
+
})
|
|
345
|
+
: undefined;
|
|
280
346
|
await serveObserveStatic(req, res, inspectStaticDir, {
|
|
281
347
|
urlPrefix: "/inspect",
|
|
348
|
+
...(htmlEnhancer ? { htmlEnhancer } : {}),
|
|
282
349
|
});
|
|
283
350
|
return;
|
|
284
351
|
}
|
|
@@ -286,10 +353,283 @@ export async function createConsoleServer(options) {
|
|
|
286
353
|
sendJson(res, 404, { error: "Not found" });
|
|
287
354
|
return;
|
|
288
355
|
}
|
|
289
|
-
|
|
356
|
+
if (options.serveStatic) {
|
|
357
|
+
await options.serveStatic(req, res, {
|
|
358
|
+
confirmationToken: bootToken.confirmationToken,
|
|
359
|
+
setCookieHeader: bootToken.setCookieHeader,
|
|
360
|
+
consoleOrigin: getConsoleOrigin(),
|
|
361
|
+
});
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
await serveConsoleStatic(req, res, {
|
|
365
|
+
repoRoot,
|
|
366
|
+
staticDir,
|
|
367
|
+
bootToken,
|
|
368
|
+
inspectBootToken,
|
|
369
|
+
getConsoleOrigin,
|
|
370
|
+
allowNonLoopbackAccess: !loopbackBind,
|
|
371
|
+
inspect: {
|
|
372
|
+
ready: existsSync(path.join(inspectStaticDir, "index.html")),
|
|
373
|
+
routeCapabilities: deriveObserveRouteCapabilities(ROUTES),
|
|
374
|
+
},
|
|
375
|
+
consoleInstanceId,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
async function handleChatDispatch(req, res, pathname, url, method,
|
|
379
|
+
/** Explicit owning workspaceId for POST /chat/sessions create
|
|
380
|
+
* (AC-R7-002); undefined for every other chat route. */
|
|
381
|
+
workspaceScope) {
|
|
382
|
+
// POST /api/operator/v1/chat/sessions — creation carries workspaceId in
|
|
383
|
+
// the JSON body; all other chat routes resolve via Session ownership
|
|
384
|
+
// (durable) with optional query workspaceId conflict check.
|
|
385
|
+
const { handleChatRequest: dispatchChat } = await import("./chat/routes.js");
|
|
386
|
+
if (method === "POST" && pathname === "/api/operator/v1/chat/sessions") {
|
|
387
|
+
// AC-R5-006: the mutation gate fires BEFORE body pre-read/workspace
|
|
388
|
+
// resolution — an unauthenticated mutation gets exactly 403, never a
|
|
389
|
+
// masked 400 (WORKSPACE_REQUIRED/INVALID_INPUT) that would hide the
|
|
390
|
+
// gate-order regression.
|
|
391
|
+
{
|
|
392
|
+
const { evaluateMutationGate } = await import("./security.js");
|
|
393
|
+
const failure = evaluateMutationGate(req, {
|
|
394
|
+
cookieName: bootToken.cookieName,
|
|
395
|
+
bootToken: bootToken.value,
|
|
396
|
+
confirmationToken: bootToken.confirmationToken,
|
|
397
|
+
consoleOrigin: getConsoleOrigin(),
|
|
398
|
+
allowNonLoopbackAccess: !loopbackBind,
|
|
399
|
+
});
|
|
400
|
+
if (failure) {
|
|
401
|
+
sendJson(res, failure.status, {
|
|
402
|
+
ok: false,
|
|
403
|
+
error: {
|
|
404
|
+
code: failure.code,
|
|
405
|
+
message: failure.message,
|
|
406
|
+
},
|
|
407
|
+
});
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
const resolution = await resolveContextFromSessionCreateBody(req, res, resolveContext);
|
|
412
|
+
if (!resolution)
|
|
413
|
+
return; // response already sent
|
|
414
|
+
// AC-R7-002: workspaceId travels as an EXPLICIT parameter through
|
|
415
|
+
// dispatchChat → handleCreateChatSession → store.create. No runtime
|
|
416
|
+
// store.create replacement exists anymore.
|
|
417
|
+
await dispatchChat(req, res, resolution.context.chatRouteDeps, pathname, resolution.context.workspaceId);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
const sessionMatch = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)(?:\/|$)/);
|
|
421
|
+
if (sessionMatch) {
|
|
422
|
+
const sessionId = decodeURIComponent(sessionMatch[1]);
|
|
423
|
+
// r4 AC-R2-001: the mutation gate fires BEFORE session ownership
|
|
424
|
+
// resolution — an unauthenticated mutation on ANY session id
|
|
425
|
+
// (existing or not) gets exactly 403, never a masked 404 that
|
|
426
|
+
// would hide a gate-order regression.
|
|
427
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
428
|
+
const { evaluateMutationGate } = await import("./security.js");
|
|
429
|
+
const failure = evaluateMutationGate(req, {
|
|
430
|
+
cookieName: bootToken.cookieName,
|
|
431
|
+
bootToken: bootToken.value,
|
|
432
|
+
confirmationToken: bootToken.confirmationToken,
|
|
433
|
+
consoleOrigin: getConsoleOrigin(),
|
|
434
|
+
allowNonLoopbackAccess: !loopbackBind,
|
|
435
|
+
});
|
|
436
|
+
if (failure) {
|
|
437
|
+
sendJson(res, failure.status, {
|
|
438
|
+
ok: false,
|
|
439
|
+
error: {
|
|
440
|
+
code: failure.code,
|
|
441
|
+
message: failure.message,
|
|
442
|
+
},
|
|
443
|
+
});
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
const requestedId = url.searchParams.get("workspaceId")?.trim();
|
|
448
|
+
let owner;
|
|
449
|
+
if (requestedId) {
|
|
450
|
+
// The current UI always sends its selected workspace. Resolve that one
|
|
451
|
+
// context directly: scanning every historical registry entry first can
|
|
452
|
+
// fail on an old, deleted temp directory before reaching the real owner.
|
|
453
|
+
const resolution = await resolveContext(requestedId);
|
|
454
|
+
if (!resolution.ok) {
|
|
455
|
+
// A session-scoped workspace hint is a conflict boundary. Unknown
|
|
456
|
+
// workspace ids must not reveal whether the session exists elsewhere.
|
|
457
|
+
const workspaceConflict = resolution.code === "WORKSPACE_NOT_FOUND";
|
|
458
|
+
sendJson(res, workspaceConflict ? 403 : resolution.status, {
|
|
459
|
+
ok: false,
|
|
460
|
+
error: workspaceConflict
|
|
461
|
+
? {
|
|
462
|
+
code: "WORKSPACE_CONFLICT",
|
|
463
|
+
message: "workspaceId does not match the session's owning workspace",
|
|
464
|
+
}
|
|
465
|
+
: { code: resolution.code, message: resolution.message },
|
|
466
|
+
});
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
const stored = await resolution.context.chatRouteDeps.store.get(sessionId);
|
|
470
|
+
if (stored)
|
|
471
|
+
owner = resolution.context;
|
|
472
|
+
else {
|
|
473
|
+
// Do not scan another workspace and disclose whether the id exists there.
|
|
474
|
+
sendJson(res, 403, {
|
|
475
|
+
ok: false,
|
|
476
|
+
error: {
|
|
477
|
+
code: "WORKSPACE_CONFLICT",
|
|
478
|
+
message: "workspaceId does not match the session's owning workspace",
|
|
479
|
+
},
|
|
480
|
+
});
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
else {
|
|
485
|
+
// Legacy callers may omit workspaceId. Session ids live in per-workspace
|
|
486
|
+
// namespaces, so scan visible workspaces but tolerate stale registry
|
|
487
|
+
// records whose directories were removed outside the Console.
|
|
488
|
+
const visible = await registry.listVisible();
|
|
489
|
+
for (const record of visible) {
|
|
490
|
+
try {
|
|
491
|
+
const resolution = await resolveContext(record.workspaceId);
|
|
492
|
+
if (!resolution.ok)
|
|
493
|
+
continue;
|
|
494
|
+
const stored = await resolution.context.chatRouteDeps.store.get(sessionId);
|
|
495
|
+
if (stored) {
|
|
496
|
+
owner = resolution.context;
|
|
497
|
+
break;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
catch {
|
|
501
|
+
// An unavailable historical Workspace is not evidence that the
|
|
502
|
+
// requested session is missing from every other visible Workspace.
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
if (!owner) {
|
|
507
|
+
sendJson(res, 404, {
|
|
508
|
+
ok: false,
|
|
509
|
+
error: {
|
|
510
|
+
code: "NOT_FOUND",
|
|
511
|
+
message: `chat session not found: ${sessionId}`,
|
|
512
|
+
},
|
|
513
|
+
});
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
owner.actionContext.readiness = owner.getReadiness();
|
|
517
|
+
// Session-scoped routes: ownership is durable on the record; no explicit
|
|
518
|
+
// workspace scope is needed (it lives in store record + query conflict check).
|
|
519
|
+
await dispatchChat(req, res, owner.chatRouteDeps, pathname, undefined);
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
// Non-session chat surface (capabilities, repo browser) resolves from
|
|
523
|
+
// query workspaceId; fail closed when missing.
|
|
524
|
+
const workspaceId = url.searchParams.get("workspaceId") ?? undefined;
|
|
525
|
+
const resolution = await resolveContext(workspaceId);
|
|
526
|
+
if (!resolution.ok) {
|
|
527
|
+
sendJson(res, resolution.status, {
|
|
528
|
+
ok: false,
|
|
529
|
+
error: { code: resolution.code, message: resolution.message },
|
|
530
|
+
});
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
resolution.context.actionContext.readiness =
|
|
534
|
+
resolution.context.getReadiness();
|
|
535
|
+
await dispatchChat(req, res, resolution.context.chatRouteDeps, pathname, undefined);
|
|
536
|
+
}
|
|
537
|
+
async function handleOperatorDispatch(req, res, pathname, url, method) {
|
|
538
|
+
const { handleConsoleOperatorRequest } = await import("./routes.js");
|
|
539
|
+
// POST /api/operator/v1/operations creation carries workspaceId in the
|
|
540
|
+
// JSON body. Operation reads carry ?workspaceId=. Non-workspace routes
|
|
541
|
+
// (readiness, recovery, session ping, night-aux-ticker) use the startup
|
|
542
|
+
// context for backward compatibility.
|
|
543
|
+
if (method === "POST" && pathname === "/api/operator/v1/operations") {
|
|
544
|
+
const resolution = await resolveContextFromOperationCreateBody(req, res, resolveContext);
|
|
545
|
+
if (!resolution)
|
|
546
|
+
return; // response already sent
|
|
547
|
+
await handleConsoleOperatorRequest(req, res, operatorRouteContext(resolution.context));
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
const opMatch = pathname.match(/^\/api\/operator\/v1\/operations\/([^/]+)(?:\/(events))?$/);
|
|
551
|
+
if ((method === "GET" && pathname === "/api/operator/v1/operations") ||
|
|
552
|
+
opMatch) {
|
|
553
|
+
const workspaceId = url.searchParams.get("workspaceId") ?? undefined;
|
|
554
|
+
const resolution = await resolveContext(workspaceId);
|
|
555
|
+
if (!resolution.ok) {
|
|
556
|
+
sendJson(res, resolution.status, {
|
|
557
|
+
ok: false,
|
|
558
|
+
error: { code: resolution.code, message: resolution.message },
|
|
559
|
+
});
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
resolution.context.actionContext.readiness =
|
|
563
|
+
resolution.context.getReadiness();
|
|
564
|
+
await handleConsoleOperatorRequest(req, res, operatorRouteContext(resolution.context));
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
// Multi-workspace read-scope (2026-08-26, AC-005): every remaining
|
|
568
|
+
// operator read route (readiness, recovery candidates, night-aux-ticker
|
|
569
|
+
// GET and the ticker POST mutation) requires an EXPLICIT workspaceId;
|
|
570
|
+
// missing/unknown ids fail closed with the shared error contract. No
|
|
571
|
+
// route silently falls back to the startup workspace anymore. Only
|
|
572
|
+
// /api/session/ping stays process-level (mutation-gate probe stub).
|
|
573
|
+
if ((method === "GET" &&
|
|
574
|
+
(pathname === "/api/operator/v1/readiness" ||
|
|
575
|
+
pathname === "/api/operator/v1/recovery/candidates" ||
|
|
576
|
+
pathname === "/api/operator/v1/night-aux-ticker")) ||
|
|
577
|
+
(method === "POST" &&
|
|
578
|
+
pathname === "/api/operator/v1/night-aux-ticker")) {
|
|
579
|
+
const workspaceId = url.searchParams.get("workspaceId") ?? undefined;
|
|
580
|
+
const resolution = await resolveContext(workspaceId);
|
|
581
|
+
if (!resolution.ok) {
|
|
582
|
+
sendJson(res, resolution.status, {
|
|
583
|
+
ok: false,
|
|
584
|
+
error: { code: resolution.code, message: resolution.message },
|
|
585
|
+
});
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
resolution.context.actionContext.readiness =
|
|
589
|
+
resolution.context.getReadiness();
|
|
590
|
+
await handleConsoleOperatorRequest(req, res, operatorRouteContext(resolution.context));
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
// /api/session/ping stays process-level (mutation-gate probe stub).
|
|
594
|
+
if (pathname === "/api/session/ping") {
|
|
595
|
+
await handleConsoleOperatorRequest(req, res, operatorRouteContext(startup));
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
// Unknown operator route: fail closed (never a startup-workspace proxy).
|
|
599
|
+
sendJson(res, 404, {
|
|
600
|
+
ok: false,
|
|
601
|
+
error: {
|
|
602
|
+
code: "NOT_FOUND",
|
|
603
|
+
message: `unknown operator route: ${pathname}`,
|
|
604
|
+
},
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
function operatorRouteContext(context) {
|
|
608
|
+
return {
|
|
609
|
+
repoRoot: context.repoRoot,
|
|
610
|
+
staticDir,
|
|
611
|
+
bootToken,
|
|
612
|
+
inspectBootToken,
|
|
613
|
+
getConsoleOrigin,
|
|
614
|
+
allowNonLoopbackAccess: !loopbackBind,
|
|
615
|
+
inspect: {
|
|
616
|
+
ready: existsSync(path.join(inspectStaticDir, "index.html")),
|
|
617
|
+
routeCapabilities: deriveObserveRouteCapabilities(ROUTES),
|
|
618
|
+
},
|
|
619
|
+
consoleInstanceId,
|
|
620
|
+
operator: {
|
|
621
|
+
actionContext: context.actionContext,
|
|
622
|
+
operations: context.operations,
|
|
623
|
+
events: context.events,
|
|
624
|
+
getReadiness: context.getReadiness,
|
|
625
|
+
chat: context.chatRouteDeps,
|
|
626
|
+
nightAuxTicker: context.nightAuxTicker,
|
|
627
|
+
},
|
|
628
|
+
workspace: context,
|
|
629
|
+
};
|
|
290
630
|
}
|
|
291
631
|
server.on("error", reject);
|
|
292
|
-
server.listen(
|
|
632
|
+
server.listen(configuredPort, host, () => {
|
|
293
633
|
const addr = server.address();
|
|
294
634
|
if (!addr || typeof addr === "string") {
|
|
295
635
|
reject(new Error("Failed to get console server address"));
|
|
@@ -308,31 +648,136 @@ export async function createConsoleServer(options) {
|
|
|
308
648
|
url,
|
|
309
649
|
host,
|
|
310
650
|
port: addr.port,
|
|
311
|
-
appData,
|
|
312
|
-
operations,
|
|
313
|
-
events,
|
|
651
|
+
appData: startup.appData,
|
|
652
|
+
operations: startup.operations,
|
|
653
|
+
events: startup.events,
|
|
654
|
+
splitOrchestrator: startup.splitOrchestrator,
|
|
655
|
+
humanDecisionAdapter: startup.humanDecisionAdapter,
|
|
656
|
+
materializer: startup.materializer,
|
|
314
657
|
getReadiness,
|
|
315
658
|
operatorSessionId,
|
|
659
|
+
registry,
|
|
660
|
+
getWorkspaceContext: (workspaceId) => workspaceContexts.get(workspaceId),
|
|
661
|
+
listWorkspaceContexts: () => [...workspaceContexts.values()],
|
|
316
662
|
close: () => new Promise((closeResolve, closeReject) => {
|
|
317
|
-
// Dispose
|
|
663
|
+
// Dispose ALL workspace contexts BEFORE closing the HTTP server so
|
|
318
664
|
// no prompt is left executing and no session/SDK state/file handle
|
|
319
|
-
// leaks across Console restarts
|
|
320
|
-
//
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
process.stderr.write(`[console] chatRuntime.disposeAll() failed during close: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
665
|
+
// leaks across Console restarts. Each context is disposed exactly
|
|
666
|
+
// once (dispose() is idempotent).
|
|
667
|
+
for (const context of workspaceContexts.values()) {
|
|
668
|
+
try {
|
|
669
|
+
context.dispose();
|
|
670
|
+
}
|
|
671
|
+
catch (error) {
|
|
672
|
+
process.stderr.write(`[console] workspace context dispose failed during close: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
673
|
+
}
|
|
329
674
|
}
|
|
675
|
+
workspaceContexts.clear();
|
|
330
676
|
server.close((err) => (err ? closeReject(err) : closeResolve()));
|
|
331
677
|
}),
|
|
332
678
|
});
|
|
333
679
|
});
|
|
334
680
|
});
|
|
335
681
|
}
|
|
682
|
+
/**
|
|
683
|
+
* Resolve a workspace context for POST /chat/sessions. The workspaceId comes
|
|
684
|
+
* from the JSON body; the body is pre-read and handed to the route handler so
|
|
685
|
+
* it is not consumed twice.
|
|
686
|
+
*/
|
|
687
|
+
async function resolveContextFromSessionCreateBody(req, res, resolveContext) {
|
|
688
|
+
const body = await readBodyOnce(req);
|
|
689
|
+
if (!body.ok) {
|
|
690
|
+
sendJson(res, body.status, {
|
|
691
|
+
ok: false,
|
|
692
|
+
error: { code: body.code, message: body.message },
|
|
693
|
+
});
|
|
694
|
+
return undefined;
|
|
695
|
+
}
|
|
696
|
+
const record = body.value;
|
|
697
|
+
const workspaceId = typeof record.workspaceId === "string" ? record.workspaceId : undefined;
|
|
698
|
+
const resolution = await resolveContext(workspaceId);
|
|
699
|
+
if (!resolution.ok) {
|
|
700
|
+
sendJson(res, resolution.status, {
|
|
701
|
+
ok: false,
|
|
702
|
+
error: { code: resolution.code, message: resolution.message },
|
|
703
|
+
});
|
|
704
|
+
return undefined;
|
|
705
|
+
}
|
|
706
|
+
// AC-R7-002 (2026-08-23): the runtime store.create wrapper
|
|
707
|
+
// (the former per-store patch + WeakMap cache) was fully removed;
|
|
708
|
+
// workspaceId now flows as an explicit parameter from this
|
|
709
|
+
// dispatcher through handleCreateChatSession into store.create.
|
|
710
|
+
return { context: resolution.context };
|
|
711
|
+
}
|
|
712
|
+
/** Resolve a workspace context for POST /operations (workspaceId in body). */
|
|
713
|
+
async function resolveContextFromOperationCreateBody(req, res, resolveContext) {
|
|
714
|
+
const body = await readBodyOnce(req);
|
|
715
|
+
if (!body.ok) {
|
|
716
|
+
sendJson(res, body.status, {
|
|
717
|
+
ok: false,
|
|
718
|
+
error: { code: body.code, message: body.message },
|
|
719
|
+
});
|
|
720
|
+
return undefined;
|
|
721
|
+
}
|
|
722
|
+
const record = body.value;
|
|
723
|
+
const workspaceId = typeof record.workspaceId === "string" ? record.workspaceId.trim() : "";
|
|
724
|
+
if (!workspaceId) {
|
|
725
|
+
sendJson(res, 400, {
|
|
726
|
+
ok: false,
|
|
727
|
+
error: {
|
|
728
|
+
code: "WORKSPACE_REQUIRED",
|
|
729
|
+
message: "workspaceId is required",
|
|
730
|
+
},
|
|
731
|
+
});
|
|
732
|
+
return undefined;
|
|
733
|
+
}
|
|
734
|
+
const resolution = await resolveContext(workspaceId);
|
|
735
|
+
if (!resolution.ok) {
|
|
736
|
+
sendJson(res, resolution.status, {
|
|
737
|
+
ok: false,
|
|
738
|
+
error: { code: resolution.code, message: resolution.message },
|
|
739
|
+
});
|
|
740
|
+
return undefined;
|
|
741
|
+
}
|
|
742
|
+
return { context: resolution.context, preReadBody: record };
|
|
743
|
+
}
|
|
744
|
+
/** Read and buffer a JSON request body once (restored onto req for handlers). */
|
|
745
|
+
async function readBodyOnce(req) {
|
|
746
|
+
const chunks = [];
|
|
747
|
+
let total = 0;
|
|
748
|
+
for await (const chunk of req) {
|
|
749
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
750
|
+
total += buf.length;
|
|
751
|
+
if (total > 10 * 1024 * 1024) {
|
|
752
|
+
return {
|
|
753
|
+
ok: false,
|
|
754
|
+
status: 400,
|
|
755
|
+
code: "INVALID_INPUT",
|
|
756
|
+
message: "request body exceeds 10485760 bytes",
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
chunks.push(buf);
|
|
760
|
+
}
|
|
761
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
762
|
+
if (!raw.trim()) {
|
|
763
|
+
return { ok: true, value: {} };
|
|
764
|
+
}
|
|
765
|
+
try {
|
|
766
|
+
const value = JSON.parse(raw);
|
|
767
|
+
// Re-inject the body so downstream route handlers can read it again.
|
|
768
|
+
req.__preReadBody =
|
|
769
|
+
value;
|
|
770
|
+
return { ok: true, value };
|
|
771
|
+
}
|
|
772
|
+
catch (error) {
|
|
773
|
+
return {
|
|
774
|
+
ok: false,
|
|
775
|
+
status: 400,
|
|
776
|
+
code: "INVALID_INPUT",
|
|
777
|
+
message: `invalid JSON body: ${error instanceof Error ? error.message : String(error)}`,
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
}
|
|
336
781
|
function requestSource(req) {
|
|
337
782
|
const forwarded = req.headers["x-forwarded-for"];
|
|
338
783
|
if (typeof forwarded === "string" && forwarded.trim()) {
|