@tea-agent/loop-agent 0.40.0-next.1 → 0.40.0-next.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +114 -109
- package/README.md +2 -2
- package/dist/application/dag/generate-task-dag.js +6 -2
- package/dist/application/task-lifecycle/advance.js +12 -0
- package/dist/build-stamp.json +3 -3
- package/dist/cli/update/runtime-activity.js +1 -29
- package/dist/commands/dag-rerun.js +1 -1
- package/dist/executors/dag-pi-executor.js +69 -7
- package/dist/executors/pi-executor.js +4 -2
- package/dist/executors/pi-sdk-executor.js +46 -1
- package/dist/executors/pi-writer-tool-policy.js +57 -1
- package/dist/executors/shell-executor.js +107 -20
- package/dist/executors/workspace-write-snapshot.js +68 -0
- package/dist/governance/checks.js +1 -0
- package/dist/infrastructure/harness/atomic-write.js +23 -0
- package/dist/shared/dag-prompt-override.js +4 -5
- package/dist/shared/update/npm-client.js +33 -11
- package/dist/task/config-types.js +10 -0
- 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 +15 -1
- package/dist/task/contract/schema.js +2 -0
- package/dist/task/contract/transaction.js +12 -0
- package/dist/task/source-prepare/completeness.js +188 -0
- package/dist/task/source-prepare/fragment-inventory.js +474 -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/prepare.js +262 -1
- package/dist/task/source-prepare/semantic-intake.js +19 -3
- package/dist/task/source-prepare/source-fidelity-pi.js +384 -0
- package/dist/task/source-prepare/types.js +22 -0
- package/dist/worker/cli.js +12 -26
- package/dist/worker/console/chat/chat-event-store.js +75 -5
- 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/pi-runtime.js +331 -18
- package/dist/worker/console/chat/provider-error.js +98 -0
- package/dist/worker/console/chat/routes.js +389 -56
- 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 +79 -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 +20 -2
- package/dist/worker/console/draft-store.js +49 -0
- package/dist/worker/console/native-directory-picker.js +13 -0
- package/dist/worker/console/operation-store.js +1 -1
- package/dist/worker/console/operator-actions.js +133 -7
- package/dist/worker/console/operator-surface-health.js +1 -0
- package/dist/worker/console/prd-intake-bridge.js +13 -0
- package/dist/worker/console/routes.js +312 -1
- package/dist/worker/console/security.js +4 -4
- package/dist/worker/console/server.js +583 -172
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Br92EGb7.js → abnfDiagram-N423BO3Z-Bs-CDAXM.js} +1 -1
- package/dist/worker/console/static/assets/{arc-oulImtQq.js → arc-CAkA3We3.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-tyHc6aqO.js → architectureDiagram-T3A2C74G-DEmI_zqr.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CY86BSpT.js → blockDiagram-VBNYF7ZC-uDHANebE.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BKKsDmNs.js → c4Diagram-5PPSVZJV-Bv97b4_B.js} +1 -1
- package/dist/worker/console/static/assets/channel-amxUpk7o.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-CXZdjl3B.js → chunk-2GRJ4B5K-shmQKsFz.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-B-aIFDQ5.js → chunk-2Q5K7J3B-BAraNvcN.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BDgAW_B2.js → chunk-5RXB4S5H-BirhxUop.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-DABa4UPR.js → chunk-5VM5RSS4-C9LgUJpR.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-aGbysVOx.js → chunk-6Q2QTUOP-DrU-s-t7.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-B8MSZDVs.js → chunk-GF5L2VYU-Tx-H_FA2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CpelV73g.js → chunk-JWPE2WC7-BjLtNFrR.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BE04ty3z.js → chunk-KBJHAD2P-UNLVwSsy.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BUxTkaaR.js → chunk-RYQCIY6F-CBEUX8XD.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CYYBVKfX.js → chunk-XXDRQBXY-WTL7IZOq.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-bejwBVIz.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-bejwBVIz.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CEQF_xmQ.js → cose-bilkent-JH36ORCC-CkHG6W3s.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-CqpxN5IT.js → cynefin-VYW2F7L2-BRzFqTaL.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-B3PzanVi.js → cynefinDiagram-MW4NZA55-2_CFp_i7.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BSUGCNx1.js → dagre-VZM6K2ZE-BJtsmdkE.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CdLevU5T.js → diagram-7IWD3JNH-DNEFFo6w.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BjpF_Kok.js → diagram-B4RE2ZJO-B_IWV7eo.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-C4I6NJny.js → diagram-LBJQPF4R-DvvRx2dz.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-C8hecCr5.js → diagram-Q27KOJAE-CsibsWLg.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-Daod6tOS.js → diagram-UB23O5K3-les_djBQ.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BvvmazGv.js → ebnfDiagram-BXEA7PRR-BEm2n7qJ.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DeN_vfV4.js → erDiagram-JOGREHBK-BI7vHHVm.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-D6dq1t9z.js → flowDiagram-UKHOOZJN-CUFCERrB.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DziIdNY4.js → ganttDiagram-PKOTCBZU-wxyLiGWt.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dy4s5CXr.js → gitGraphDiagram-DS77QQ5N-Bt897K8Z.js} +1 -1
- package/dist/worker/console/static/assets/index-BHhUOTri.css +1 -0
- package/dist/worker/console/static/assets/index-DqJbO3-p.js +407 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Czv5eYx2.js → infoDiagram-6WML65LV-DYNx_IPv.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-Bsk3nFRT.js → ishikawaDiagram-WSZJBQD7-CzpLNEu6.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DxGmNj5K.js → journeyDiagram-NVQOT4AX-97f9owNU.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CYCFqkO_.js → kanban-definition-27J2QSJJ-BxM9hyBn.js} +1 -1
- package/dist/worker/console/static/assets/{linear-B0YzRwrd.js → linear-ByuxcHvp.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-t3EZtIGh.js → mermaid.core-DwPGvpCv.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BTXpTv0T.js → mindmap-definition-FAOFIHXS-DCoqG9RT.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DU8UkKqA.js → pegDiagram-VL7TDLO6-BLSBmBMx.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DP4y8ohp.js → pieDiagram-7S7Q4E2Y-B0t_0oJW.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-B07vNaYR.js → quadrantDiagram-CIZ2JOQS-sTzDk7Nt.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-xDWktb2i.js → railroadDiagram-AXF67PYL-B3jRdATC.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BLNMKqeS.js → requirementDiagram-LRYGKXZP-D-cTtXEN.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-n0adalxH.js → sankeyDiagram-W5VNT64P-B25igJRE.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BbnS-2DL.js → sequenceDiagram-SI44F4Z6-jW_lM1_D.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4uPpCLu.js → sizeCapture-X5ZJPWSS-CrzTgsHE.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA--MMbB0fX.js → stateDiagram-OKZ733FA-B01MvlqE.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BvFKEtm2.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BXUgp2QY.js → swimlanes-SLNWSIFB-YcF12FiU.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DIB2eVg-.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CsaMxki7.js → timeline-definition-Z64GVDOM-ClnRcg2s.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-C2IuG9iH.js → vennDiagram-T6HMQDX7-B6gr_z-q.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CmEWOXCW.js → wardleyDiagram-T6FBY63Y-CvSgguGH.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-D_9LE0MG.js → xychartDiagram-ELKLHX3M-23OtSZCb.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- 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 +10 -11
- package/dist/worker/console/static-src/app/useRunProgress.js +2 -1
- 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/chat-sse-events.js +141 -8
- package/dist/worker/console/static-src/operator-chat/compaction-message.js +79 -0
- package/dist/worker/console/static-src/operator-chat/details-dag-actions.js +122 -0
- 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/spatial-overlay.js +1 -2
- package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +25 -0
- package/dist/worker/console/static-src/operator-chat/turn-submission.js +3 -2
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +181 -87
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +159 -44
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +177 -8
- 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/shell/useWorkspaces.js +299 -0
- package/dist/worker/console/static-src/shell/workspace-route.js +339 -0
- package/dist/worker/console/workspace-context.js +234 -0
- package/dist/worker/console/workspace-registry.js +214 -0
- package/dist/worker/observability/init-runtime-activity.js +26 -0
- package/dist/worker/observe/node-input.js +88 -2
- package/dist/worker/observe/routes.js +188 -0
- package/dist/worker/observe/server.js +4 -0
- package/dist/worker/observe/static/api.js +29 -9
- package/dist/worker/observe/static/app.js +18 -70
- package/dist/worker/observe/static/dag-node-purpose.d.ts +6 -0
- package/dist/worker/observe/static/dag-node-purpose.js +203 -0
- package/dist/worker/observe/static/format.js +37 -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/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 -100
- 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 +174 -2
- package/dist/worker/observe/static/styles.css +357 -35
- package/dist/worker/observe/static/views/dag-graph.js +4 -2
- package/dist/worker/observe/static/views/dag-inspector.js +533 -79
- package/dist/worker/observe/static/views/dag.d.ts +13 -0
- package/dist/worker/observe/static/views/dag.js +6 -4
- package/dist/worker/observe/static/views/dashboard.js +6 -4
- 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/task.js +56 -1
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +186 -8
- package/dist/workflows/dag/backend-test-markdown-workflow.js +176 -1
- package/dist/workflows/dag/backend-test-pytest-collection.js +168 -16
- package/dist/workflows/dag/backend-test-result-contract.js +4 -0
- package/dist/workflows/dag/backend-test-scenario-param.js +339 -53
- package/dist/workflows/dag/backend-test-writer-completeness.js +28 -17
- package/dist/workflows/dag/dynamic-runtime/map.js +1 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +88 -2
- package/dist/workflows/dag/frontend-plan-render.js +24 -0
- package/dist/workflows/dag/frontend-prewrite-gate.js +197 -0
- package/dist/workflows/dag/frontend-review-context.js +92 -10
- package/dist/workflows/dag/init-hybrid.js +239 -52
- package/dist/workflows/dag/interrupt-request.js +3 -3
- package/dist/workflows/dag/lifecycle.js +3 -3
- package/dist/workflows/dag/node-execution.js +45 -0
- package/dist/workflows/dag/types.js +62 -12
- package/docs/README.md +2 -0
- package/docs/architecture/evolution.md +45 -0
- package/docs/init-surface.manifest.json +1 -0
- package/docs/operations/local-development-environment.md +21 -0
- package/docs/templates/README.md +1 -1
- package/docs/templates/agent-dag.schema.json +18 -0
- package/docs/templates/backend-test-dag.json +267 -238
- package/docs/templates/frontend-implementation-contract.schema.json +681 -24
- package/harness.json +3 -3
- package/package.json +3 -1
- package/skills/loop-agent/references/hybrid-dag.md +7 -0
- package/dist/worker/console/static/assets/channel-TY36zt-i.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Dk7QCChM.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Dk7QCChM.js +0 -1
- package/dist/worker/console/static/assets/index-C3GgHg-g.css +0 -1
- package/dist/worker/console/static/assets/index-Ci8ksvBm.js +0 -406
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BqOigMQ4.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Caxz9UA2.js +0 -8
- package/dist/worker/console/static-src/operator-chat/activity-rail-presentation.js +0 -73
- package/dist/worker/console/static-src/operator-chat/useActivityRailTransition.js +0 -59
|
@@ -1,32 +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 { createChatEventStore, createChatOperationLinker, } from "./chat/chat-event-store.js";
|
|
17
|
-
import { createChatTurnExecutionRegistry } from "./chat/turn-execution-registry.js";
|
|
18
|
-
import { ConsolePiRuntime } from "./chat/pi-runtime.js";
|
|
19
|
-
import { handleChatRequest, recoverDeferredTurns, } from "./chat/routes.js";
|
|
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";
|
|
11
|
+
import { handleConsoleHealthRequest, handleWorkspaceRoutes, sendJson, serveConsoleStatic, } from "./routes.js";
|
|
24
12
|
import { issueBootCapabilityToken, injectConfirmationTokenScript, } from "./security.js";
|
|
25
|
-
import {
|
|
13
|
+
import { resolvePackageRootFromConsole } from "./sibling-controller.js";
|
|
26
14
|
import { isDagRerunPromptOverrideEnabled } from "../../shared/dag-prompt-override.js";
|
|
27
|
-
import {
|
|
15
|
+
import { defaultObserveStaticDir, handleMatchedObserveRoute, matchObserveGetRoute, ROUTES, serveObserveStatic, } from "../observe/routes.js";
|
|
28
16
|
import { deriveObserveRouteCapabilities } from "../observe/health.js";
|
|
29
|
-
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";
|
|
30
21
|
/**
|
|
31
22
|
* Default hard timeout for the Console-owned LoopAgentClient.
|
|
32
23
|
*
|
|
@@ -72,17 +63,20 @@ export async function createConsoleServer(options) {
|
|
|
72
63
|
const keepAliveTimeoutMs = options.keepAliveTimeoutMs ?? 65_000;
|
|
73
64
|
const staticDir = options.staticDir ?? defaultConsoleStaticDir();
|
|
74
65
|
const inspectStaticDir = options.inspectStaticDir ?? defaultObserveStaticDir();
|
|
75
|
-
const observeRouteContext = createObserveRouteContext(repoRoot, {
|
|
76
|
-
staticDir: inspectStaticDir,
|
|
77
|
-
});
|
|
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
|
});
|
|
75
|
+
const registry = new WorkspaceRegistry(bootstrapAppData);
|
|
76
|
+
const startupRecord = await registry.ensureStartupWorkspace(repoRoot);
|
|
83
77
|
const instanceNonce = configuredPort === 0 ? randomBytes(16).toString("hex") : undefined;
|
|
84
78
|
const bootToken = issueBootCapabilityToken({
|
|
85
|
-
repoFingerprint:
|
|
79
|
+
repoFingerprint: startupRecord.repoFingerprint,
|
|
86
80
|
bindHost: host,
|
|
87
81
|
configuredPort,
|
|
88
82
|
...(instanceNonce ? { instanceNonce } : {}),
|
|
@@ -91,135 +85,134 @@ export async function createConsoleServer(options) {
|
|
|
91
85
|
// separate cookie name + confirmation token so the read-only inspect page
|
|
92
86
|
// can never replay them against the general Console mutation surface.
|
|
93
87
|
const inspectBootToken = issueBootCapabilityToken({
|
|
94
|
-
repoFingerprint:
|
|
88
|
+
repoFingerprint: startupRecord.repoFingerprint,
|
|
95
89
|
bindHost: host,
|
|
96
90
|
configuredPort,
|
|
97
91
|
...(instanceNonce ? { instanceNonce } : {}),
|
|
98
92
|
}, { variant: "inspect" });
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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,
|
|
122
135
|
});
|
|
123
|
-
|
|
124
|
-
|
|
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}`);
|
|
125
146
|
}
|
|
126
|
-
const
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
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,
|
|
130
160
|
repoRoot,
|
|
161
|
+
host: boundHost,
|
|
162
|
+
port: boundPort,
|
|
163
|
+
...(options.appDataRoot ? { appDataRoot: options.appDataRoot } : {}),
|
|
164
|
+
targetVersion,
|
|
131
165
|
});
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const chatStore = new ChatSessionStore(appData);
|
|
162
|
-
// M5.1: every Console boot owns a fresh event-log generation. A browser
|
|
163
|
-
// reconnecting with an old SSE cursor must reconcile from snapshot rather
|
|
164
|
-
// than silently consume a different process' event namespace.
|
|
165
|
-
const chatEventEpoch = randomBytes(12).toString("hex");
|
|
166
|
-
const chatEvents = createChatEventStore({ appData, epoch: chatEventEpoch });
|
|
167
|
-
const chatTurnExecutions = createChatTurnExecutionRegistry();
|
|
168
|
-
const chatOperationLinker = createChatOperationLinker({
|
|
169
|
-
chatEvents,
|
|
170
|
-
operations,
|
|
171
|
-
operationEvents: events,
|
|
172
|
-
});
|
|
173
|
-
const chatRuntime = new ConsolePiRuntime({
|
|
174
|
-
cwd: repoRoot,
|
|
175
|
-
sessionDir: appData.chatSessions,
|
|
176
|
-
resourcePreferencesPath: appData.resourcePreferences,
|
|
177
|
-
actionContext,
|
|
178
|
-
});
|
|
179
|
-
// chatRouteDeps is a plain object literal — it cannot throw. The previous
|
|
180
|
-
// try/catch was dead code that created a false "handled failure" impression.
|
|
181
|
-
// If a future field can throw, scope it explicitly instead of wrapping the
|
|
182
|
-
// literal.
|
|
183
|
-
const chatRouteDeps = {
|
|
184
|
-
runtime: chatRuntime,
|
|
185
|
-
store: chatStore,
|
|
186
|
-
events: chatEvents,
|
|
187
|
-
turnExecutions: chatTurnExecutions,
|
|
188
|
-
operationLinker: chatOperationLinker,
|
|
189
|
-
appData,
|
|
190
|
-
bootToken,
|
|
191
|
-
getConsoleOrigin,
|
|
192
|
-
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
|
+
}
|
|
193
195
|
};
|
|
194
|
-
|
|
195
|
-
// when no browser currently has the Session open.
|
|
196
|
-
void recoverDeferredTurns(chatRouteDeps).catch((error) => {
|
|
197
|
-
console.error(`[console] deferred Turn recovery failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
198
|
-
});
|
|
199
|
-
const nightAuxTicker = new NightAuxTicker({
|
|
196
|
+
sharedConsoleUpdateExecutor ??= createConsoleUpdateExecutor({
|
|
200
197
|
repoRoot,
|
|
201
|
-
|
|
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,
|
|
202
203
|
});
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
actionContext,
|
|
216
|
-
operations,
|
|
217
|
-
events,
|
|
218
|
-
getReadiness,
|
|
219
|
-
chat: chatRouteDeps,
|
|
220
|
-
nightAuxTicker,
|
|
221
|
-
},
|
|
222
|
-
};
|
|
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;
|
|
223
216
|
return new Promise((resolve, reject) => {
|
|
224
217
|
const server = createServer((req, res) => {
|
|
225
218
|
const startedAt = Date.now();
|
|
@@ -267,44 +260,87 @@ export async function createConsoleServer(options) {
|
|
|
267
260
|
}
|
|
268
261
|
});
|
|
269
262
|
async function dispatch(req, res) {
|
|
270
|
-
// Refresh readiness pointer on each request surface
|
|
271
|
-
actionContext.readiness = getReadiness();
|
|
272
263
|
const method = (req.method ?? "GET").toUpperCase();
|
|
273
|
-
const
|
|
264
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
265
|
+
const pathname = decodeURIComponent(url.pathname);
|
|
274
266
|
if (method === "GET" && pathname === "/api/health") {
|
|
275
|
-
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
|
+
});
|
|
276
281
|
return;
|
|
277
282
|
}
|
|
278
283
|
if (pathname.startsWith("/api/operator/v1/") ||
|
|
279
284
|
pathname.startsWith("/api/session/")) {
|
|
280
|
-
//
|
|
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
|
+
}
|
|
281
303
|
if (pathname.startsWith("/api/operator/v1/chat/") ||
|
|
282
304
|
pathname.startsWith("/api/operator/v1/pi/")) {
|
|
283
|
-
await
|
|
305
|
+
await handleChatDispatch(req, res, pathname, url, method);
|
|
284
306
|
return;
|
|
285
307
|
}
|
|
286
|
-
await
|
|
308
|
+
await handleOperatorDispatch(req, res, pathname, url, method);
|
|
287
309
|
return;
|
|
288
310
|
}
|
|
289
311
|
if (method === "GET" && pathname.startsWith("/api/")) {
|
|
290
312
|
const match = matchObserveGetRoute(pathname);
|
|
291
313
|
if (match) {
|
|
292
|
-
|
|
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);
|
|
293
332
|
return;
|
|
294
333
|
}
|
|
295
334
|
}
|
|
296
335
|
if (pathname.startsWith("/inspect")) {
|
|
297
|
-
//
|
|
298
|
-
//
|
|
336
|
+
// Prompt-override rerun is enabled by default. Unless the emergency
|
|
337
|
+
// kill switch disables it, hand the inspect page SCOPED mutation
|
|
299
338
|
// credentials (separate cookie + token, only strong enough for
|
|
300
|
-
// INSPECT_MUTATION_ALLOWED_ACTIONS — see routes.ts).
|
|
301
|
-
// strictly read-only: no token, no cookie, no tab.
|
|
339
|
+
// INSPECT_MUTATION_ALLOWED_ACTIONS — see routes.ts).
|
|
302
340
|
const htmlEnhancer = isDagRerunPromptOverrideEnabled()
|
|
303
341
|
? (html) => ({
|
|
304
|
-
content: injectConfirmationTokenScript(html,
|
|
305
|
-
|
|
306
|
-
setCookie: routeContext.inspectBootToken?.setCookieHeader ??
|
|
307
|
-
routeContext.bootToken.setCookieHeader,
|
|
342
|
+
content: injectConfirmationTokenScript(html, inspectBootToken.confirmationToken, "__INSPECT_MUTATION_TOKEN__"),
|
|
343
|
+
setCookie: inspectBootToken.setCookieHeader,
|
|
308
344
|
})
|
|
309
345
|
: undefined;
|
|
310
346
|
await serveObserveStatic(req, res, inspectStaticDir, {
|
|
@@ -317,7 +353,280 @@ export async function createConsoleServer(options) {
|
|
|
317
353
|
sendJson(res, 404, { error: "Not found" });
|
|
318
354
|
return;
|
|
319
355
|
}
|
|
320
|
-
|
|
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
|
+
};
|
|
321
630
|
}
|
|
322
631
|
server.on("error", reject);
|
|
323
632
|
server.listen(configuredPort, host, () => {
|
|
@@ -339,31 +648,133 @@ export async function createConsoleServer(options) {
|
|
|
339
648
|
url,
|
|
340
649
|
host,
|
|
341
650
|
port: addr.port,
|
|
342
|
-
appData,
|
|
343
|
-
operations,
|
|
344
|
-
events,
|
|
651
|
+
appData: startup.appData,
|
|
652
|
+
operations: startup.operations,
|
|
653
|
+
events: startup.events,
|
|
345
654
|
getReadiness,
|
|
346
655
|
operatorSessionId,
|
|
656
|
+
registry,
|
|
657
|
+
getWorkspaceContext: (workspaceId) => workspaceContexts.get(workspaceId),
|
|
658
|
+
listWorkspaceContexts: () => [...workspaceContexts.values()],
|
|
347
659
|
close: () => new Promise((closeResolve, closeReject) => {
|
|
348
|
-
// Dispose
|
|
660
|
+
// Dispose ALL workspace contexts BEFORE closing the HTTP server so
|
|
349
661
|
// no prompt is left executing and no session/SDK state/file handle
|
|
350
|
-
// leaks across Console restarts
|
|
351
|
-
//
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
process.stderr.write(`[console] chatRuntime.disposeAll() failed during close: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
662
|
+
// leaks across Console restarts. Each context is disposed exactly
|
|
663
|
+
// once (dispose() is idempotent).
|
|
664
|
+
for (const context of workspaceContexts.values()) {
|
|
665
|
+
try {
|
|
666
|
+
context.dispose();
|
|
667
|
+
}
|
|
668
|
+
catch (error) {
|
|
669
|
+
process.stderr.write(`[console] workspace context dispose failed during close: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
670
|
+
}
|
|
360
671
|
}
|
|
672
|
+
workspaceContexts.clear();
|
|
361
673
|
server.close((err) => (err ? closeReject(err) : closeResolve()));
|
|
362
674
|
}),
|
|
363
675
|
});
|
|
364
676
|
});
|
|
365
677
|
});
|
|
366
678
|
}
|
|
679
|
+
/**
|
|
680
|
+
* Resolve a workspace context for POST /chat/sessions. The workspaceId comes
|
|
681
|
+
* from the JSON body; the body is pre-read and handed to the route handler so
|
|
682
|
+
* it is not consumed twice.
|
|
683
|
+
*/
|
|
684
|
+
async function resolveContextFromSessionCreateBody(req, res, resolveContext) {
|
|
685
|
+
const body = await readBodyOnce(req);
|
|
686
|
+
if (!body.ok) {
|
|
687
|
+
sendJson(res, body.status, {
|
|
688
|
+
ok: false,
|
|
689
|
+
error: { code: body.code, message: body.message },
|
|
690
|
+
});
|
|
691
|
+
return undefined;
|
|
692
|
+
}
|
|
693
|
+
const record = body.value;
|
|
694
|
+
const workspaceId = typeof record.workspaceId === "string" ? record.workspaceId : undefined;
|
|
695
|
+
const resolution = await resolveContext(workspaceId);
|
|
696
|
+
if (!resolution.ok) {
|
|
697
|
+
sendJson(res, resolution.status, {
|
|
698
|
+
ok: false,
|
|
699
|
+
error: { code: resolution.code, message: resolution.message },
|
|
700
|
+
});
|
|
701
|
+
return undefined;
|
|
702
|
+
}
|
|
703
|
+
// AC-R7-002 (2026-08-23): the runtime store.create wrapper
|
|
704
|
+
// (the former per-store patch + WeakMap cache) was fully removed;
|
|
705
|
+
// workspaceId now flows as an explicit parameter from this
|
|
706
|
+
// dispatcher through handleCreateChatSession into store.create.
|
|
707
|
+
return { context: resolution.context };
|
|
708
|
+
}
|
|
709
|
+
/** Resolve a workspace context for POST /operations (workspaceId in body). */
|
|
710
|
+
async function resolveContextFromOperationCreateBody(req, res, resolveContext) {
|
|
711
|
+
const body = await readBodyOnce(req);
|
|
712
|
+
if (!body.ok) {
|
|
713
|
+
sendJson(res, body.status, {
|
|
714
|
+
ok: false,
|
|
715
|
+
error: { code: body.code, message: body.message },
|
|
716
|
+
});
|
|
717
|
+
return undefined;
|
|
718
|
+
}
|
|
719
|
+
const record = body.value;
|
|
720
|
+
const workspaceId = typeof record.workspaceId === "string" ? record.workspaceId.trim() : "";
|
|
721
|
+
if (!workspaceId) {
|
|
722
|
+
sendJson(res, 400, {
|
|
723
|
+
ok: false,
|
|
724
|
+
error: {
|
|
725
|
+
code: "WORKSPACE_REQUIRED",
|
|
726
|
+
message: "workspaceId is required",
|
|
727
|
+
},
|
|
728
|
+
});
|
|
729
|
+
return undefined;
|
|
730
|
+
}
|
|
731
|
+
const resolution = await resolveContext(workspaceId);
|
|
732
|
+
if (!resolution.ok) {
|
|
733
|
+
sendJson(res, resolution.status, {
|
|
734
|
+
ok: false,
|
|
735
|
+
error: { code: resolution.code, message: resolution.message },
|
|
736
|
+
});
|
|
737
|
+
return undefined;
|
|
738
|
+
}
|
|
739
|
+
return { context: resolution.context, preReadBody: record };
|
|
740
|
+
}
|
|
741
|
+
/** Read and buffer a JSON request body once (restored onto req for handlers). */
|
|
742
|
+
async function readBodyOnce(req) {
|
|
743
|
+
const chunks = [];
|
|
744
|
+
let total = 0;
|
|
745
|
+
for await (const chunk of req) {
|
|
746
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
747
|
+
total += buf.length;
|
|
748
|
+
if (total > 10 * 1024 * 1024) {
|
|
749
|
+
return {
|
|
750
|
+
ok: false,
|
|
751
|
+
status: 400,
|
|
752
|
+
code: "INVALID_INPUT",
|
|
753
|
+
message: "request body exceeds 10485760 bytes",
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
chunks.push(buf);
|
|
757
|
+
}
|
|
758
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
759
|
+
if (!raw.trim()) {
|
|
760
|
+
return { ok: true, value: {} };
|
|
761
|
+
}
|
|
762
|
+
try {
|
|
763
|
+
const value = JSON.parse(raw);
|
|
764
|
+
// Re-inject the body so downstream route handlers can read it again.
|
|
765
|
+
req.__preReadBody =
|
|
766
|
+
value;
|
|
767
|
+
return { ok: true, value };
|
|
768
|
+
}
|
|
769
|
+
catch (error) {
|
|
770
|
+
return {
|
|
771
|
+
ok: false,
|
|
772
|
+
status: 400,
|
|
773
|
+
code: "INVALID_INPUT",
|
|
774
|
+
message: `invalid JSON body: ${error instanceof Error ? error.message : String(error)}`,
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
}
|
|
367
778
|
function requestSource(req) {
|
|
368
779
|
const forwarded = req.headers["x-forwarded-for"];
|
|
369
780
|
if (typeof forwarded === "string" && forwarded.trim()) {
|