@tea-agent/loop-agent 0.40.0-next.4 → 0.40.0-next.5
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/CHANGELOG.md +20 -0
- package/dist/build-stamp.json +3 -3
- package/dist/infrastructure/harness/atomic-write.js +23 -0
- 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/pi-runtime.js +264 -16
- package/dist/worker/console/chat/provider-error.js +98 -0
- package/dist/worker/console/chat/routes.js +292 -29
- 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/native-directory-picker.js +13 -0
- package/dist/worker/console/routes.js +309 -1
- package/dist/worker/console/security.js +4 -4
- package/dist/worker/console/server.js +529 -186
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-CTx1TrNa.js → abnfDiagram-N423BO3Z-CGZrpj0K.js} +1 -1
- package/dist/worker/console/static/assets/{arc-B4i9cVxX.js → arc-CwEyA8iq.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BAXE8_Rl.js → architectureDiagram-T3A2C74G-DLTkrEQ3.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-C5Xh59at.js → blockDiagram-VBNYF7ZC-4w12zR3t.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B_asOzAu.js → c4Diagram-5PPSVZJV-BppqLAjk.js} +1 -1
- package/dist/worker/console/static/assets/channel-TzuwQha5.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-wRO_iRNc.js → chunk-2GRJ4B5K-DO6U26Pm.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-ByOaSh_G.js → chunk-2Q5K7J3B-DC7YEdOy.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLzudan.js → chunk-5RXB4S5H-DtMYVQBw.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-JCNGkXYT.js → chunk-5VM5RSS4-C5Ed-57N.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BhrPXXMZ.js → chunk-6Q2QTUOP-BUY4oIzT.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-TJ5JuEBT.js → chunk-GF5L2VYU-VnWHALIV.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-VVthn_mD.js → chunk-JWPE2WC7-COV-RQk4.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-D60-Mny4.js → chunk-KBJHAD2P-dSnLtUiz.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-zXv7aVye.js → chunk-RYQCIY6F-CX6FDzWh.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-By9VQoi1.js → chunk-XXDRQBXY-D3mEgb6W.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DCu69b3S.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DCu69b3S.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-C4zQLe-M.js → cose-bilkent-JH36ORCC-QQBMUnAf.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-wh5N2ReN.js → cynefin-VYW2F7L2-BxSD-LVV.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Dlu5wIqs.js → cynefinDiagram-MW4NZA55-DxHNiNbk.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CrDgthc6.js → dagre-VZM6K2ZE-C83bYy8E.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-zN1clg27.js → diagram-7IWD3JNH-BhtZu2YY.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BN-o7J7-.js → diagram-B4RE2ZJO-K0P_ntRE.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-C8SuPt0d.js → diagram-LBJQPF4R-CXYy1Nib.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BoizuHbZ.js → diagram-Q27KOJAE-D-CtIFkU.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BzUzXixG.js → diagram-UB23O5K3-BEK9-_7c.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Ev3lA4Cf.js → ebnfDiagram-BXEA7PRR-L0EX-9wn.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-GALUzRbs.js → erDiagram-JOGREHBK-9pt0fc_5.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CZJN65Sb.js → flowDiagram-UKHOOZJN-CZVeM_n1.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-JLB9OESl.js → ganttDiagram-PKOTCBZU-DqUKkAZP.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CcvCr4GD.js → gitGraphDiagram-DS77QQ5N-BYUuXEgp.js} +1 -1
- package/dist/worker/console/static/assets/index-B0jGzQj6.js +407 -0
- package/dist/worker/console/static/assets/index-BiYbPiTw.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-HG5z7aVg.js → infoDiagram-6WML65LV-DNzKk7ah.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BHEu9chR.js → ishikawaDiagram-WSZJBQD7-Clbyg5IP.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-Cp_Up8-a.js → journeyDiagram-NVQOT4AX-zDO_gZNO.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-DvVeY-em.js → kanban-definition-27J2QSJJ-x3LpRYek.js} +1 -1
- package/dist/worker/console/static/assets/{linear-aGmcjgTn.js → linear-CTpwZqKR.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-Biyo-tqA.js → mermaid.core-D_WXK6C7.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DyxY2pIb.js → mindmap-definition-FAOFIHXS-CWK-OYP5.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CkdGuc50.js → pegDiagram-VL7TDLO6-B9gtUPsM.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-NNQI9xVR.js → pieDiagram-7S7Q4E2Y-Dj2lzceR.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-PAJkuz_G.js → quadrantDiagram-CIZ2JOQS-Nwljut7m.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-ZwrQod0O.js → railroadDiagram-AXF67PYL-BY-KNt93.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D3ZfrzM2.js → requirementDiagram-LRYGKXZP-Vx0wvpPy.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-DDGFmWk5.js → sankeyDiagram-W5VNT64P-B6grNXUk.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DO6mmXTA.js → sequenceDiagram-SI44F4Z6-CteidBvh.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DMbCQiJt.js → sizeCapture-X5ZJPWSS-Du4W8mFK.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Di_YUEfC.js → stateDiagram-OKZ733FA-jyI6Yh81.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DyQblISl.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-Bn2PphNJ.js → swimlanes-SLNWSIFB-DYWs_fRv.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DiygmgYP.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DPh8pRsp.js → timeline-definition-Z64GVDOM-DO5LLy47.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-GMfl26pW.js → vennDiagram-T6HMQDX7-CLF2p1_L.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DTlmVrmF.js → wardleyDiagram-T6FBY63Y-AwTaYBxA.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-YGBjKAS5.js → xychartDiagram-ELKLHX3M-DzbblQRw.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +29 -4
- package/dist/worker/console/static-src/app/useConsoleShell.js +30 -12
- package/dist/worker/console/static-src/app/useOperatorActions.js +36 -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 +2 -2
- package/dist/worker/console/static-src/night/useNightBoard.js +8 -5
- 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/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 +3 -2
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +168 -72
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +78 -17
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +184 -4
- 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 +337 -0
- package/dist/worker/console/workspace-context.js +234 -0
- package/dist/worker/console/workspace-registry.js +214 -0
- package/dist/worker/observe/server.js +4 -0
- package/dist/worker/observe/static/api.js +26 -6
- package/dist/worker/observe/static/app.js +18 -70
- package/dist/worker/observe/static/index.html +1 -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/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 +172 -0
- package/dist/worker/observe/static/views/dag-inspector.js +3 -2
- package/dist/worker/observe/static/views/dag.js +2 -1
- 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 +1 -1
- package/dist/workflows/dag/interrupt-request.js +3 -3
- package/dist/workflows/dag/lifecycle.js +3 -3
- package/docs/README.md +1 -0
- package/docs/operations/local-development-environment.md +21 -0
- package/harness.json +3 -3
- package/package.json +3 -1
- package/dist/worker/console/static/assets/channel-Dj_4Moei.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C0YYAyCa.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C0YYAyCa.js +0 -1
- package/dist/worker/console/static/assets/index-CW88Fjbw.js +0 -406
- package/dist/worker/console/static/assets/index-DP0ZJl39.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DrdDvela.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DPkEAULl.js +0 -8
|
@@ -4,31 +4,18 @@ import { existsSync } from "node:fs";
|
|
|
4
4
|
import { createServer } from "node:http";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { LoopAgentClient } from "../loop-agent/loop-agent-client.js";
|
|
8
7
|
import { readPackageVersion } from "../../shared/package-metadata.js";
|
|
9
8
|
import { DAG_ABSOLUTE_MAX_WALL_CLOCK_MS } from "../../shared/timeout-policy.js";
|
|
10
9
|
import { openConsoleAppData } from "./app-data.js";
|
|
11
|
-
import { DagConfirmationStore } from "./dag-confirmation.js";
|
|
12
|
-
import { DagExecutionReceiptStore } from "./dag-execution-receipt.js";
|
|
13
|
-
import { DraftStore } from "./draft-store.js";
|
|
14
|
-
import { AssessmentStore } from "./interview/assessment.js";
|
|
15
|
-
import { InterviewSessionStore } from "./interview/session.js";
|
|
16
10
|
import { isLoopbackHost } from "./loopback.js";
|
|
17
|
-
import {
|
|
18
|
-
import { createChatEventStore, createChatOperationLinker, } from "./chat/chat-event-store.js";
|
|
19
|
-
import { createChatTurnExecutionRegistry } from "./chat/turn-execution-registry.js";
|
|
20
|
-
import { ConsolePiRuntime } from "./chat/pi-runtime.js";
|
|
21
|
-
import { handleChatRequest, recoverDeferredTurns, } from "./chat/routes.js";
|
|
22
|
-
import { createOperationEventStore, } from "./operation-sse.js";
|
|
23
|
-
import { OperationStore } from "./operation-store.js";
|
|
24
|
-
import { probePiReadiness, } from "./pi-readiness.js";
|
|
25
|
-
import { handleConsoleHealthRequest, handleConsoleOperatorRequest, sendJson, serveConsoleStatic, } from "./routes.js";
|
|
11
|
+
import { handleConsoleHealthRequest, handleWorkspaceRoutes, sendJson, serveConsoleStatic, } from "./routes.js";
|
|
26
12
|
import { issueBootCapabilityToken, injectConfirmationTokenScript, } from "./security.js";
|
|
27
|
-
import { resolvePackageRootFromConsole
|
|
13
|
+
import { resolvePackageRootFromConsole } from "./sibling-controller.js";
|
|
28
14
|
import { isDagRerunPromptOverrideEnabled } from "../../shared/dag-prompt-override.js";
|
|
29
|
-
import {
|
|
15
|
+
import { defaultObserveStaticDir, handleMatchedObserveRoute, matchObserveGetRoute, ROUTES, serveObserveStatic, } from "../observe/routes.js";
|
|
30
16
|
import { deriveObserveRouteCapabilities } from "../observe/health.js";
|
|
31
|
-
import {
|
|
17
|
+
import { WorkspaceRegistry, } from "./workspace-registry.js";
|
|
18
|
+
import { createWorkspaceContext, } from "./workspace-context.js";
|
|
32
19
|
import { createConsoleUpdateExecutor } from "./console-update-runtime.js";
|
|
33
20
|
import { createConsoleHandoff } from "./console-handoff.js";
|
|
34
21
|
/**
|
|
@@ -76,18 +63,20 @@ export async function createConsoleServer(options) {
|
|
|
76
63
|
const keepAliveTimeoutMs = options.keepAliveTimeoutMs ?? 65_000;
|
|
77
64
|
const staticDir = options.staticDir ?? defaultConsoleStaticDir();
|
|
78
65
|
const inspectStaticDir = options.inspectStaticDir ?? defaultObserveStaticDir();
|
|
79
|
-
const observeRouteContext = createObserveRouteContext(repoRoot, {
|
|
80
|
-
staticDir: inspectStaticDir,
|
|
81
|
-
});
|
|
82
66
|
const operatorSessionId = `sess_${randomBytes(16).toString("hex")}`;
|
|
83
67
|
const consoleInstanceId = options.consoleInstanceId ?? `console_${randomBytes(16).toString("hex")}`;
|
|
84
|
-
|
|
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({
|
|
85
72
|
repoRoot,
|
|
86
73
|
appDataRoot: options.appDataRoot,
|
|
87
74
|
});
|
|
75
|
+
const registry = new WorkspaceRegistry(bootstrapAppData);
|
|
76
|
+
const startupRecord = await registry.ensureStartupWorkspace(repoRoot);
|
|
88
77
|
const instanceNonce = configuredPort === 0 ? randomBytes(16).toString("hex") : undefined;
|
|
89
78
|
const bootToken = issueBootCapabilityToken({
|
|
90
|
-
repoFingerprint:
|
|
79
|
+
repoFingerprint: startupRecord.repoFingerprint,
|
|
91
80
|
bindHost: host,
|
|
92
81
|
configuredPort,
|
|
93
82
|
...(instanceNonce ? { instanceNonce } : {}),
|
|
@@ -96,86 +85,67 @@ export async function createConsoleServer(options) {
|
|
|
96
85
|
// separate cookie name + confirmation token so the read-only inspect page
|
|
97
86
|
// can never replay them against the general Console mutation surface.
|
|
98
87
|
const inspectBootToken = issueBootCapabilityToken({
|
|
99
|
-
repoFingerprint:
|
|
88
|
+
repoFingerprint: startupRecord.repoFingerprint,
|
|
100
89
|
bindHost: host,
|
|
101
90
|
configuredPort,
|
|
102
91
|
...(instanceNonce ? { instanceNonce } : {}),
|
|
103
92
|
}, { variant: "inspect" });
|
|
104
|
-
const operations = new OperationStore(appData);
|
|
105
|
-
const events = createOperationEventStore({
|
|
106
|
-
persistenceDir: path.join(appData.operations, "events"),
|
|
107
|
-
});
|
|
108
|
-
const drafts = new DraftStore(appData);
|
|
109
|
-
const confirmations = new DagConfirmationStore(appData, bootToken.confirmationToken);
|
|
110
|
-
const dagExecutionReceipts = new DagExecutionReceiptStore(appData);
|
|
111
|
-
const interviews = new InterviewSessionStore(appData);
|
|
112
|
-
const assessments = new AssessmentStore(appData);
|
|
113
|
-
const readiness = await probePiReadiness({
|
|
114
|
-
override: options.piReadinessProbe,
|
|
115
|
-
repoRoot,
|
|
116
|
-
});
|
|
117
|
-
let readinessCache = readiness;
|
|
118
|
-
const artifactRoot = path.join(appData.root, "client-artifacts", appData.fingerprint);
|
|
119
|
-
const client = options.createClient?.(artifactRoot) ??
|
|
120
|
-
new LoopAgentClient({
|
|
121
|
-
loopAgentBin: resolveSiblingLoopAgentBin(),
|
|
122
|
-
artifactRoot,
|
|
123
|
-
resolveIdentity: true,
|
|
124
|
-
// See DEFAULT_CONSOLE_CLIENT_TIMEOUT_MS: dag execute / task advance
|
|
125
|
-
// must not inherit the 120s worker-command default.
|
|
126
|
-
defaultTimeoutMs: resolveConsoleDefaultTimeoutMs(options.defaultTimeoutMs),
|
|
127
|
-
});
|
|
128
|
-
if (!options.skipReconcile) {
|
|
129
|
-
await operations.reconcileOnBoot();
|
|
130
|
-
}
|
|
131
|
-
const getReadiness = () => readinessCache;
|
|
132
|
-
const refreshReadiness = async () => {
|
|
133
|
-
readinessCache = await probePiReadiness({
|
|
134
|
-
override: options.piReadinessProbe,
|
|
135
|
-
repoRoot,
|
|
136
|
-
});
|
|
137
|
-
actionContext.readiness = readinessCache;
|
|
138
|
-
return readinessCache;
|
|
139
|
-
};
|
|
140
|
-
const actionContext = {
|
|
141
|
-
repoRoot,
|
|
142
|
-
appData,
|
|
143
|
-
operatorSessionId,
|
|
144
|
-
client,
|
|
145
|
-
operations,
|
|
146
|
-
events,
|
|
147
|
-
drafts,
|
|
148
|
-
confirmations,
|
|
149
|
-
dagExecutionReceipts,
|
|
150
|
-
humanGateSecret: bootToken.confirmationToken,
|
|
151
|
-
interviews,
|
|
152
|
-
assessments,
|
|
153
|
-
readiness: readinessCache,
|
|
154
|
-
getReadiness,
|
|
155
|
-
refreshReadiness,
|
|
156
|
-
observeBaseUrl: options.observeBaseUrl,
|
|
157
|
-
fetchImpl: options.fetchImpl,
|
|
158
|
-
consoleUpdateExecutor: options.consoleUpdateExecutor,
|
|
159
|
-
getConsoleOrigin: () => `http://${boundHost}:${boundPort}`,
|
|
160
|
-
};
|
|
161
93
|
let boundHost = host;
|
|
162
94
|
let boundPort = configuredPort;
|
|
163
95
|
const getConsoleOrigin = () => `http://${boundHost}:${boundPort}`;
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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,
|
|
135
|
+
});
|
|
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}`);
|
|
146
|
+
}
|
|
147
|
+
const startup = startupResolution.context;
|
|
174
148
|
const requestConsoleHandoff = async (targetVersion) => {
|
|
175
|
-
// The maintenance preflight proves this running Console was loaded from
|
|
176
|
-
// the global package. npm replaces that package in place, so resolve the
|
|
177
|
-
// replacement worker from that same package root rather than trusting the
|
|
178
|
-
// ambient PATH (which could point at another installation).
|
|
179
149
|
const packageRoot = resolvePackageRootFromConsole();
|
|
180
150
|
const agentWorkerBin = path.join(packageRoot, "bin", "agent-worker.js");
|
|
181
151
|
if (readPackageVersion(packageRoot) !== targetVersion ||
|
|
@@ -223,72 +193,26 @@ export async function createConsoleServer(options) {
|
|
|
223
193
|
};
|
|
224
194
|
}
|
|
225
195
|
};
|
|
226
|
-
|
|
227
|
-
options.consoleUpdateExecutor ??
|
|
228
|
-
createConsoleUpdateExecutor({
|
|
229
|
-
repoRoot,
|
|
230
|
-
packageRoot: resolvePackageRootFromConsole(),
|
|
231
|
-
client,
|
|
232
|
-
operations,
|
|
233
|
-
activeChatTurnCount: () => chatTurnExecutions.size(),
|
|
234
|
-
requestHandoff: requestConsoleHandoff,
|
|
235
|
-
});
|
|
236
|
-
const chatOperationLinker = createChatOperationLinker({
|
|
237
|
-
chatEvents,
|
|
238
|
-
operations,
|
|
239
|
-
operationEvents: events,
|
|
240
|
-
});
|
|
241
|
-
const chatRuntime = new ConsolePiRuntime({
|
|
242
|
-
cwd: repoRoot,
|
|
243
|
-
sessionDir: appData.chatSessions,
|
|
244
|
-
resourcePreferencesPath: appData.resourcePreferences,
|
|
245
|
-
actionContext,
|
|
246
|
-
});
|
|
247
|
-
// chatRouteDeps is a plain object literal — it cannot throw. The previous
|
|
248
|
-
// try/catch was dead code that created a false "handled failure" impression.
|
|
249
|
-
// If a future field can throw, scope it explicitly instead of wrapping the
|
|
250
|
-
// literal.
|
|
251
|
-
const chatRouteDeps = {
|
|
252
|
-
runtime: chatRuntime,
|
|
253
|
-
store: chatStore,
|
|
254
|
-
events: chatEvents,
|
|
255
|
-
turnExecutions: chatTurnExecutions,
|
|
256
|
-
operationLinker: chatOperationLinker,
|
|
257
|
-
appData,
|
|
258
|
-
bootToken,
|
|
259
|
-
getConsoleOrigin,
|
|
260
|
-
allowNonLoopbackAccess: !loopbackBind,
|
|
261
|
-
};
|
|
262
|
-
// Durable single-slot follow-ups are server-owned. Recover them at boot even
|
|
263
|
-
// when no browser currently has the Session open.
|
|
264
|
-
void recoverDeferredTurns(chatRouteDeps).catch((error) => {
|
|
265
|
-
console.error(`[console] deferred Turn recovery failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
266
|
-
});
|
|
267
|
-
const nightAuxTicker = new NightAuxTicker({
|
|
196
|
+
sharedConsoleUpdateExecutor ??= createConsoleUpdateExecutor({
|
|
268
197
|
repoRoot,
|
|
269
|
-
|
|
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,
|
|
270
203
|
});
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
operator: {
|
|
284
|
-
actionContext,
|
|
285
|
-
operations,
|
|
286
|
-
events,
|
|
287
|
-
getReadiness,
|
|
288
|
-
chat: chatRouteDeps,
|
|
289
|
-
nightAuxTicker,
|
|
290
|
-
},
|
|
291
|
-
};
|
|
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;
|
|
292
216
|
return new Promise((resolve, reject) => {
|
|
293
217
|
const server = createServer((req, res) => {
|
|
294
218
|
const startedAt = Date.now();
|
|
@@ -336,29 +260,75 @@ export async function createConsoleServer(options) {
|
|
|
336
260
|
}
|
|
337
261
|
});
|
|
338
262
|
async function dispatch(req, res) {
|
|
339
|
-
// Refresh readiness pointer on each request surface
|
|
340
|
-
actionContext.readiness = getReadiness();
|
|
341
263
|
const method = (req.method ?? "GET").toUpperCase();
|
|
342
|
-
const
|
|
264
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
265
|
+
const pathname = decodeURIComponent(url.pathname);
|
|
343
266
|
if (method === "GET" && pathname === "/api/health") {
|
|
344
|
-
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
|
+
});
|
|
345
281
|
return;
|
|
346
282
|
}
|
|
347
283
|
if (pathname.startsWith("/api/operator/v1/") ||
|
|
348
284
|
pathname.startsWith("/api/session/")) {
|
|
349
|
-
//
|
|
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
|
+
}
|
|
350
303
|
if (pathname.startsWith("/api/operator/v1/chat/") ||
|
|
351
304
|
pathname.startsWith("/api/operator/v1/pi/")) {
|
|
352
|
-
await
|
|
305
|
+
await handleChatDispatch(req, res, pathname, url, method);
|
|
353
306
|
return;
|
|
354
307
|
}
|
|
355
|
-
await
|
|
308
|
+
await handleOperatorDispatch(req, res, pathname, url, method);
|
|
356
309
|
return;
|
|
357
310
|
}
|
|
358
311
|
if (method === "GET" && pathname.startsWith("/api/")) {
|
|
359
312
|
const match = matchObserveGetRoute(pathname);
|
|
360
313
|
if (match) {
|
|
361
|
-
|
|
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);
|
|
362
332
|
return;
|
|
363
333
|
}
|
|
364
334
|
}
|
|
@@ -369,10 +339,8 @@ export async function createConsoleServer(options) {
|
|
|
369
339
|
// INSPECT_MUTATION_ALLOWED_ACTIONS — see routes.ts).
|
|
370
340
|
const htmlEnhancer = isDagRerunPromptOverrideEnabled()
|
|
371
341
|
? (html) => ({
|
|
372
|
-
content: injectConfirmationTokenScript(html,
|
|
373
|
-
|
|
374
|
-
setCookie: routeContext.inspectBootToken?.setCookieHeader ??
|
|
375
|
-
routeContext.bootToken.setCookieHeader,
|
|
342
|
+
content: injectConfirmationTokenScript(html, inspectBootToken.confirmationToken, "__INSPECT_MUTATION_TOKEN__"),
|
|
343
|
+
setCookie: inspectBootToken.setCookieHeader,
|
|
376
344
|
})
|
|
377
345
|
: undefined;
|
|
378
346
|
await serveObserveStatic(req, res, inspectStaticDir, {
|
|
@@ -385,7 +353,280 @@ export async function createConsoleServer(options) {
|
|
|
385
353
|
sendJson(res, 404, { error: "Not found" });
|
|
386
354
|
return;
|
|
387
355
|
}
|
|
388
|
-
|
|
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
|
+
};
|
|
389
630
|
}
|
|
390
631
|
server.on("error", reject);
|
|
391
632
|
server.listen(configuredPort, host, () => {
|
|
@@ -407,31 +648,133 @@ export async function createConsoleServer(options) {
|
|
|
407
648
|
url,
|
|
408
649
|
host,
|
|
409
650
|
port: addr.port,
|
|
410
|
-
appData,
|
|
411
|
-
operations,
|
|
412
|
-
events,
|
|
651
|
+
appData: startup.appData,
|
|
652
|
+
operations: startup.operations,
|
|
653
|
+
events: startup.events,
|
|
413
654
|
getReadiness,
|
|
414
655
|
operatorSessionId,
|
|
656
|
+
registry,
|
|
657
|
+
getWorkspaceContext: (workspaceId) => workspaceContexts.get(workspaceId),
|
|
658
|
+
listWorkspaceContexts: () => [...workspaceContexts.values()],
|
|
415
659
|
close: () => new Promise((closeResolve, closeReject) => {
|
|
416
|
-
// Dispose
|
|
660
|
+
// Dispose ALL workspace contexts BEFORE closing the HTTP server so
|
|
417
661
|
// no prompt is left executing and no session/SDK state/file handle
|
|
418
|
-
// leaks across Console restarts
|
|
419
|
-
//
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
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
|
+
}
|
|
428
671
|
}
|
|
672
|
+
workspaceContexts.clear();
|
|
429
673
|
server.close((err) => (err ? closeReject(err) : closeResolve()));
|
|
430
674
|
}),
|
|
431
675
|
});
|
|
432
676
|
});
|
|
433
677
|
});
|
|
434
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
|
+
}
|
|
435
778
|
function requestSource(req) {
|
|
436
779
|
const forwarded = req.headers["x-forwarded-for"];
|
|
437
780
|
if (typeof forwarded === "string" && forwarded.trim()) {
|