@deksden-com/dd-flow-cli 0.9.0-beta.123 → 0.9.0-beta.124
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 +10 -0
- package/README.md +23 -0
- package/dist/build-info.json +4 -4
- package/dist/cli/run-cli.js +1 -1
- package/dist/domain/stage-catalog.js +1 -1
- package/dist/harness-runtime/lib/codex-capacity-policy.d.mts +10 -0
- package/dist/harness-runtime/lib/codex-capacity-policy.mjs +89 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +100 -3
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +66 -16
- package/dist/harness-runtime/lib/dd-agy.mjs +13 -3
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +39 -10
- package/dist/harness-runtime/lib/dd-codex.mjs +11 -25
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +19 -12
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +42 -10
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +57 -14
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +25 -23
- package/dist/harness-runtime/lib/dd-zcode.mjs +9 -10
- package/dist/harness-runtime/lib/driver-recovery.mjs +12 -2
- package/dist/harness-runtime/lib/managed-daemon.d.mts +2 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +100 -32
- package/dist/schemas/flow-run.schema.json +1 -0
- package/dist/schemas/plan-review-decision.schema.json +3 -3
- package/dist/services/code-checks.js +14 -18
- package/dist/services/codex-capacity.js +149 -48
- package/dist/services/controller-fanout.js +1 -0
- package/dist/services/external-work-launch.js +10 -3
- package/dist/services/harness-adapter.js +21 -15
- package/dist/services/managed-processes.js +14 -6
- package/dist/services/merge-server.js +10 -4
- package/dist/services/review-deferral.js +23 -0
- package/dist/services/run-controller-adapter.js +8 -5
- package/dist/services/run-controller-recovery.js +17 -0
- package/dist/services/run-controller.js +42 -14
- package/dist/services/run-recovery-runtime.js +49 -3
- package/dist/services/runs.js +1 -1
- package/dist/services/verification-matrix.js +30 -1
- package/dist/services/vnext-code-review.js +16 -4
- package/dist/services/vnext-merge.js +20 -2
- package/dist/services/vnext-plan-review.js +81 -18
- package/dist/services/work-registry.js +9 -2
- package/package.json +1 -1
|
@@ -2,8 +2,9 @@ import { captureDispatchGuard, cancelPendingDispatch } from "./dispatch-fence.mj
|
|
|
2
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
3
|
import { chmod, lstat, mkdir, open, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
4
4
|
import net from "node:net";
|
|
5
|
-
import { durableDaemonDispatch } from "./daemon-operations.mjs";
|
|
5
|
+
import { durableDaemonDispatch, completeDaemonShutdown, assertDaemonDispatch, assertDaemonRestartable, trackDaemonConnections, retireDaemonEndpoint, retireFailedDaemon } from "./daemon-operations.mjs";
|
|
6
6
|
import { observedTimeout } from "./observation-clock.mjs";
|
|
7
|
+
import { parseCapacityOperation } from "./codex-capacity-policy.mjs";
|
|
7
8
|
import path from "node:path";
|
|
8
9
|
import { spawn } from "node:child_process";
|
|
9
10
|
import { waitForSettlement } from "./session-settlement.mjs";
|
|
@@ -84,23 +85,23 @@ export async function startDaemon(options) {
|
|
|
84
85
|
const paths = locations(options.stateDir); const cwd = absolute(options.cwd, "--cwd"); const config = { cwd, journal: absolute(options.journal, "--journal"), bin: options.bin ?? null, ddFlowBin: options.ddFlowBin ? absolute(options.ddFlowBin, "--dd-flow-bin") : null, ddFlowHome: options.ddFlowHome ? absolute(options.ddFlowHome, "--dd-flow-home") : null, projectRoot: options.projectRoot ? absolute(options.projectRoot, "--project-root") : cwd, resourceHome: process.env.DD_FLOW_RESOURCE_HOME ? absolute(process.env.DD_FLOW_RESOURCE_HOME, "DD_FLOW_RESOURCE_HOME") : null, env: options.ddFlowHome ? { DD_FLOW_HOME: absolute(options.ddFlowHome, "--dd-flow-home"), ...(options.ddFlowBin ? { DD_FLOW_BIN: absolute(options.ddFlowBin, "--dd-flow-bin") } : {}) } : {} };
|
|
85
86
|
await prepareRuntimeOwner(config, paths.dir);
|
|
86
87
|
await mkdir(paths.dir, { recursive: true, mode: 0o700 }); await chmod(paths.dir, 0o700);
|
|
88
|
+
let previous = await readState(paths.state); assertDaemonRestartable(previous);
|
|
87
89
|
try { const status = await callDaemon(paths.dir, "daemon.status", {}, 1_000); if (JSON.stringify(status.config) !== JSON.stringify(config)) throw Object.assign(new Error("running daemon has a different configuration"), { code: "daemon_config_mismatch" }); return { ...status, already_running: true }; } catch (error) { if (!["daemon_not_running", "daemon_timeout"].includes(error.code)) throw error; }
|
|
88
|
-
|
|
90
|
+
previous = await readState(paths.state); assertDaemonRestartable(previous);
|
|
91
|
+
if (previous?.shutdown_state === "running") throw Object.assign(new Error("previous dd-codex daemon died; do not reuse its state directory"), { code: "invalid_harness_crash" });
|
|
89
92
|
if (previous?.shutdown_state === "clean") await authorizeRetainedDaemonResume(paths.dir, previous, options.sessionId, config);
|
|
90
93
|
await removeSocket(paths.socket); const resourceProcess = await registerDaemonProcess(config, { kind: "codex-daemon", owner: `codex:${path.basename(paths.dir)}`, operation: `codex-daemon:${path.basename(paths.dir)}`, stdout: paths.log, stderr: paths.log }); const state = { schema_id: stateSchema, daemon_id: randomUUID(), pid: null, cwd: config.cwd, journal: config.journal, config, socket: paths.socket, started_at: new Date().toISOString(), shutdown_state: "starting", sessions: previous?.sessions ?? [], resource_process: resourceProcess }; await writeState(paths.state, state);
|
|
91
94
|
const log = await open(paths.log, "a", 0o600); const child = spawn(process.execPath, [options.entryPath, "daemon", "serve", "--state-dir", paths.dir], { cwd: config.cwd, detached: true, stdio: ["ignore", log.fd, log.fd] }); child.unref(); await log.close();
|
|
92
95
|
try { await confirmDaemonProcess(config, resourceProcess, child); const deadline = Date.now() + 60_000; while (Date.now() < deadline) { await new Promise((resolve) => setTimeout(resolve, 100)); try { return await callDaemon(paths.dir, "daemon.status", {}, 5_000); } catch (error) { if (!["daemon_not_running", "daemon_timeout"].includes(error.code)) throw error; } } throw Object.assign(new Error("daemon did not become ready"), { code: "daemon_start_failed" }); }
|
|
93
96
|
catch (error) { await cleanupFailedDaemonStart(config, resourceProcess, child, error, () => readState(paths.state), state.daemon_id); throw error; }
|
|
94
97
|
}
|
|
95
|
-
export async function stopDaemon(options) { const timeoutMs = options.timeoutMs ?? 30_000; return await confirmDaemonStopped(() => callDaemon(options.stateDir, "daemon.stop", { cancelTree: options.cancelTree === true }, timeoutMs), locations(options.stateDir).socket, timeoutMs); }
|
|
98
|
+
export async function stopDaemon(options) { const timeoutMs = options.timeoutMs ?? 30_000; return await confirmDaemonStopped(() => callDaemon(options.stateDir, "daemon.stop", { cancelTree: options.cancelTree === true }, timeoutMs), locations(options.stateDir).socket, timeoutMs, { readState: () => readState(locations(options.stateDir).state) }); }
|
|
96
99
|
|
|
97
100
|
export async function serveDaemon(stateDir) {
|
|
98
101
|
const paths = locations(stateDir); const state = await readState(paths.state); if (!state || state.schema_id !== stateSchema) throw Object.assign(new Error("daemon state missing"), { code: "daemon_state_missing" });
|
|
99
102
|
const bridge = new CodexBridge({ ...state.config, env: { ...state.config.env, CODEX_HOME: process.env.CODEX_HOME, DD_FLOW_DAEMON_ID: state.daemon_id, DD_FLOW_CODEX_STATE_DIR: paths.dir } }); await startManagedBridge({ config: state.config, state, bridge, kind: "codex-provider", publish: snapshot => writeState(paths.state, snapshot) }); const sessions = new Set(state.sessions ?? []); let active = null; let server; let persist = Promise.resolve();
|
|
100
|
-
const closeBridge = bridge.close.bind(bridge);
|
|
101
|
-
bridge.close = async () => { await closeBridge(); await finishDaemonProcess(state.config, state.provider_process, "stopped", "daemon_stopped"); };
|
|
102
103
|
scheduleDaemonProcessHeartbeat(state.config, state.resource_process);
|
|
103
|
-
const save = (patch = {}) => { Object.assign(state, patch, { pid: process.pid, updated_at: new Date().toISOString(), sessions: [...sessions] }); const snapshot = structuredClone(state); persist = persist.then(() => writeState(paths.state, snapshot)); return persist; };
|
|
104
|
+
const save = (patch = {}) => { Object.assign(state, patch, { pid: process.pid, updated_at: new Date().toISOString(), sessions: [...sessions] }); const snapshot = structuredClone(state); persist = persist.catch(() => {}).then(() => writeState(paths.state, snapshot)); return persist; };
|
|
104
105
|
const dispatch = async (operation, params, operationId) => {
|
|
105
106
|
const assertDispatch = captureDispatchGuard(bridge);
|
|
106
107
|
if (operation === "hook.verify") {
|
|
@@ -113,6 +114,7 @@ export async function serveDaemon(stateDir) {
|
|
|
113
114
|
// available while a request is marked active; callers must first prove the
|
|
114
115
|
// provider Session itself is idle.
|
|
115
116
|
if (operation === "daemon.stop") {
|
|
117
|
+
return await completeDaemonShutdown(paths.dir, state, save, async () => {
|
|
116
118
|
if (params.cancelTree) cancelPendingDispatch(bridge);
|
|
117
119
|
if (active && !params.cancelTree) throw Object.assign(new Error("productive operation is running"), { code: "tree_not_settled" });
|
|
118
120
|
await waitForSettlement({
|
|
@@ -120,12 +122,18 @@ export async function serveDaemon(stateDir) {
|
|
|
120
122
|
observe: () => observeCodexSessions(bridge, state.config, sessions, () => active),
|
|
121
123
|
...(params.cancelTree ? { cancel: async sessionId => { try { await cancelSessionWithBridge(bridge, { ...state.config, sessionId }); } catch (error) { if (error.code !== "turn_not_active") throw error; } } } : {})
|
|
122
124
|
});
|
|
123
|
-
|
|
124
|
-
|
|
125
|
+
return { clean: true };
|
|
126
|
+
}, [
|
|
127
|
+
["provider", () => bridge.close()],
|
|
128
|
+
["provider_resource", () => finishDaemonProcess(state.config, state.provider_process)],
|
|
129
|
+
["daemon_resource", () => finishDaemonProcess(state.config, state.resource_process)]
|
|
130
|
+
]);
|
|
125
131
|
}
|
|
132
|
+
if (["session.create", "session.prompt", "session.start", "session.fork", "session.resume"].includes(operation)) assertDaemonDispatch(paths.dir);
|
|
126
133
|
if (active && !["session.inspect", "session.cancel"].includes(operation)) throw Object.assign(new Error(`${active} is in progress`), { code: "operation_busy", retryable: true });
|
|
127
134
|
if (operation === "session.create") { active = operation; try { await save({ active_operation: active }); const result = await createSessionWithBridge(bridge, { ...state.config, ...params }); sessions.add(result.provider_session_id); return result; } finally { active = null; await save({ active_operation: null }); } }
|
|
128
135
|
if (operation === "session.prompt") {
|
|
136
|
+
parseCapacityOperation(operationId);
|
|
129
137
|
// Admission can wait for a permit. Observe again after it returns, not
|
|
130
138
|
// just before the caller's backoff, so no child Turn races this dispatch.
|
|
131
139
|
// Reserve before the first await: distinct operation IDs are independent
|
|
@@ -135,7 +143,7 @@ export async function serveDaemon(stateDir) {
|
|
|
135
143
|
const tree = await inspectCodexTree(bridge, { ...state.config, ...params });
|
|
136
144
|
assertDispatch();
|
|
137
145
|
if (tree.settled !== true) throw Object.assign(new Error("native Session tree changed while waiting for admission"), { code: "tree_not_settled", details: { provider_session_id: params.sessionId, unsettled_sessions: tree.unsettled_sessions } });
|
|
138
|
-
if (/:capacity:[
|
|
146
|
+
if (/:capacity:[1-9]\d*:session\.prompt$|:child:[a-f0-9]+:followup:[1-9]\d*$/.test(operationId ?? ""))
|
|
139
147
|
await assertDaemonOwnership(paths.dir, { "operation-id": operationId, operation, "native-session-id": params.sessionId, "prompt-sha256": createHash("sha256").update(params.prompt ?? "").digest("hex") }, assertDispatch);
|
|
140
148
|
sessions.add(params.sessionId);
|
|
141
149
|
await save({ active_operation: active }); return await promptSessionWithBridge(bridge, { ...state.config, ...params, assertDispatch });
|
|
@@ -159,7 +167,28 @@ export async function serveDaemon(stateDir) {
|
|
|
159
167
|
}
|
|
160
168
|
throw Object.assign(new Error(`unknown operation: ${operation}`), { code: "unknown_operation" });
|
|
161
169
|
};
|
|
162
|
-
await removeSocket(paths.socket);
|
|
170
|
+
await removeSocket(paths.socket);
|
|
171
|
+
server = net.createServer(connection => {
|
|
172
|
+
connection.setEncoding("utf8"); let buffer = "", consumed = false;
|
|
173
|
+
connection.on("data", chunk => {
|
|
174
|
+
if (consumed) return;
|
|
175
|
+
buffer += chunk; const at = buffer.indexOf("\n"); if (at < 0) return; consumed = true;
|
|
176
|
+
void (async () => {
|
|
177
|
+
let request;
|
|
178
|
+
try {
|
|
179
|
+
request = JSON.parse(buffer.slice(0, at));
|
|
180
|
+
if (request.schema_id !== requestSchema || !request.id || !request.operation) throw Object.assign(new Error("invalid daemon request"), { code: "daemon_protocol" });
|
|
181
|
+
const result = await durableDaemonDispatch(paths.dir, request, () => dispatch(request.operation, request.params ?? {}, request.id), () => dispatch("session.inspect", request.params ?? {}));
|
|
182
|
+
if (result._shutdown) retireDaemonEndpoint(server);
|
|
183
|
+
connection.end(`${JSON.stringify({ schema_id: responseSchema, id: request.id, ok: true, result: { ...result, _shutdown: undefined } })}\n`);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
await retireFailedDaemon(server, state, error, save);
|
|
186
|
+
connection.end(`${JSON.stringify({ schema_id: responseSchema, id: request?.id ?? null, ok: false, error: errorPayload(error) })}\n`);
|
|
187
|
+
}
|
|
188
|
+
})();
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
trackDaemonConnections(server);
|
|
163
192
|
await new Promise((resolve, reject) => { server.once("error", reject); server.listen(paths.socket, resolve); }); await chmod(paths.socket, 0o600); await save({ shutdown_state: "running", active_operation: null }); return await new Promise(() => {});
|
|
164
193
|
}
|
|
165
194
|
import { authorizeRetainedDaemonResume } from "./driver-recovery.mjs";
|
|
@@ -2,7 +2,8 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { appendFile, mkdir, readFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import readline from "node:readline";
|
|
5
|
-
import {
|
|
5
|
+
import { closeBridgeProcess } from "./managed-daemon.mjs";
|
|
6
|
+
import { errorRecord } from "./operation-errors.mjs";
|
|
6
7
|
import { ObservationClock, observedTimeout } from "./observation-clock.mjs";
|
|
7
8
|
import { observeModel } from "./model-observations.mjs";
|
|
8
9
|
|
|
@@ -230,7 +231,7 @@ export class CodexBridge {
|
|
|
230
231
|
this.descendants.set(threadId, { ...previous, status: "running", active_turn_id: turn.id, terminal_turn_id: null, provider_error: null });
|
|
231
232
|
} else if (value.method === "turn/completed" && (!previous.active_turn_id || previous.active_turn_id === turn.id)) {
|
|
232
233
|
this.descendants.set(threadId, { ...previous, status: turn.status === "completed" ? "completed" : turn.status ?? "unknown",
|
|
233
|
-
active_turn_id: turn.id, terminal_turn_id: turn.id, terminal_observed_at: now(), provider_error: nativeTurnError(this, threadId, turn), native_turn_items: turnItemEvidence(turn) });
|
|
234
|
+
active_turn_id: turn.id, terminal_turn_id: turn.id, terminal_observed_at: previous.terminal_turn_id === turn.id ? previous.terminal_observed_at ?? now() : now(), provider_error: nativeTurnError(this, threadId, turn), native_turn_items: turnItemEvidence(turn) });
|
|
234
235
|
}
|
|
235
236
|
}
|
|
236
237
|
}
|
|
@@ -312,16 +313,16 @@ export class CodexBridge {
|
|
|
312
313
|
}
|
|
313
314
|
rejectAll(error) { for (const pending of this.pending.values()) { clearTimeout(pending.timer); pending.reject(error); } this.pending.clear(); }
|
|
314
315
|
async close() {
|
|
315
|
-
|
|
316
|
-
this.child.stdin.end();
|
|
317
|
-
await Promise.race([new Promise((resolve) => this.child.once("exit", resolve)), new Promise((resolve) => setTimeout(resolve, 2_000))]);
|
|
318
|
-
await stopProcessGroup(this.child, 2_000);
|
|
319
|
-
await this.modelObservations;
|
|
320
|
-
await this.journal.flush();
|
|
316
|
+
return await closeBridgeProcess(this, async () => { await this.modelObservations; await this.journal.flush(); });
|
|
321
317
|
}
|
|
322
318
|
}
|
|
323
319
|
|
|
324
|
-
async function withBridge(options, action) {
|
|
320
|
+
async function withBridge(options, action) {
|
|
321
|
+
const bridge = new CodexBridge(options); let primary;
|
|
322
|
+
try { await bridge.start(); return await action(bridge); }
|
|
323
|
+
catch (error) { primary = error; throw error; }
|
|
324
|
+
finally { try { await bridge.close(); } catch (error) { if (!primary) throw error; primary.cleanup_error = errorRecord(error); primary.details = { ...primary.details, cleanup_error: primary.cleanup_error }; } }
|
|
325
|
+
}
|
|
325
326
|
function profile(options) { return { harness: "codex-desktop", model: options.model ?? null, reasoning: options.reasoning ?? null }; }
|
|
326
327
|
function threadId(result) { const id = result?.thread?.id ?? result?.threadId; return required(id, "Codex thread id"); }
|
|
327
328
|
function directChildren(bridge, sessionId) { return typeof bridge?.directChildren === "function" ? bridge.directChildren(sessionId) : []; }
|
|
@@ -547,22 +548,7 @@ function turnItemEvidence(turn) {
|
|
|
547
548
|
possible_effects: items.some(item => !["agentMessage", "reasoning", "userMessage"].includes(item?.type)) } : { observed: false };
|
|
548
549
|
}
|
|
549
550
|
|
|
550
|
-
export
|
|
551
|
-
let current = error;
|
|
552
|
-
for (let depth = 0; depth < 4 && current && typeof current === "object"; depth += 1) {
|
|
553
|
-
if (current.code === "turn_interrupted") {
|
|
554
|
-
const details = current.details ?? {}, native = details.provider_error;
|
|
555
|
-
const code = native?.codexErrorInfo;
|
|
556
|
-
return details.terminal_status === "failed" && typeof details.provider_session_id === "string" && details.provider_session_id === expectedSessionId
|
|
557
|
-
&& typeof details.turn_id === "string" && Boolean(details.turn_id) && details.native_turn_id === details.turn_id
|
|
558
|
-
&& (expectedTurnId === undefined || details.turn_id === expectedTurnId)
|
|
559
|
-
&& (code === "serverOverloaded" || code?.code === "serverOverloaded");
|
|
560
|
-
}
|
|
561
|
-
if (current.code !== "harness_adapter_failed") return false;
|
|
562
|
-
current = current.details?.cause ?? current.cause;
|
|
563
|
-
}
|
|
564
|
-
return false;
|
|
565
|
-
}
|
|
551
|
+
export { terminalCodexOverload } from "./codex-capacity-policy.mjs";
|
|
566
552
|
|
|
567
553
|
function isTerminalTurnStatus(status) { return ["completed", "failed", "interrupted", "cancelled"].includes(status); }
|
|
568
554
|
function isAuthoritativeTerminal(bridge, turnId, status, threadStatus, failureNotBefore) {
|
|
@@ -4,7 +4,7 @@ import { spawn } from "node:child_process";
|
|
|
4
4
|
import net from "node:net";
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import { DroidError, DroidRuntime, absolute, droidPaths, droidVersion, prepareDroidHome, readJson, removeDroidAuth, processSnapshot } from "./dd-droid.mjs";
|
|
7
|
-
import { durableDaemonDispatch, inspectDaemonOperation } from "./daemon-operations.mjs";
|
|
7
|
+
import { durableDaemonDispatch, inspectDaemonOperation, completeDaemonShutdown, commitDaemonShutdown, assertDaemonDispatch, assertDaemonRestartable, trackDaemonConnections, retireDaemonEndpoint, retireFailedDaemon } from "./daemon-operations.mjs";
|
|
8
8
|
import { observedTimeout } from "./observation-clock.mjs";
|
|
9
9
|
import { writeJsonAtomic } from "./runner-events.mjs";
|
|
10
10
|
import { errorRecord } from "./operation-errors.mjs";
|
|
@@ -33,8 +33,9 @@ export async function startDaemon(options) {
|
|
|
33
33
|
config.cwd = await realpath(config.cwd); config.projectRoot = await realpath(config.projectRoot);
|
|
34
34
|
if (config.mode !== "auto-high") throw new DroidError("droid_mode_unsupported", "The qualified Droid profile requires auto-high");
|
|
35
35
|
await mkdir(paths.dir, { recursive: true, mode: 0o700 }); await chmod(paths.dir, 0o700);
|
|
36
|
+
let previous = await readJson(paths.state); assertDaemonRestartable(previous);
|
|
36
37
|
try { const live = await callDaemon(paths.dir, "daemon.status", {}, 1_000); if (!sameConfig(live.config, config)) throw new DroidError("daemon_config_mismatch", "Droid daemon already uses another configuration"); return { ...live, already_running: true }; } catch (error) { if (!["daemon_not_running", "daemon_timeout"].includes(error.code)) throw error; }
|
|
37
|
-
|
|
38
|
+
previous = await readJson(paths.state); assertDaemonRestartable(previous);
|
|
38
39
|
if (previous) {
|
|
39
40
|
const rows = await processSnapshot();
|
|
40
41
|
if (rows.some(row => !row.zombie && ([previous.pid, previous.provider_pid].includes(row.pid) || previous.owned_processes?.some(old => old.pid === row.pid && old.started === row.started)))) throw new DroidError("invalid_harness_crash", "Previous daemon or provider ownership remains; refusing another owner");
|
|
@@ -53,11 +54,11 @@ export async function startDaemon(options) {
|
|
|
53
54
|
throw new DroidError("daemon_start_failed", "Droid daemon did not become ready");
|
|
54
55
|
} catch (error) { await cleanupFailedStart(config, resource, child, error); throw error; }
|
|
55
56
|
}
|
|
56
|
-
export async function stopDaemon(options) { return await confirmDaemonStopped(() => callDaemon(options.stateDir, "daemon.stop", { cancelTree: options.cancelTree === true }, 30_000), droidPaths(options.stateDir).socket, 30_000); }
|
|
57
|
+
export async function stopDaemon(options) { return await confirmDaemonStopped(() => callDaemon(options.stateDir, "daemon.stop", { cancelTree: options.cancelTree === true }, 30_000), droidPaths(options.stateDir).socket, 30_000, { readState: () => readJson(droidPaths(options.stateDir).state) }); }
|
|
57
58
|
export async function serveDaemon(stateDir) {
|
|
58
59
|
const paths = droidPaths(stateDir), state = await readJson(paths.state); if (!state?.config) throw new DroidError("daemon_state_missing", "Missing Droid daemon state");
|
|
59
60
|
let saving = Promise.resolve(), busy = null, stopping = false;
|
|
60
|
-
const save = patch => { Object.assign(state, patch, { pid: process.pid, updated_at: new Date().toISOString() }); const snapshot = structuredClone(state); saving = saving.then(() => writeJsonAtomic(paths.state, snapshot)); return saving; };
|
|
61
|
+
const save = patch => { Object.assign(state, patch, { pid: process.pid, updated_at: new Date().toISOString() }); const snapshot = structuredClone(state); saving = saving.catch(() => {}).then(() => writeJsonAtomic(paths.state, snapshot)); return saving; };
|
|
61
62
|
const runtime = new DroidRuntime(state.config, state, save);
|
|
62
63
|
const dispatch = async request => {
|
|
63
64
|
const params = request.params ?? {}, operation = request.operation;
|
|
@@ -71,24 +72,29 @@ export async function serveDaemon(stateDir) {
|
|
|
71
72
|
return { ...inspected, cancelled: true, settled: cleanup.settled, cleanup };
|
|
72
73
|
}
|
|
73
74
|
if (operation === "daemon.stop") {
|
|
74
|
-
if (stopping) throw new DroidError("operation_busy", "Droid shutdown is already in progress");
|
|
75
|
-
if (!params.cancelTree && (busy || (runtime.rootId && !(await runtime.inspect(runtime.rootId)).settled))) throw new DroidError("tree_not_settled", "Droid still owns unfinished work");
|
|
76
75
|
stopping = true;
|
|
77
76
|
try {
|
|
77
|
+
return await completeDaemonShutdown(paths.dir, state, save, async () => {
|
|
78
|
+
if (!params.cancelTree && (busy || (runtime.rootId && !(await runtime.inspect(runtime.rootId)).settled))) throw new DroidError("tree_not_settled", "Droid still owns unfinished work");
|
|
79
|
+
commitDaemonShutdown(paths.dir);
|
|
78
80
|
const cleanup = await runtime.closeTree(params.cancelTree === true);
|
|
79
81
|
if (runtime.rootId) try { await runtime.inspect(runtime.rootId, { ignoreActive: true }); } catch (error) { cleanup.inspection_error = errorRecord(error); }
|
|
80
82
|
// Native evidence failures remain visible, but cannot strand copied
|
|
81
83
|
// credentials or the daemon after the owned process tree is gone.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
84
|
+
const clean = !cleanup.forced && !cleanup.topology_error && !cleanup.inspection_error;
|
|
85
|
+
return { clean, cleanup };
|
|
86
|
+
}, [
|
|
87
|
+
["auth", () => removeDroidAuth(state.config)],
|
|
88
|
+
["daemon_resource", () => finishDaemonProcess(state.config, state.resource_process)]
|
|
89
|
+
]);
|
|
85
90
|
}
|
|
86
|
-
catch (error) {
|
|
91
|
+
catch (error) { if (error.code === "tree_not_settled" && !runtime.closed) stopping = false; throw error; }
|
|
87
92
|
}
|
|
88
93
|
if (busy || stopping) throw new DroidError("operation_busy", "Another Droid operation is active");
|
|
89
94
|
busy = operation;
|
|
90
95
|
try {
|
|
91
96
|
await save({ active_operation: busy });
|
|
97
|
+
assertDaemonDispatch(paths.dir);
|
|
92
98
|
if (operation === "session.create") { if (params.prompt) throw new DroidError("invalid_create", "create does not execute a prompt"); return await runtime.create(request.id); }
|
|
93
99
|
if (operation === "session.resume") return await runtime.resume(params.sessionId);
|
|
94
100
|
if (["session.prompt", "session.start"].includes(operation)) return await runtime.prompt(params.sessionId, params.prompt, request.id);
|
|
@@ -114,10 +120,11 @@ export async function serveDaemon(stateDir) {
|
|
|
114
120
|
// durable stop receipt while the Unix pathname is atomically retired.
|
|
115
121
|
// Unlinking it from the request handler races that final response and
|
|
116
122
|
// can leave a stop caller observing the socket for its whole timeout.
|
|
117
|
-
if (_shutdown) server
|
|
123
|
+
if (_shutdown) retireDaemonEndpoint(server);
|
|
118
124
|
socket.end(`${JSON.stringify({ schema_id: RESPONSE, id: request.id, ok: true, result: receipt })}\n`);
|
|
119
|
-
} catch (error) { socket.end(`${JSON.stringify({ schema_id: RESPONSE, id: request?.id, ok: false, error: errorRecord(error) })}\n`); } })();
|
|
125
|
+
} catch (error) { await retireFailedDaemon(server, state, error, save); socket.end(`${JSON.stringify({ schema_id: RESPONSE, id: request?.id, ok: false, error: errorRecord(error) })}\n`); } })();
|
|
120
126
|
}); });
|
|
127
|
+
trackDaemonConnections(server);
|
|
121
128
|
try { await runtime.start(); scheduleDaemonProcessHeartbeat(state.config, state.resource_process); await save({ shutdown_state: "running" }); await new Promise((resolve, reject) => { server.once("error", reject); server.listen(paths.socket, resolve); }); await chmod(paths.socket, 0o600); }
|
|
122
129
|
catch (error) { let cleanup; try { cleanup = await runtime.closeTree(true); await removeDroidAuth(state.config); } catch (failure) { await save({ shutdown_state: "cleanup_failed", startup_error: errorRecord(error), cleanup_error: errorRecord(failure) }); throw error; } await save({ shutdown_state: "failed", startup_error: errorRecord(error), cleanup }); await finishDaemonProcess(state.config, state.resource_process, "failed", "startup_failed"); throw error; }
|
|
123
130
|
await new Promise(resolve => server.once("close", resolve));
|
|
@@ -5,7 +5,7 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
5
5
|
import { chmod, copyFile, cp, lstat, mkdir, open, readFile, readdir, rename, unlink, writeFile } from "node:fs/promises";
|
|
6
6
|
import { spawn } from "node:child_process";
|
|
7
7
|
import net from "node:net";
|
|
8
|
-
import { durableDaemonDispatch as dispatchOperation } from "./daemon-operations.mjs";
|
|
8
|
+
import { durableDaemonDispatch as dispatchOperation, completeDaemonShutdown, trackDaemonConnections, retireDaemonEndpoint, retireFailedDaemon, assertDaemonDispatch, assertDaemonRestartable } from "./daemon-operations.mjs";
|
|
9
9
|
import { waitForSettlement } from "./session-settlement.mjs";
|
|
10
10
|
import os from "node:os";
|
|
11
11
|
import path from "node:path";
|
|
@@ -13,7 +13,7 @@ import path from "node:path";
|
|
|
13
13
|
import { AcpBridge } from "./dd-zcode.mjs";
|
|
14
14
|
import { observeModel, readModelObservations } from "./model-observations.mjs";
|
|
15
15
|
import { cancelSessionWithBridge, createSessionWithBridge, doctor, forkSessionWithBridge, inspectSessionWithBridge, promptSessionWithBridge } from "./dd-grok.mjs";
|
|
16
|
-
import { prepareRuntimeOwner, cleanupFailedDaemonStart,
|
|
16
|
+
import { prepareRuntimeOwner, cleanupFailedDaemonStart, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, scheduleDaemonProcessHeartbeat, registerDaemonProcess, startManagedBridge, stopProcessGroup } from "./managed-daemon.mjs";
|
|
17
17
|
|
|
18
18
|
const REQUEST_SCHEMA = "dd-grok/daemon-request@1";
|
|
19
19
|
const RESPONSE_SCHEMA = "dd-grok/daemon-response@1";
|
|
@@ -169,8 +169,10 @@ export async function startDaemon(options) {
|
|
|
169
169
|
await prepareRuntimeOwner(config, paths.dir);
|
|
170
170
|
if (!config.model || !config.reasoning) throw new DaemonError("profile_required", "--model and --reasoning are required");
|
|
171
171
|
await mkdir(paths.dir, { recursive: true, mode: 0o700 }); await chmod(paths.dir, 0o700);
|
|
172
|
+
let previous = await readState(paths.dir); assertDaemonRestartable(previous);
|
|
172
173
|
try { const status = await callDaemon(paths.dir, "daemon.ready", {}, 5000); if (!sameConfig(status, config)) throw new DaemonError("daemon_config_mismatch", "a daemon is already running with different configuration"); if (!status.ready) throw new DaemonError("daemon_start_in_progress", "an existing daemon is still starting", true); return { ...status, already_running: true }; } catch (error) { if (error.code !== "daemon_not_running") throw error; }
|
|
173
|
-
|
|
174
|
+
previous = await readState(paths.dir); assertDaemonRestartable(previous);
|
|
175
|
+
if (previous?.shutdown_state === "clean") await authorizeRetainedDaemonResume(paths.dir, previous, options.sessionId, config); if (previous?.shutdown_state === "running" && previous.active_tree) throw new DaemonError("invalid_harness_crash", "previous daemon died with an active or unproven Session tree", false, { daemon_id: previous.daemon_id });
|
|
174
176
|
await removeSocket(paths.socket, paths.dir); await mkdir(paths.home, { recursive: true, mode: 0o700 }); const importedSession = await materializeSessionArchive(paths, config.sessionArchive); const copiedAuth = await copyAuth(paths, authSource); await writeIsolatedConfig(paths); if (!noFlow) await writeHook(paths, config, absolute(options.entryPath, "dd-grok entry")); const versions = await doctor({ bin: config.bin }); if (importedSession && importedSession.grok_version !== versions.versions.grok) throw new DaemonError("session_archive_version_mismatch", "Grok Build session archive was created by a different CLI version", false, { archive_version: importedSession.grok_version, current_version: versions.versions.grok }); const configIsolation = await inspectIsolatedConfig(config);
|
|
175
177
|
const resourceProcess = await registerDaemonProcess(config, { kind: "grok-daemon", owner: `grok:${path.basename(paths.dir)}`, operation: `grok-daemon:${path.basename(paths.dir)}`, stdout: paths.log, stderr: paths.log });
|
|
176
178
|
const daemonId = randomUUID();
|
|
@@ -179,11 +181,11 @@ export async function startDaemon(options) {
|
|
|
179
181
|
try { await confirmDaemonProcess(config, resourceProcess, child); const deadline = Date.now() + 120_000; while (Date.now() < deadline) { await new Promise((resolve) => setTimeout(resolve, 100)); const failure = grokStartupFailure(await readState(paths.dir)); if (failure) throw failure; try { const status = await callDaemon(paths.dir, "daemon.ready", {}, 5000); if (status.ready) return status; } catch (error) { if (error.code !== "daemon_not_running" && error.code !== "daemon_timeout") throw error; } } throw new DaemonError("daemon_start_failed", "daemon did not become ready", true); }
|
|
180
182
|
catch (error) { await cleanupFailedDaemonStart(config, resourceProcess, child, error, () => readState(paths.dir), daemonId); throw error; }
|
|
181
183
|
}
|
|
182
|
-
export async function stopDaemon(options) { const timeoutMs = options.timeoutMs ?? 30_000; return await confirmDaemonStopped(() => callDaemon(options.stateDir, "daemon.stop", { cancelTree: options.cancelTree === true }, timeoutMs), locations(options.stateDir).socket, timeoutMs); }
|
|
184
|
+
export async function stopDaemon(options) { const timeoutMs = options.timeoutMs ?? 30_000; return await confirmDaemonStopped(() => callDaemon(options.stateDir, "daemon.stop", { cancelTree: options.cancelTree === true }, timeoutMs), locations(options.stateDir).socket, timeoutMs, { readState: () => readState(options.stateDir) }); }
|
|
183
185
|
|
|
184
186
|
export class Runtime {
|
|
185
187
|
constructor(paths, state, bridge, initialized) { this.paths = paths; this.state = state; this.bridge = bridge; this.initialized = initialized; this.sessions = new Map((state.sessions ?? []).map((item) => [item.provider_session_id, item])); this.descendants = new Map((state.descendants ?? []).map(child => [child.provider_session_id, child])); for (const session of this.sessions.values()) if (session.parent_provider_session_id && !this.descendants.has(session.provider_session_id)) this.descendants.set(session.provider_session_id, { provider_session_id: session.provider_session_id, parent_provider_session_id: session.parent_provider_session_id, status: "unknown", source: "legacy.daemon_state", outcome_error: { code: "native_child_outcome_unavailable", message: "Retained Grok child has no durable outcome receipt" } }); this.toolUsage = new Map(); this.loadedSessionId = null; this.active = null; this.server = null; this.ready = false; this.persisting = Promise.resolve(); }
|
|
186
|
-
persist(patch = {}) { Object.assign(this.state, patch, { pid: process.pid, updated_at: new Date().toISOString(), sessions: [...this.sessions.values()], descendants: [...this.descendants.values()] }); const snapshot = structuredClone(this.state); this.persisting = this.persisting.then(() => writeState(this.paths.state, snapshot)); return this.persisting; }
|
|
188
|
+
persist(patch = {}) { Object.assign(this.state, patch, { pid: process.pid, updated_at: new Date().toISOString(), sessions: [...this.sessions.values()], descendants: [...this.descendants.values()] }); const snapshot = structuredClone(this.state); this.persisting = this.persisting.catch(() => {}).then(() => writeState(this.paths.state, snapshot)); return this.persisting; }
|
|
187
189
|
options(params) { const sessionId = params.sessionId; const session = sessionId ? this.sessions.get(sessionId) : null; const root = session?.root_provider_session_id ?? sessionId; return { ...this.state.config, ...params, cwd: params.cwd ?? session?.cwd ?? this.state.config.cwd, initialized: this.initialized, daemonId: this.state.daemon_id, rootProviderSessionId: root, toolUsage: this.toolUsage, allowBackground: true, liveSession: params.liveSession ?? Boolean(sessionId && sessionId === this.loadedSessionId) }; }
|
|
188
190
|
track(result, before = null) { if (!result?.provider_session_id) return; const session = this.sessions.get(result.provider_session_id); const root = result.parent_provider_session_id ? (this.sessions.get(result.parent_provider_session_id)?.root_provider_session_id ?? result.parent_provider_session_id) : (session?.root_provider_session_id ?? result.provider_session_id); this.sessions.set(result.provider_session_id, { native_root_receipt: session?.native_root_receipt ?? null, provider_session_id: result.provider_session_id, adapter_session_id: result.adapter_session_id ?? result.provider_session_id, parent_provider_session_id: result.parent_provider_session_id ?? session?.parent_provider_session_id ?? null, root_provider_session_id: root, cwd: result.cwd ?? result.target?.newCwd ?? result.info?.cwd ?? session?.cwd ?? null }); for (const child of this.running(result)) this.recordDescendant({ ...child, stale_activity: Boolean(before && before.get(child.sessionId ?? child.subagentId ?? child.provider_session_id) !== this.descendants.get(child.sessionId ?? child.subagentId ?? child.provider_session_id)) }, root, "x.ai/subagent/list_running", "running"); for (const child of result.descendants ?? []) this.recordDescendant(child, root, child.source ?? "x.ai/subagent/event", child.status ?? "unknown"); }
|
|
189
191
|
recordDescendant(value, root, source, fallback) {
|
|
@@ -339,6 +341,7 @@ export class Runtime {
|
|
|
339
341
|
}
|
|
340
342
|
async requireSettled() { const running = await this.refreshTree(); if (running.length) throw new DaemonError("tree_not_settled", "daemon still owns a running Session tree", false, { sessions: running }); }
|
|
341
343
|
async productive(name, task, sessionId) {
|
|
344
|
+
assertDaemonDispatch(this.paths?.dir);
|
|
342
345
|
if (this.active) throw new DaemonError("operation_busy", `${this.active} is already running`, true);
|
|
343
346
|
this.active = name;
|
|
344
347
|
// Only a new native turn invalidates its own root proof. Creation, fork
|
|
@@ -346,6 +349,7 @@ export class Runtime {
|
|
|
346
349
|
if (name === "session.prompt" && this.sessions.has(sessionId)) this.sessions.get(sessionId).native_root_receipt = null;
|
|
347
350
|
try {
|
|
348
351
|
await this.persist({ active_tree: true, active_operation: name });
|
|
352
|
+
assertDaemonDispatch(this.paths?.dir);
|
|
349
353
|
const before = new Map(this.descendants);
|
|
350
354
|
const result = await task();
|
|
351
355
|
this.track(result, before);
|
|
@@ -371,8 +375,9 @@ export class Runtime {
|
|
|
371
375
|
}
|
|
372
376
|
async dispatch(operation, params) {
|
|
373
377
|
const assertDispatch = captureDispatchGuard(this);
|
|
378
|
+
if (["session.create", "session.prompt", "session.fork", "session.archive"].includes(operation)) assertDaemonDispatch(this.paths.dir);
|
|
374
379
|
if (operation === "daemon.ready") return { ready: this.ready, daemon_id: this.state.daemon_id, pid: process.pid, socket: this.paths.socket, versions: this.state.versions, config_isolation: this.state.config_isolation, shutdown_state: this.state.shutdown_state, recovery_status: this.state.recovery_status, auth_status: this.state.auth_status, active_tree: this.state.active_tree, active_operation: this.active, sessions: [...this.sessions.values()], config: this.state.config };
|
|
375
|
-
if (operation === "daemon.status") { let observationError; if (!this.active) try { await this.refreshTree(); } catch (error) { observationError = errorPayload(error); } return { ...await this.dispatch("daemon.ready", {}), observation_error: observationError ?? this.state.observation_error ?? null }; }
|
|
380
|
+
if (operation === "daemon.status") { let observationError; if (!this.active && !this.state.shutdown?.phases?.tree) try { await this.refreshTree(); } catch (error) { observationError = errorPayload(error); } return { ...await this.dispatch("daemon.ready", {}), observation_error: observationError ?? this.state.observation_error ?? null }; }
|
|
376
381
|
if (operation === "hook.resolve") { const sessionId = String(params.sessionId ?? ""); if (!sessionId) throw new DaemonError("hook_identity_missing", "hook has no sessionId"); let session = this.sessions.get(sessionId); if (!session) { const roots = [...this.sessions.values()].filter((item) => !item.parent_provider_session_id); if (roots.length !== 1) throw new DaemonError("hook_identity_unknown", "unknown hook Session has no unique root"); const observed = await inspectSessionWithBridge(this.bridge, this.options({ sessionId, liveSession: true })); this.track(observed); session = this.sessions.get(sessionId); if (!session || session.root_provider_session_id !== roots[0].provider_session_id) throw new DaemonError("hook_identity_unknown", "hook Session is not a member of the retained native tree"); await this.persist(); } return { daemonId: this.state.daemon_id, rootProviderSessionId: session.root_provider_session_id, parentProviderSessionId: session.parent_provider_session_id ?? null, observedProfile: this.bridge.modelProfiles?.get(sessionId) ?? {} }; }
|
|
377
382
|
if (operation === "session.create") return await this.productive(operation, async () => { const result = await createSessionWithBridge(this.bridge, { ...this.options(params), onSessionCreated: async (session) => { this.track(session); this.loadedSessionId = session.provider_session_id; await this.persist(); } }, this.initialized); this.loadedSessionId = result.provider_session_id; return result; });
|
|
378
383
|
if (operation === "session.prompt") { await this.requireSettled(); return await this.productive(operation, async () => { const result = await promptSessionWithBridge(this.bridge, { ...this.options(params), assertDispatch }); this.loadedSessionId = params.sessionId; return result; }, params.sessionId); }
|
|
@@ -381,18 +386,23 @@ export class Runtime {
|
|
|
381
386
|
if (operation === "session.cancel") { if (!this.sessions.has(params.sessionId)) throw new DaemonError("session_identity_mismatch", "cancellation Session is not owned by this daemon"); cancelPendingDispatch(this); const result = await cancelSessionWithBridge(this.bridge, this.options({ ...params, liveSession: true })); await this.persist({ active_tree: Boolean(this.active) || !result.settled }); return result; }
|
|
382
387
|
if (operation === "session.archive") { await this.requireSettled(); return await this.productive(operation, async () => { let session = this.sessions.get(params.sessionId); if (!session?.cwd) { const result = await inspectSessionWithBridge(this.bridge, this.options({ ...params, liveSession: true })); this.track(result); session = this.sessions.get(result.provider_session_id); } return await exportSessionArchive(this.paths, session.provider_session_id, absolute(session.cwd, "Grok Build session cwd"), params.archiveDir, this.state.versions); }); }
|
|
383
388
|
if (operation === "daemon.stop") {
|
|
389
|
+
return await completeDaemonShutdown(this.paths.dir, this.state, patch => this.persist(patch), async () => {
|
|
384
390
|
if (params.cancelTree) cancelPendingDispatch(this);
|
|
385
391
|
await waitForSettlement({
|
|
386
392
|
knownSessions: () => [...this.sessions.values()].filter(s => !s.parent_provider_session_id).map(s => s.provider_session_id),
|
|
387
393
|
observe: async () => ({ sessions: await this.refreshTree(), active: Boolean(this.active), active_operation: this.active }),
|
|
388
394
|
...(params.cancelTree ? { cancel: id => cancelSessionWithBridge(this.bridge, this.options({ sessionId: id, liveSession: true })) } : {})
|
|
389
395
|
});
|
|
390
|
-
|
|
391
|
-
|
|
396
|
+
return { clean: true };
|
|
397
|
+
}, [
|
|
398
|
+
["provider", () => this.bridge.close()],
|
|
399
|
+
["provider_resource", () => finishDaemonProcess(this.state.config, this.state.provider_process)],
|
|
400
|
+
["auth", () => unlink(path.join(this.paths.home, "auth.json")).catch(error => { if (error.code !== "ENOENT") throw error; })],
|
|
401
|
+
["daemon_resource", () => finishDaemonProcess(this.state.config, this.state.resource_process)]
|
|
402
|
+
]);
|
|
392
403
|
}
|
|
393
404
|
throw new DaemonError("unknown_operation", `unknown daemon operation: ${operation}`);
|
|
394
405
|
}
|
|
395
|
-
async shutdown() { await closeManagedBridge(this.state.config, this.state, this.bridge); await finishDaemonProcess(this.state.config, this.state.resource_process); await unlink(path.join(this.paths.home, "auth.json")).catch(error => { if (error.code !== "ENOENT") throw error; }); await this.persist({ shutdown_state: "clean", stopped_at: new Date().toISOString() }); await new Promise((resolve) => this.server.close(resolve)); await removeSocket(this.paths.socket, this.paths.dir); }
|
|
396
406
|
}
|
|
397
407
|
|
|
398
408
|
export async function serveDaemon(stateDir) {
|
|
@@ -426,6 +436,28 @@ export async function serveDaemon(stateDir) {
|
|
|
426
436
|
}
|
|
427
437
|
}
|
|
428
438
|
scheduleDaemonProcessHeartbeat(state.config, state.resource_process);
|
|
429
|
-
await removeSocket(paths.socket, paths.dir);
|
|
439
|
+
await removeSocket(paths.socket, paths.dir);
|
|
440
|
+
const server = net.createServer(connection => {
|
|
441
|
+
connection.setEncoding("utf8"); let buffer = "", consumed = false;
|
|
442
|
+
connection.on("data", chunk => {
|
|
443
|
+
if (consumed) return;
|
|
444
|
+
buffer += chunk; const at = buffer.indexOf("\n"); if (at < 0) return; consumed = true;
|
|
445
|
+
void (async () => {
|
|
446
|
+
let request;
|
|
447
|
+
try {
|
|
448
|
+
request = JSON.parse(buffer.slice(0, at));
|
|
449
|
+
if (request.schema_id !== REQUEST_SCHEMA || !request.id || !request.operation) throw new DaemonError("daemon_protocol_mismatch", "invalid daemon request");
|
|
450
|
+
const result = await durableDaemonDispatch(paths.dir, request, () => runtime.dispatch(request.operation, request.params ?? {}), () => runtime.dispatch("session.inspect", request.params ?? {}));
|
|
451
|
+
if (result._shutdown) retireDaemonEndpoint(server);
|
|
452
|
+
connection.end(`${JSON.stringify({ schema_id: RESPONSE_SCHEMA, id: request.id, ok: true, result: { ...result, _shutdown: undefined } })}\n`);
|
|
453
|
+
} catch (error) {
|
|
454
|
+
await retireFailedDaemon(server, state, error, patch => runtime.persist(patch));
|
|
455
|
+
connection.end(`${JSON.stringify({ schema_id: RESPONSE_SCHEMA, id: request?.id ?? null, ok: false, error: errorPayload(error) })}\n`);
|
|
456
|
+
}
|
|
457
|
+
})();
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
trackDaemonConnections(server); runtime.server = server;
|
|
461
|
+
await new Promise((resolve, reject) => { server.once("error", reject); server.listen(paths.socket, resolve); }); await chmod(paths.socket, 0o600); await runtime.persist({ shutdown_state: "running", active_tree: false }); runtime.ready = true; return await new Promise(() => {});
|
|
430
462
|
}
|
|
431
463
|
import { authorizeRetainedDaemonResume } from "./driver-recovery.mjs";
|
|
@@ -5,8 +5,9 @@ import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
|
5
5
|
import { appendFile, chmod, copyFile, lstat, mkdir, open, readFile, realpath, rename, unlink, writeFile } from "node:fs/promises";
|
|
6
6
|
import { spawn } from "node:child_process";
|
|
7
7
|
import net from "node:net";
|
|
8
|
-
import { durableDaemonDispatch } from "./daemon-operations.mjs";
|
|
8
|
+
import { durableDaemonDispatch, completeDaemonShutdown, trackDaemonConnections, retireDaemonEndpoint, retireFailedDaemon, assertDaemonDispatch, assertDaemonRestartable } from "./daemon-operations.mjs";
|
|
9
9
|
import { waitForSettlement } from "./session-settlement.mjs";
|
|
10
|
+
import { errorRecord } from "./operation-errors.mjs";
|
|
10
11
|
import { checkObservedProfile, observeModel } from "./model-observations.mjs";
|
|
11
12
|
import os from "node:os";
|
|
12
13
|
import path from "node:path";
|
|
@@ -23,7 +24,25 @@ async function readState(dir) { try { return JSON.parse(await readFile(locations
|
|
|
23
24
|
async function removeSocket(paths) { try { if (!(await lstat(paths.socket)).isSocket()) throw new DaemonError("unsafe_socket", "refusing to replace non-socket daemon path"); await unlink(paths.socket); } catch (error) { if (error.code !== "ENOENT") throw error; } }
|
|
24
25
|
function executable(bin, args = []) { return /\.[cm]?js$/.test(bin) ? { command: process.execPath, args: [bin, ...args] } : { command: bin, args }; }
|
|
25
26
|
function isolatedEnv(config) { return { ...process.env, XDG_DATA_HOME: config.dataHome, XDG_CONFIG_HOME: config.configHome, XDG_CACHE_HOME: config.cacheHome, OPENCODE_CONFIG_DIR: config.adapterDir, OPENCODE_DISABLE_PROJECT_CONFIG: "1", OPENCODE_SERVER_PASSWORD: config.password, ...(config.ddFlowHome ? { DD_FLOW_HOME: config.ddFlowHome } : {}), ...(config.resourceHome ? { DD_FLOW_RESOURCE_HOME: config.resourceHome } : {}), ...(config.ddFlowBin ? { DD_OPENCODE_DD_FLOW_BIN: config.ddFlowBin, DD_OPENCODE_NODE_BIN: process.execPath, DD_OPENCODE_PROJECT_ROOT: config.projectRoot } : {}), DD_OPENCODE_DAEMON_ID: config.daemonId, DD_OPENCODE_PROVIDER: config.provider, DD_OPENCODE_MODEL: config.model, DD_OPENCODE_VARIANT: config.variant, DD_OPENCODE_AGENT: config.agent, DD_OPENCODE_SERVER_VERSION: config.version ?? "unknown", DD_OPENCODE_PLUGIN_SHA256: config.pluginSha256 ?? "unknown" }; }
|
|
26
|
-
async function run(bin, args, config, { stdin, timeoutMs = 30_000 } = {}) {
|
|
27
|
+
async function run(bin, args, config, { stdin, timeoutMs = 30_000 } = {}) {
|
|
28
|
+
const target = executable(bin, args);
|
|
29
|
+
return await new Promise((resolve, reject) => {
|
|
30
|
+
const child = spawn(target.command, target.args, { cwd: config.cwd, env: isolatedEnv(config), detached: process.platform !== "win32", stdio: ["pipe", "pipe", "pipe"] });
|
|
31
|
+
let stdout = "", stderr = "", timeoutError, inputError, cleanup;
|
|
32
|
+
const timer = setTimeout(() => {
|
|
33
|
+
timeoutError = new DaemonError("opencode_timeout", `${args[0]} timed out`, true);
|
|
34
|
+
cleanup = stopProcessGroup(child, 1_000).catch(error => { timeoutError.details = { cleanup_error: errorRecord(error) }; reject(timeoutError); });
|
|
35
|
+
}, timeoutMs);
|
|
36
|
+
child.stdout.setEncoding("utf8").on("data", chunk => stdout += chunk); child.stderr.setEncoding("utf8").on("data", chunk => stderr += chunk);
|
|
37
|
+
child.on("error", error => { clearTimeout(timer); reject(error); });
|
|
38
|
+
child.on("close", async code => { clearTimeout(timer); await cleanup; if (timeoutError) reject(timeoutError); else if (inputError) reject(inputError); else if (code === 0) resolve(stdout); else reject(new DaemonError("opencode_cli_failed", stderr.trim() || `${target.command} exited ${code}`)); });
|
|
39
|
+
child.stdin.on("error", error => {
|
|
40
|
+
inputError ??= error;
|
|
41
|
+
cleanup ??= stopProcessGroup(child, 1_000).catch(cause => { clearTimeout(timer); inputError.details = { ...inputError.details, cleanup_error: errorRecord(cause) }; reject(timeoutError ?? inputError); });
|
|
42
|
+
});
|
|
43
|
+
child.stdin.end(stdin ?? "");
|
|
44
|
+
});
|
|
45
|
+
}
|
|
27
46
|
async function forwardUsage(config, sessionId, snapshot) {
|
|
28
47
|
if (!config.ddFlowBin) return;
|
|
29
48
|
const payload = { provider_session_id: sessionId, daemon_id: config.daemonId, observed_at: new Date().toISOString(), usage_scope: "physical_session", completeness: "complete", usage: { inputTokens: snapshot.input_tokens, outputTokens: snapshot.output_tokens, reasoningTokens: snapshot.reasoning_tokens, cacheReadInputTokens: snapshot.cached_input_tokens, cacheWriteInputTokens: snapshot.cache_write_tokens, totalTokens: snapshot.total_tokens, costUsd: snapshot.cost_usd }, tool_calls: snapshot.tool_calls };
|
|
@@ -53,8 +72,10 @@ export async function startDaemon(options) {
|
|
|
53
72
|
const paths = locations(options.stateDir); await mkdir(paths.dir, { recursive: true, mode: 0o700 }); await chmod(paths.dir, 0o700);
|
|
54
73
|
const previous = await readState(paths.dir), daemonId = randomUUID(), noFlow = options.noFlow === true, cwd = await realpath(absolute(options.cwd, "--cwd")); const config = { cwd, journal: options.journal ? absolute(options.journal, "--journal") : paths.journal, bin: options.bin ?? process.env.DD_OPENCODE_BIN ?? "opencode", provider: String(options.provider ?? ""), model: String(options.model ?? ""), variant: String(options.variant ?? "default"), agent: String(options.agent ?? "build"), projectRoot: noFlow ? cwd : await realpath(absolute(options.projectRoot, "--project-root")), ddFlowBin: noFlow ? null : absolute(options.ddFlowBin, "--dd-flow-bin"), ddFlowHome: noFlow ? null : absolute(options.ddFlowHome, "--dd-flow-home"), resourceHome: noFlow ? null : (process.env.DD_FLOW_RESOURCE_HOME ? absolute(process.env.DD_FLOW_RESOURCE_HOME, "DD_FLOW_RESOURCE_HOME") : null), env: noFlow ? {} : { DD_FLOW_HOME: absolute(options.ddFlowHome, "--dd-flow-home") }, sessionArchive: options.sessionArchive ? absolute(options.sessionArchive, "--session-archive") : null, dataHome: paths.data, configHome: paths.config, cacheHome: paths.cache, adapterDir: path.join(paths.config, "opencode"), password: randomBytes(24).toString("hex"), daemonId, noFlow };
|
|
55
74
|
await prepareRuntimeOwner(config, paths.dir);
|
|
75
|
+
assertDaemonRestartable(previous);
|
|
56
76
|
if (!config.provider || !config.model) throw new DaemonError("profile_required", "--provider and --model are required");
|
|
57
77
|
try { const status = await callDaemon(paths.dir, "daemon.status", {}, 1000); if (!sameConfig(status, config)) throw new DaemonError("daemon_config_mismatch", "a daemon is already running with different configuration"); return { ...status, already_running: true }; } catch (error) { if (!["daemon_not_running","daemon_timeout"].includes(error.code)) throw error; }
|
|
78
|
+
assertDaemonRestartable(await readState(paths.dir));
|
|
58
79
|
if (previous?.shutdown_state === "clean") await authorizeRetainedDaemonResume(paths.dir, previous, options.sessionId, config); if (previous?.shutdown_state === "running" && previous.active_tree) throw new DaemonError("invalid_harness_crash", "previous daemon died with an active or unproven Session tree");
|
|
59
80
|
await removeSocket(paths); const authSource = options.authPath ? absolute(options.authPath, "--auth-path") : path.join(os.homedir(), ".local", "share", "opencode", "auth.json"); await prepare(paths, config, authSource); config.version = (await run(config.bin, ["--version"], config)).trim(); if (config.version !== OPENCODE_BASELINE.version) throw new DaemonError("unsupported_version", `unsupported OpenCode version ${config.version}; expected ${OPENCODE_BASELINE.version}`); const imported = await importArchive(paths, config, config.sessionArchive);
|
|
60
81
|
const resourceProcess = await registerDaemonProcess(config, { kind: "opencode-daemon", owner: `opencode:${path.basename(paths.dir)}`, operation: `opencode-daemon:${path.basename(paths.dir)}`, stdout: paths.log, stderr: paths.log });
|
|
@@ -63,11 +84,11 @@ export async function startDaemon(options) {
|
|
|
63
84
|
try { await confirmDaemonProcess(config, resourceProcess, child); const deadline = Date.now() + 20_000; while (Date.now() < deadline) { await new Promise(resolve => setTimeout(resolve, 100)); try { return await callDaemon(paths.dir, "daemon.status", {}, 1000); } catch (error) { if (!["daemon_not_running","daemon_timeout"].includes(error.code)) throw error; } } throw new DaemonError("daemon_start_failed", "daemon did not become ready", true); }
|
|
64
85
|
catch (error) { await cleanupFailedStart(config, resourceProcess, child, error); throw error; }
|
|
65
86
|
}
|
|
66
|
-
export const stopDaemon = async options => { const timeoutMs = options.timeoutMs ?? 30_000; return await confirmDaemonStopped(() => callDaemon(options.stateDir, "daemon.stop", { cancelTree: options.cancelTree === true }, timeoutMs), locations(options.stateDir).socket, timeoutMs); };
|
|
87
|
+
export const stopDaemon = async options => { const timeoutMs = options.timeoutMs ?? 30_000; return await confirmDaemonStopped(() => callDaemon(options.stateDir, "daemon.stop", { cancelTree: options.cancelTree === true }, timeoutMs), locations(options.stateDir).socket, timeoutMs, { readState: () => readState(options.stateDir) }); };
|
|
67
88
|
|
|
68
89
|
export class Runtime {
|
|
69
90
|
constructor(paths, state, client, child, providerProcess) { this.paths = paths; this.state = state; this.client = client; this.child = child; this.providerProcess = providerProcess; this.sessions = new Map((state.sessions ?? []).map(x => [x.provider_session_id, x])); this.active = null; this.persisting = Promise.resolve(); }
|
|
70
|
-
persist(patch = {}) { Object.assign(this.state, patch, { pid: process.pid, sessions: [...this.sessions.values()] }); const snapshot = structuredClone(this.state); this.persisting = this.persisting.then(() => writeJson(this.paths.state, snapshot)); return this.persisting; }
|
|
91
|
+
persist(patch = {}) { Object.assign(this.state, patch, { pid: process.pid, sessions: [...this.sessions.values()] }); const snapshot = structuredClone(this.state); this.persisting = this.persisting.catch(() => {}).then(() => writeJson(this.paths.state, snapshot)); return this.persisting; }
|
|
71
92
|
async journal(operation, result) { const line = `${JSON.stringify({ schema_id: "dd-opencode/daemon-event@1", event_id: randomUUID(), observed_at: new Date().toISOString(), operation, active_tree: this.state.active_tree, session_id: result?.provider_session_id ?? null })}\n`; for (const journal of new Set([this.paths.journal, this.state.config.journal].filter(Boolean))) { await mkdir(path.dirname(journal), { recursive: true, mode: 0o700 }); await appendFile(journal, line, { mode: 0o600 }); } }
|
|
72
93
|
async childTree(id, seen = new Set()) { if (seen.has(id)) return []; seen.add(id); const direct = await this.client.children(id) ?? []; const nested = []; for (const child of direct) nested.push(child, ...await this.childTree(child.id, seen)); return nested; }
|
|
73
94
|
async describe(id) { const [info, messages, children, statuses] = await Promise.all([this.client.inspect(id), this.client.messages(id), this.childTree(id), this.client.statuses()]); if (path.resolve(info.directory) !== path.resolve(this.state.config.cwd)) throw new DaemonError("foreign_session", "OpenCode Session directory is outside the controlled workspace", false, { session_directory: info.directory, workspace: this.state.config.cwd }); const status = statuses?.[id]?.type ?? "idle", parent = info.parentID ?? null, existing = this.sessions.get(id); const record = { provider_session_id: id, adapter_session_id: id, parent_provider_session_id: parent, root_provider_session_id: parent ? (this.sessions.get(parent)?.root_provider_session_id ?? parent) : (existing?.root_provider_session_id ?? id), cwd: info.directory }; this.sessions.set(id, record); for (const child of children) if (!this.sessions.has(child.id)) this.sessions.set(child.id, { provider_session_id: child.id, parent_provider_session_id: child.parentID ?? id, root_provider_session_id: record.root_provider_session_id, cwd: child.directory }); const unsettledChildren = children.filter(child => (statuses?.[child.id]?.type ?? "idle") !== "idle").map(child => child.id); const lastAssistant = messages.findLast(message => message.info?.role === "assistant"); const assistantText = (lastAssistant?.parts ?? []).filter(part => part.type === "text").map(part => part.text ?? "").join(""); await this.observeModels(id, messages, parent); this.journaledTools ??= new Map(); journalToolObservations(this.state.config.journal, messages.flatMap(message => (message.parts ?? []).map(part => openCodeToolObservation(part, id, message.info?.id))), this.journaledTools); return { ...record, observed_profile: { provider: lastAssistant?.info?.providerID ?? null, model: lastAssistant?.info?.modelID ?? null, reasoning: lastAssistant?.info?.variant ?? null, mode: info.agent ?? null }, assistant_text: assistantText, info, status, settled: status === "idle" && unsettledChildren.length === 0, unsettled_children: unsettledChildren, tree_observations: children.map(child => ({ provider_session_id: child.id, status: statuses?.[child.id]?.type ?? "idle", settled: (statuses?.[child.id]?.type ?? "idle") === "idle" })), children, usage: usageSnapshot(messages, id), history_sha256: historyDigest(messages), message_count: messages.length }; }
|
|
@@ -86,10 +107,12 @@ export class Runtime {
|
|
|
86
107
|
async requireSettled(id) { const value = await this.describe(id); if (!value.settled) throw new DaemonError("tree_not_settled", "OpenCode Session is not idle", false, { session_id: id, status: value.status }); return value; }
|
|
87
108
|
async reconcileUsage(observed) { const errors = []; try { await forwardUsage(this.state.config, observed.provider_session_id, observed.usage); } catch (error) { errors.push({ session_id: observed.provider_session_id, phase: "usage", ...errorPayload(error) }); } for (const child of observed.children.filter(item => (item.parentID ?? observed.provider_session_id) === observed.provider_session_id)) { try { errors.push(...await this.reconcileUsage(await this.describe(child.id))); } catch (error) { errors.push({ session_id: child.id, phase: "usage_child", ...errorPayload(error) }); } } return errors; }
|
|
88
109
|
async productive(name, task) {
|
|
110
|
+
assertDaemonDispatch(this.paths?.dir);
|
|
89
111
|
if (this.active) throw new DaemonError("operation_busy", `${this.active} is already running`, true);
|
|
90
112
|
this.active = name; let result, completed = false;
|
|
91
113
|
try {
|
|
92
114
|
await this.persist({ active_tree: true, active_operation: name });
|
|
115
|
+
assertDaemonDispatch(this.paths?.dir);
|
|
93
116
|
result = await task(); completed = true;
|
|
94
117
|
await this.persist({ active_tree: !result?.settled, active_operation: null });
|
|
95
118
|
await this.journal(name, result); return result;
|
|
@@ -118,6 +141,7 @@ export class Runtime {
|
|
|
118
141
|
}
|
|
119
142
|
async dispatch(operation, params) {
|
|
120
143
|
const assertDispatch = captureDispatchGuard(this);
|
|
144
|
+
if (["session.create", "session.prompt", "session.start", "session.fork", "session.resume", "session.archive"].includes(operation)) assertDaemonDispatch(this.paths.dir);
|
|
121
145
|
if (operation === "daemon.status") { const { password, ...config } = this.state.config; return { daemon_id: this.state.daemon_id, pid: process.pid, socket: this.paths.socket, versions: this.state.versions, auth_status: this.state.auth_status, shutdown_state: this.state.shutdown_state, active_tree: this.state.active_tree, active_operation: this.active, sessions: [...this.sessions.values()], config }; }
|
|
122
146
|
if (operation === "daemon.doctor") { const doc = await this.client.request("GET", "/doc", undefined, { directory: null, timeoutMs: 5_000 }); const apiFingerprint = createHash("sha256").update(typeof doc === "string" ? doc : JSON.stringify(doc)).digest("hex"); if (apiFingerprint !== OPENCODE_BASELINE.api_fingerprint) throw new DaemonError("api_fingerprint_mismatch", "OpenCode API differs from the qualified baseline", false, { expected: OPENCODE_BASELINE.api_fingerprint, actual: apiFingerprint }); return { compatible: true, versions: this.state.versions, api_fingerprint: apiFingerprint, plugin_sha256: this.state.config.pluginSha256, auth_status: this.state.auth_status }; }
|
|
123
147
|
if (operation === "session.create") return this.productive(operation, async () => {
|
|
@@ -177,6 +201,7 @@ export class Runtime {
|
|
|
177
201
|
}
|
|
178
202
|
if (operation === "session.archive") { const observed = await this.requireSettled(params.sessionId); const payload = await run(this.state.config.bin, ["export", params.sessionId], this.state.config); const manifest = { schema_id: ARCHIVE_SCHEMA, provider_session_id: params.sessionId, source_cwd: observed.cwd, opencode_version: this.state.versions.opencode, history_sha256: observed.history_sha256, exported_at: new Date().toISOString(), payload: Buffer.from(payload).toString("base64") }; const file = absolute(params.archiveFile, "--archive-file"); await writeJson(file, manifest); return { archive_file: file, ...manifest, payload: undefined, sha256: createHash("sha256").update(await readFile(file)).digest("hex") }; }
|
|
179
203
|
if (operation === "daemon.stop") {
|
|
204
|
+
return await completeDaemonShutdown(this.paths.dir, this.state, patch => this.persist(patch), async () => {
|
|
180
205
|
if (params.cancelTree) cancelPendingDispatch(this);
|
|
181
206
|
await waitForSettlement({
|
|
182
207
|
knownSessions: () => [...this.sessions.keys()],
|
|
@@ -187,18 +212,15 @@ export class Runtime {
|
|
|
187
212
|
},
|
|
188
213
|
...(params.cancelTree ? { cancel: id => this.cancelTree(id) } : {})
|
|
189
214
|
});
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
215
|
+
return { clean: true };
|
|
216
|
+
}, [
|
|
217
|
+
["provider", () => stopProcessGroup(this.child)],
|
|
218
|
+
["provider_resource", () => finishDaemonProcess(this.state.config, this.providerProcess)],
|
|
219
|
+
["daemon_resource", () => finishDaemonProcess(this.state.config, this.state.resource_process)]
|
|
220
|
+
]);
|
|
194
221
|
}
|
|
195
222
|
throw new DaemonError("unknown_operation", `unknown daemon operation: ${operation}`);
|
|
196
223
|
}
|
|
197
|
-
async shutdown() {
|
|
198
|
-
try { await stopProcessGroup(this.child); await finishDaemonProcess(this.state.config, this.providerProcess, "stopped", "daemon_stopped"); }
|
|
199
|
-
catch (error) { await this.persist({ shutdown_state: "cleanup_failed", cleanup_error: errorPayload(error) }); throw error; }
|
|
200
|
-
await finishDaemonProcess(this.state.config, this.state.resource_process); await this.persist({ shutdown_state: "clean", stopped_at: new Date().toISOString() }); await new Promise(resolve => this.server.close(resolve)); await removeSocket(this.paths);
|
|
201
|
-
}
|
|
202
224
|
}
|
|
203
225
|
export async function serveDaemon(stateDir) {
|
|
204
226
|
const paths = locations(stateDir), state = await readState(paths.dir); if (state?.schema_id !== STATE_SCHEMA) throw new DaemonError("daemon_state_missing", "daemon state is missing");
|
|
@@ -211,7 +233,28 @@ export async function serveDaemon(stateDir) {
|
|
|
211
233
|
scheduleDaemonProcessHeartbeat(state.config, state.resource_process);
|
|
212
234
|
scheduleDaemonProcessHeartbeat(state.config, providerProcess);
|
|
213
235
|
state.provider_process = providerProcess; state.provider_pid = child.pid; await writeJson(paths.state, state);
|
|
214
|
-
const client = new OpenCodeClient({ baseUrl: `http://127.0.0.1:${port}`, password: state.config.password, directory: state.config.cwd }); const deadline = Date.now() + 15_000; while (Date.now() < deadline) { try { await client.health(); break; } catch { await new Promise(resolve => setTimeout(resolve, 100)); } } await client.health(); const runtime = new Runtime(paths, state, client, child, providerProcess); await removeSocket(paths);
|
|
236
|
+
const client = new OpenCodeClient({ baseUrl: `http://127.0.0.1:${port}`, password: state.config.password, directory: state.config.cwd }); const deadline = Date.now() + 15_000; while (Date.now() < deadline) { try { await client.health(); break; } catch { await new Promise(resolve => setTimeout(resolve, 100)); } } await client.health(); const runtime = new Runtime(paths, state, client, child, providerProcess); await removeSocket(paths);
|
|
237
|
+
const server = net.createServer(connection => {
|
|
238
|
+
connection.setEncoding("utf8"); let buffer = "", consumed = false;
|
|
239
|
+
connection.on("data", chunk => {
|
|
240
|
+
if (consumed) return;
|
|
241
|
+
buffer += chunk; const at = buffer.indexOf("\n"); if (at < 0) return; consumed = true;
|
|
242
|
+
void (async () => {
|
|
243
|
+
let request;
|
|
244
|
+
try {
|
|
245
|
+
request = JSON.parse(buffer.slice(0, at));
|
|
246
|
+
const result = await durableDaemonDispatch(paths.dir, request, () => runtime.dispatch(request.operation, request.params ?? {}), () => runtime.dispatch("session.inspect", request.params ?? {}));
|
|
247
|
+
if (result._shutdown) retireDaemonEndpoint(server);
|
|
248
|
+
connection.end(`${JSON.stringify({ schema_id: RESPONSE_SCHEMA, id: request.id, ok: true, result: { ...result, _shutdown: undefined } })}\n`);
|
|
249
|
+
} catch (error) {
|
|
250
|
+
await retireFailedDaemon(server, state, error, patch => runtime.persist(patch));
|
|
251
|
+
connection.end(`${JSON.stringify({ schema_id: RESPONSE_SCHEMA, id: request?.id ?? null, ok: false, error: errorPayload(error) })}\n`);
|
|
252
|
+
}
|
|
253
|
+
})();
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
trackDaemonConnections(server); runtime.server = server;
|
|
257
|
+
await new Promise((resolve, reject) => { server.once("error", reject); server.listen(paths.socket, resolve); }); await chmod(paths.socket, 0o600); await runtime.persist({ shutdown_state: "running", active_tree: false, server_port: port }); return await new Promise(() => {});
|
|
215
258
|
} catch (error) {
|
|
216
259
|
await cleanupFailedStart(state.config, providerProcess, child, error); throw error;
|
|
217
260
|
}
|