@deksden-com/dd-flow-cli 0.9.0-beta.37 → 0.9.0-beta.39
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 +16 -0
- package/dist/build-info.json +5 -5
- package/dist/cli/help.js +42 -1
- package/dist/cli/run-cli.js +145 -17
- package/dist/harness-runtime/bin/dd-agy.mjs +3 -2
- package/dist/harness-runtime/bin/dd-codex.mjs +4 -3
- package/dist/harness-runtime/bin/dd-droid.mjs +3 -2
- package/dist/harness-runtime/bin/dd-grok.mjs +4 -3
- package/dist/harness-runtime/bin/dd-opencode.mjs +4 -3
- package/dist/harness-runtime/bin/dd-zcode.mjs +4 -3
- package/dist/harness-runtime/lib/daemon-operations.mjs +48 -9
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +73 -20
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +57 -16
- package/dist/harness-runtime/lib/dd-codex.mjs +10 -2
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +3 -2
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +12 -6
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +4 -3
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +8 -5
- package/dist/harness-runtime/lib/managed-daemon.mjs +125 -6
- package/dist/harness-runtime/lib/process-json.mjs +1 -1
- package/dist/harness-runtime/lib/session-settlement.mjs +4 -2
- package/dist/schemas/run-control-receipt.schema.json +99 -0
- package/dist/schemas/run-control-request.schema.json +36 -0
- package/dist/services/cleanup.js +7 -4
- package/dist/services/cli-operation-classifier.js +3 -1
- package/dist/services/code-checks.js +33 -7
- package/dist/services/controller-fanout.js +105 -0
- package/dist/services/engines.js +2 -0
- package/dist/services/eval-snapshots.js +347 -36
- package/dist/services/execution-policy.js +122 -6
- package/dist/services/external-work-launch.js +33 -12
- package/dist/services/harness-adapter.js +49 -2
- package/dist/services/managed-daemon-binding.js +38 -0
- package/dist/services/managed-processes.js +54 -7
- package/dist/services/merge-server.js +53 -30
- package/dist/services/native-daemon-history.js +49 -0
- package/dist/services/native-session-control.js +31 -0
- package/dist/services/protocols.js +9 -0
- package/dist/services/recovery-snapshot-database.js +103 -0
- package/dist/services/run-control-receipt.js +56 -0
- package/dist/services/run-control-worker.js +297 -0
- package/dist/services/run-control.js +752 -0
- package/dist/services/run-controller-adapter.js +44 -0
- package/dist/services/run-controller-capture.js +117 -0
- package/dist/services/run-controller-process.js +178 -0
- package/dist/services/run-controller-recovery.js +219 -0
- package/dist/services/run-controller-state.js +31 -0
- package/dist/services/run-controller.js +538 -0
- package/dist/services/run-recovery-runtime.js +69 -0
- package/dist/services/run-recovery.js +33 -9
- package/dist/services/runs.js +65 -13
- package/dist/services/runtime-budget.js +362 -0
- package/dist/services/runtime-scope-capture.js +50 -0
- package/dist/services/runtime-scope-control.js +431 -0
- package/dist/services/runtime-scope-resume.js +543 -0
- package/dist/services/runtime-scope-stop.js +95 -0
- package/dist/services/runtime-scope-worker.js +165 -0
- package/dist/services/runtime-service.js +4 -0
- package/dist/services/vnext-code-review.js +1 -1
- package/dist/services/vnext-code.js +1 -1
- package/dist/services/vnext-execution-profile.js +2 -35
- package/dist/services/vnext-fanout.js +23 -7
- package/dist/services/vnext-plan-review.js +12 -9
- package/dist/services/vnext-plan.js +1 -1
- package/dist/services/vnext-protocolize.js +10 -4
- package/dist/services/vnext-specify.js +12 -5
- package/dist/services/work-registry.js +26 -8
- package/dist/services/workspace-bootstrap.js +10 -1
- package/dist/storage/database.js +160 -13
- package/dist/storage/writer-contract.js +76 -0
- package/dist/storage/writer-migration.js +102 -0
- package/package.json +1 -1
|
@@ -4,11 +4,12 @@ import { randomUUID } from "node:crypto";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { hashJson, writeJsonAtomic } from "./runner-events.mjs";
|
|
6
6
|
import { errorRecord, isObservationLoss, reportedError } from "./operation-errors.mjs";
|
|
7
|
-
import { assertDaemonOwnership } from "./managed-daemon.mjs";
|
|
7
|
+
import { assertDaemonOwnership, budgetObservationScope, releaseBudgetObservation, completeBudgetOperation } from "./managed-daemon.mjs";
|
|
8
8
|
import { readModelObservations, modelAttribution } from "./model-observations.mjs";
|
|
9
9
|
|
|
10
|
-
const productive = new Set(["session.create", "session.prompt", "session.start", "session.fork"]);
|
|
10
|
+
const productive = new Set(["session.create", "session.prompt", "session.start", "session.fork", "session.resume"]);
|
|
11
11
|
const controls = new Map();
|
|
12
|
+
const dispatchGenerations = new Map();
|
|
12
13
|
|
|
13
14
|
async function controlDispatch(root, request, action) {
|
|
14
15
|
const key = request.operation === "daemon.stop" ? `stop:${request.params?.cancelTree === true}` : `cancel:${request.params?.sessionId ?? ""}`;
|
|
@@ -30,6 +31,7 @@ function location(root, id) {
|
|
|
30
31
|
async function read(file) { try { return JSON.parse(await readFile(file, "utf8")); } catch (error) { if (error.code === "ENOENT") return null; throw error; } }
|
|
31
32
|
|
|
32
33
|
export async function inspectDaemonOperation(root, id) {
|
|
34
|
+
if (typeof root !== "string" || !path.isAbsolute(root)) throw Object.assign(new Error("operation observation requires an absolute state directory"), { code: "invalid_path" });
|
|
33
35
|
const directory = location(root, id);
|
|
34
36
|
const requested = await read(path.join(directory, "requested.json"));
|
|
35
37
|
if (!requested) throw Object.assign(new Error("daemon operation is not recorded"), { code: "operation_not_found", details: { operation_id: id } });
|
|
@@ -38,7 +40,7 @@ export async function inspectDaemonOperation(root, id) {
|
|
|
38
40
|
|
|
39
41
|
/** At-most-once dispatch and a durable terminal reply, including when the socket
|
|
40
42
|
* observer disconnects. Recovery reads this receipt; it never resends a prompt. */
|
|
41
|
-
export async function durableDaemonDispatch(root, request, action) {
|
|
43
|
+
export async function durableDaemonDispatch(root, request, action, observe) {
|
|
42
44
|
if (request.operation === "operation.inspect") return await inspectDaemonOperation(root, request.params?.operationId);
|
|
43
45
|
if (request.operation === "daemon.status") {
|
|
44
46
|
const result = await action();
|
|
@@ -46,15 +48,50 @@ export async function durableDaemonDispatch(root, request, action) {
|
|
|
46
48
|
try { return { ...result, model_attribution: modelAttribution(await readModelObservations(result.config.journal)) }; }
|
|
47
49
|
catch (error) { return { ...result, model_observation_error: errorRecord(error) }; }
|
|
48
50
|
}
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
if (request.operation === "session.inspect") {
|
|
52
|
+
const scope = await budgetObservationScope(root, request.params?.sessionId);
|
|
53
|
+
const result = await action();
|
|
54
|
+
await releaseBudgetObservation(scope, result);
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
const control = ["daemon.stop", "session.cancel"].includes(request.operation);
|
|
58
|
+
if (!productive.has(request.operation) && !control) return await action();
|
|
59
|
+
const generation = dispatchGenerations.get(root) ?? 0;
|
|
60
|
+
if (control) dispatchGenerations.set(root, generation + 1);
|
|
61
|
+
const assertCurrent = () => { if (generation !== (dispatchGenerations.get(root) ?? 0)) throw Object.assign(new Error("control cancelled a queued productive dispatch"), { code: "operation_cancelled" }); };
|
|
62
|
+
const result = await journalDispatch(root, request, async () => {
|
|
63
|
+
// Persist before admission so a racing fence can observe the writer.
|
|
64
|
+
if (!control) await assertDaemonOwnership(root, { "operation-id": request.id, operation: request.operation, "native-session-id": request.params?.sessionId, "prompt-sha256": typeof request.params?.prompt === "string" ? createHash("sha256").update(request.params.prompt).digest("hex") : undefined }, assertCurrent);
|
|
65
|
+
if (!control) assertCurrent();
|
|
66
|
+
return control ? await controlDispatch(root, request, action) : await action();
|
|
67
|
+
});
|
|
68
|
+
if (!control) await completeBudgetOperation(root, request, result, observe);
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Startup has no registered daemon yet. Its existing native implementation
|
|
73
|
+
* owns registration/admission; only the CLI entrypoint uses this journal path. */
|
|
74
|
+
export async function durableDaemonStart(options, action) {
|
|
75
|
+
const root = options.stateDir;
|
|
76
|
+
if (typeof root !== "string" || !path.isAbsolute(root)) throw Object.assign(new Error("daemon startup requires an absolute state directory"), { code: "invalid_path" });
|
|
77
|
+
// Keep only configuration locations, never inherited credentials. The owned
|
|
78
|
+
// scope can reuse this exact adapter launch after retiring the old daemon.
|
|
79
|
+
const launch = { schema_id: "dd-flow/native-launch@1", executable: options.entryPath ?? null, args: process.argv.slice(2), cwd: process.cwd(), runtime_owner: process.env.DD_FLOW_RUNTIME_OWNER ?? null,
|
|
80
|
+
env: Object.fromEntries(["CODEX_HOME", "DD_FLOW_CONFIG_HOME", "DD_FLOW_HOME", "DD_FLOW_BIN", "DD_FLOW_RESOURCE_HOME"].map(key => [key, process.env[key] ?? null])) };
|
|
81
|
+
const request = { id: process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation: "daemon.start", params: { ...options, runtimeOwner: process.env.DD_FLOW_RUNTIME_OWNER ?? null, resourceHome: process.env.DD_FLOW_RESOURCE_HOME ?? null, launch } };
|
|
82
|
+
return await journalDispatch(root, request, () => action(options));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function journalDispatch(root, request, action) {
|
|
52
86
|
const directory = location(root, request.id), fingerprint = hashJson({ operation: request.operation, params: request.params ?? {} });
|
|
53
87
|
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
54
88
|
// Publish a complete request atomically and exclusively. Writing directly with
|
|
55
89
|
// wx exposes a partially written JSON document to a concurrent observer.
|
|
56
90
|
const temporary = path.join(directory, `requested-${randomUUID()}.json`);
|
|
57
|
-
|
|
91
|
+
const daemon = await read(path.join(root, "daemon.json"));
|
|
92
|
+
const startup = request.operation === "daemon.start";
|
|
93
|
+
const daemonId = daemon?.daemon_id ?? daemon?.config?.daemonId ?? null;
|
|
94
|
+
await writeJsonAtomic(temporary, { schema_id: "dd-flow/daemon-operation@1", operation_id: request.id, operation: request.operation, session_id: request.params?.sessionId ?? null, daemon_id: startup ? null : daemonId, ...(startup ? { predecessor_daemon_id: daemonId, launch: request.params?.launch } : {}), params_sha256: fingerprint, requested_at: new Date().toISOString() });
|
|
58
95
|
try {
|
|
59
96
|
await link(temporary, path.join(directory, "requested.json"));
|
|
60
97
|
} catch (error) {
|
|
@@ -66,13 +103,15 @@ export async function durableDaemonDispatch(root, request, action) {
|
|
|
66
103
|
throw Object.assign(new Error("provider operation has no confirmed terminal result; inspect the existing operation instead of resending it"), { code: "operation_observation_lost", details: { operation_id: request.id, receipt_directory: directory } });
|
|
67
104
|
} finally { await unlink(temporary); }
|
|
68
105
|
let result;
|
|
69
|
-
try {
|
|
106
|
+
try {
|
|
107
|
+
result = await action();
|
|
108
|
+
}
|
|
70
109
|
catch (error) {
|
|
71
110
|
const unknown = isObservationLoss(error);
|
|
72
111
|
await writeJsonAtomic(path.join(directory, unknown ? "observation-lost.json" : "result.json"), { state: unknown ? "awaiting_provider" : "failed", observed_at: new Date().toISOString(), error: errorRecord(error) });
|
|
73
112
|
throw error;
|
|
74
113
|
}
|
|
75
|
-
try { await writeJsonAtomic(path.join(directory, "result.json"), { state: "completed", finished_at: new Date().toISOString(), result }); }
|
|
114
|
+
try { await writeJsonAtomic(path.join(directory, "result.json"), { state: "completed", ...(startup ? { started_daemon_id: result?.daemon_id ?? null } : {}), finished_at: new Date().toISOString(), result }); }
|
|
76
115
|
catch (cause) { throw Object.assign(new Error("provider returned but the durable reply could not be saved; do not resend the operation"), { code: "operation_observation_lost", cause, details: { operation_id: request.id, receipt_directory: directory } }); }
|
|
77
116
|
return result;
|
|
78
117
|
}
|
|
@@ -8,7 +8,7 @@ import { durableDaemonDispatch } from "./daemon-operations.mjs";
|
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import { observeModel } from "./model-observations.mjs";
|
|
10
10
|
import { AgyError, assertProfile, executable, observedProfile, runAgy, usageSnapshot } from "./dd-agy.mjs";
|
|
11
|
-
import { cleanupFailedStart, runtimeProcess, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, heartbeatDaemonProcess, registerDaemonProcess, stopProcessGroup } from "./managed-daemon.mjs";
|
|
11
|
+
import { prepareRuntimeOwner, cleanupFailedStart, runtimeProcess, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, heartbeatDaemonProcess, registerDaemonProcess, stopProcessGroup } from "./managed-daemon.mjs";
|
|
12
12
|
|
|
13
13
|
const REQUEST_SCHEMA = "dd-agy/daemon-request@1", RESPONSE_SCHEMA = "dd-agy/daemon-response@1", STATE_SCHEMA = "dd-agy/daemon-state@1";
|
|
14
14
|
class DaemonError extends AgyError {}
|
|
@@ -47,13 +47,15 @@ export async function prepare(paths, config) {
|
|
|
47
47
|
}
|
|
48
48
|
const command = `${shell(process.execPath)} ${shell(config.entryPath)} hook handle --event __EVENT__ --state-dir ${shell(paths.dir)} --project-root ${shell(config.projectRoot)} --dd-flow-bin ${shell(config.ddFlowBin)} --dd-flow-home ${shell(config.ddFlowHome)} --json`;
|
|
49
49
|
const hooks = { "dd-flow": {
|
|
50
|
-
|
|
51
|
-
PostToolUse
|
|
50
|
+
// Only shell lifecycle commands require trusted pre-execution identity.
|
|
51
|
+
// PostToolUse only recorded observations; native updates and Stop settle work.
|
|
52
|
+
PreToolUse: [{ matcher: "^run_command$", hooks: [{ type: "command", command: command.replace("__EVENT__", "PreToolUse"), timeout: 30 }] }],
|
|
52
53
|
Stop: [{ type: "command", command: command.replace("__EVENT__", "Stop"), timeout: 30 }]
|
|
53
54
|
} };
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
55
|
+
// Register in workspace discovery before creating any Session: native root
|
|
56
|
+
// and children load these named hook definitions without a global duplicate.
|
|
57
|
+
// Replace our prior handlers, including the obsolete wildcard post hook,
|
|
58
|
+
// without dropping unrelated commands in a shared matcher group.
|
|
57
59
|
const workspaceConfig = path.join(config.projectRoot, ".agents");
|
|
58
60
|
await mkdir(workspaceConfig, { recursive: true, mode: 0o700 });
|
|
59
61
|
const workspaceHooks = path.join(workspaceConfig, "hooks.json");
|
|
@@ -65,14 +67,48 @@ export async function prepare(paths, config) {
|
|
|
65
67
|
} catch (error) {
|
|
66
68
|
if (error.code !== "ENOENT") throw new DaemonError("workspace_hooks_invalid", `cannot install Antigravity lifecycle hooks: ${error.message}`);
|
|
67
69
|
}
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
+
const isManagedHandler = handler => typeof handler?.command === "string"
|
|
71
|
+
&& handler.command.includes(config.entryPath) && handler.command.includes("hook handle");
|
|
72
|
+
const events = ["PreToolUse", "PostToolUse", "PreInvocation", "PostInvocation", "Stop"];
|
|
73
|
+
const retain = handlers => handlers.flatMap(item => {
|
|
74
|
+
if (!Array.isArray(item?.hooks)) return isManagedHandler(item) ? [] : [item];
|
|
75
|
+
const remaining = item.hooks.filter(handler => !isManagedHandler(handler));
|
|
76
|
+
return remaining.length ? [{ ...item, hooks: remaining }] : [];
|
|
77
|
+
});
|
|
78
|
+
const workspace = Object.create(null), legacy = Object.create(null);
|
|
79
|
+
for (const [name, definition] of Object.entries(existing)) {
|
|
80
|
+
// Migrate the old adapter's event-keyed workspace format.
|
|
81
|
+
if (events.includes(name) && Array.isArray(definition)) {
|
|
82
|
+
const remaining = retain(definition);
|
|
83
|
+
if (remaining.length) legacy[name] = remaining;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (!definition || Array.isArray(definition) || typeof definition !== "object") {
|
|
87
|
+
throw new DaemonError("workspace_hooks_invalid", `invalid named hook definition: ${name}`);
|
|
88
|
+
}
|
|
89
|
+
const cleaned = { ...definition };
|
|
90
|
+
for (const event of events) if (Array.isArray(cleaned[event])) {
|
|
91
|
+
const remaining = retain(cleaned[event]);
|
|
92
|
+
if (remaining.length || cleaned[event].length === 0) cleaned[event] = remaining;
|
|
93
|
+
else delete cleaned[event];
|
|
94
|
+
}
|
|
95
|
+
workspace[name] = cleaned;
|
|
96
|
+
}
|
|
97
|
+
if (Object.keys(legacy).length) {
|
|
98
|
+
let name = "dd-flow-legacy";
|
|
99
|
+
while (Object.hasOwn(workspace, name)) name += "-retained";
|
|
100
|
+
workspace[name] = legacy;
|
|
101
|
+
}
|
|
102
|
+
const managed = workspace["dd-flow"] ?? {};
|
|
103
|
+
if (managed.enabled === false) throw new DaemonError("workspace_hooks_invalid", "dd-flow lifecycle hooks are disabled");
|
|
70
104
|
for (const [event, handlers] of Object.entries(hooks["dd-flow"])) {
|
|
71
|
-
|
|
72
|
-
workspace[event] = [...retained, ...handlers];
|
|
105
|
+
managed[event] = [...(managed[event] ?? []), ...handlers];
|
|
73
106
|
}
|
|
107
|
+
workspace["dd-flow"] = managed;
|
|
74
108
|
await writeJson(workspaceHooks, workspace);
|
|
75
|
-
|
|
109
|
+
// Workspace discovery covers root and children. Installing the same handler
|
|
110
|
+
// globally as well invokes it twice, even under the same hook name.
|
|
111
|
+
await writeJson(path.join(paths.config, "hooks.json"), {});
|
|
76
112
|
config.hooksSha256 = createHash("sha256").update(JSON.stringify(hooks)).digest("hex");
|
|
77
113
|
config.workspaceHooksPath = workspaceHooks;
|
|
78
114
|
}
|
|
@@ -128,6 +164,7 @@ export function retainedAgyState(previous, sessionId) {
|
|
|
128
164
|
export async function startDaemon(options) {
|
|
129
165
|
const paths = locations(options.stateDir); await mkdir(paths.dir, { recursive: true, mode: 0o700 }); await chmod(paths.dir, 0o700);
|
|
130
166
|
const previous = await readState(paths.dir), noFlow = options.noFlow === true, cwd = await realpath(absolute(options.cwd, "--cwd")), config = { cwd, journal: options.journal ? absolute(options.journal, "--journal") : paths.journal, bin: options.bin ?? process.env.DD_AGY_BIN ?? "agy", provider: String(options.provider ?? "google"), model: String(options.model ?? "gemini-3.1-pro-high"), reasoning: String(options.reasoning ?? "high"), mode: String(options.mode ?? "accept-edits"), 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") }, entryPath: absolute(options.entryPath, "dd-agy entry"), daemonId: randomUUID(), resumeSessionId: options.sessionId ?? null, geminiDir: paths.gemini, appDataDir: "runtime", temporary: paths.temporary, noFlow };
|
|
167
|
+
await prepareRuntimeOwner(config, paths.dir);
|
|
131
168
|
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; }
|
|
132
169
|
if (previous?.shutdown_state === "clean") await authorizeRetainedDaemonResume(paths.dir, previous, options.sessionId, config);
|
|
133
170
|
if (previous?.shutdown_state === "running" && previous.active_tree) throw new DaemonError("invalid_harness_crash", "previous Antigravity daemon died with an active or unproven tree");
|
|
@@ -212,12 +249,16 @@ export class Runtime {
|
|
|
212
249
|
if (this.active) { this.active.reject(error); this.active = null; }
|
|
213
250
|
const patch = beforeInit ? { shutdown_state: "failed", startup_error: errorPayload(error), provider_exit: { code, signal }, active_tree: false } : { provider_exit: { code, signal }, active_tree: interrupted };
|
|
214
251
|
await providerRegistered;
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
252
|
+
this.retryProviderFinalization = async (graceMs = 1_000) => {
|
|
253
|
+
try {
|
|
254
|
+
await stopProcessGroup(providerChild, graceMs);
|
|
255
|
+
await this.persist(patch);
|
|
256
|
+
await finishDaemonProcess(this.config, providerProcess, beforeInit || interrupted ? "failed" : "stopped", beforeInit || interrupted ? "provider_interrupted" : "provider_stopped");
|
|
257
|
+
this.retryProviderFinalization = null;
|
|
258
|
+
} catch (cleanup) { await this.persist({ shutdown_state: "cleanup_failed", active_tree: true, cleanup_error: errorPayload(cleanup) }); throw cleanup; }
|
|
259
|
+
finally { if (beforeInit) this.server?.close(); }
|
|
260
|
+
};
|
|
261
|
+
await this.retryProviderFinalization();
|
|
221
262
|
}).then(resolve, reject);
|
|
222
263
|
});
|
|
223
264
|
});
|
|
@@ -328,13 +369,25 @@ export class Runtime {
|
|
|
328
369
|
await observeModel({ journal: this.config.journal, harness: "antigravity-cli", sessionId: conversationId, parentSessionId: parent, requested: this.config, observed, source: "native.hook", evidence: payload.model ? "configured" : "unavailable", reason: payload.model ? null : "hook_omits_current_model" });
|
|
329
370
|
const eventId = createHash("sha256").update(JSON.stringify({ daemon: this.config.daemonId, event, conversationId, turn_generation: this.turnGeneration, fully_idle: payload.fullyIdle ?? null, termination_reason: payload.terminationReason ?? null, execution: payload.executionNum ?? null, step: payload.stepIdx ?? null, tool: payload.toolCall?.name ?? null, args: payload.toolCall?.args ?? null })).digest("hex"); await this.persist(completed ? { active_tree: false } : {}); await this.journal("hook", { stale, hook_event: event, event_id: eventId, conversation_id: conversationId, turn_generation: this.turnGeneration, execution_num: execution, step_index: step, termination_reason: payload.terminationReason ?? null, fully_idle: payload.fullyIdle ?? null }); completed?.resolve(settled); return { event_id: eventId, daemon_id: this.config.daemonId, parent_conversation_id: parent, profile: observed, requested_profile: { provider: this.config.provider, model: this.config.model, reasoning: this.config.reasoning, mode: this.config.mode }, project_root: this.config.projectRoot, dd_flow_bin: this.config.ddFlowBin, dd_flow_home: this.config.ddFlowHome };
|
|
330
371
|
}
|
|
331
|
-
async
|
|
372
|
+
async finalizeProviderForControl() {
|
|
373
|
+
const pending = this.providerFinalization;
|
|
374
|
+
try { await pending; }
|
|
375
|
+
catch (error) {
|
|
376
|
+
// A leaderless group may have exited naturally since the last check.
|
|
377
|
+
// Only control may recheck it; startup never retries failed cleanup.
|
|
378
|
+
if (this.providerFinalization !== pending) return await this.providerFinalization;
|
|
379
|
+
if (error.code !== "process_group_ownership_unknown" || !this.retryProviderFinalization) throw error;
|
|
380
|
+
this.providerFinalization = this.retryProviderFinalization(5_000);
|
|
381
|
+
await this.providerFinalization;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
async cancel() { cancelPendingDispatch(this); if (!this.child) return { ...this.receipt(), settled: Boolean(this.exited) }; if (!this.exited) { try { process.kill(-this.child.pid, "SIGINT"); } catch (error) { if (error.code !== "ESRCH") throw error; } const deadline = Date.now() + 5000; while (!this.exited && Date.now() < deadline) await new Promise(resolve => setTimeout(resolve, 100)); if (!this.exited) await stopProcessGroup(this.child, 5_000); } await this.finalizeProviderForControl(); await this.persist({ active_tree: false }); return { ...this.receipt(), cancelled: true, settled: Boolean(this.exited) };
|
|
332
385
|
}
|
|
333
386
|
async close(cancelTree) { if (cancelTree) cancelPendingDispatch(this); if (!cancelTree && (this.active || this.state.active_tree || this.state.unclaimed_activity || (this.lastResult && !this.receipt().settled))) throw new DaemonError("tree_not_settled", "Antigravity root or child tree is still running"); if (cancelTree) await this.cancel(); else if (this.child && !this.exited) this.child.stdin.end(); const deadline = Date.now() + 5000; while (!this.exited && Date.now() < deadline) await new Promise(resolve => setTimeout(resolve, 100)); if (!this.exited) await this.cancel(); await this.providerFinalization; }
|
|
334
387
|
async abortProvider(reason) {
|
|
335
388
|
const child = this.child, providerProcess = this.providerProcess;
|
|
336
389
|
await stopProcessGroup(child);
|
|
337
|
-
await this.
|
|
390
|
+
await this.finalizeProviderForControl();
|
|
338
391
|
await finishDaemonProcess(this.config, providerProcess, "failed", reason);
|
|
339
392
|
}
|
|
340
393
|
async finishResource(state = "stopped", reason = "daemon_stopped") { await finishDaemonProcess(this.config, this.state.resource_process, state, reason); }
|
|
@@ -342,7 +395,7 @@ export class Runtime {
|
|
|
342
395
|
|
|
343
396
|
export async function serveDaemon(stateDir) {
|
|
344
397
|
const paths = locations(stateDir), state = await readState(paths.dir); if (!state?.config) throw new DaemonError("daemon_state_missing", "Antigravity daemon state is missing");
|
|
345
|
-
const runtime = new Runtime(paths, state); await removeSocket(paths); const server = net.createServer(socket => { let buffer = ""; socket.setEncoding("utf8"); socket.on("data", chunk => { buffer += chunk; const index = buffer.indexOf("\n"); if (index < 0) return; let request; try { request = JSON.parse(buffer.slice(0, index)); } catch (error) { socket.end(`${JSON.stringify({ schema_id: RESPONSE_SCHEMA, ok: false, error: errorPayload(error) })}\n`); return; } void durableDaemonDispatch(paths.dir, request, () => dispatch(runtime, request)).then(result => socket.end(`${JSON.stringify({ schema_id: RESPONSE_SCHEMA, id: request.id, ok: true, result })}\n`), error => socket.end(`${JSON.stringify({ schema_id: RESPONSE_SCHEMA, id: request.id, ok: false, error: errorPayload(error) })}\n`)); }); }); runtime.server = server;
|
|
398
|
+
const runtime = new Runtime(paths, state); await removeSocket(paths); const server = net.createServer(socket => { let buffer = ""; socket.setEncoding("utf8"); socket.on("data", chunk => { buffer += chunk; const index = buffer.indexOf("\n"); if (index < 0) return; let request; try { request = JSON.parse(buffer.slice(0, index)); } catch (error) { socket.end(`${JSON.stringify({ schema_id: RESPONSE_SCHEMA, ok: false, error: errorPayload(error) })}\n`); return; } void durableDaemonDispatch(paths.dir, request, () => dispatch(runtime, request), () => dispatch(runtime, { operation: "session.inspect", params: request.params ?? {} })).then(result => socket.end(`${JSON.stringify({ schema_id: RESPONSE_SCHEMA, id: request.id, ok: true, result })}\n`), error => socket.end(`${JSON.stringify({ schema_id: RESPONSE_SCHEMA, id: request.id, ok: false, error: errorPayload(error) })}\n`)); }); }); runtime.server = server;
|
|
346
399
|
await new Promise((resolve, reject) => { server.once("error", reject); server.listen(paths.socket, resolve); }); await chmod(paths.socket, 0o600);
|
|
347
400
|
try { await runtime.start(state.config.resumeSessionId); runtime.requireSessionIdentity(state.config.resumeSessionId); }
|
|
348
401
|
catch (error) {
|
|
@@ -9,7 +9,7 @@ import { spawn } from "node:child_process";
|
|
|
9
9
|
import { waitForSettlement } from "./session-settlement.mjs";
|
|
10
10
|
import { errorRecord } from "./operation-errors.mjs";
|
|
11
11
|
import { CodexBridge, cancelSessionWithBridge, createSessionWithBridge, inspectSessionWithBridge, promptSessionWithBridge, startSessionWithBridge } from "./dd-codex.mjs";
|
|
12
|
-
import { cleanupFailedStart, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, heartbeatDaemonProcess, registerDaemonProcess, stopProcessGroup } from "./managed-daemon.mjs";
|
|
12
|
+
import { prepareRuntimeOwner, cleanupFailedStart, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, heartbeatDaemonProcess, registerDaemonProcess, stopProcessGroup } from "./managed-daemon.mjs";
|
|
13
13
|
|
|
14
14
|
const requestSchema = "dd-codex/daemon-request@1";
|
|
15
15
|
const responseSchema = "dd-codex/daemon-response@1";
|
|
@@ -29,6 +29,45 @@ export function settledCodexSession(status, latestTurn, active) {
|
|
|
29
29
|
return status === "systemError" && !active && terminalTurn(latestTurn);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
async function observeCodexSessions(bridge, options, sessions, active, remember = () => {}) {
|
|
33
|
+
// Expand trusted native topology before profile/history diagnostics: an
|
|
34
|
+
// unavailable root must not hide its already discovered siblings/children.
|
|
35
|
+
for (const id of sessions) for (const child of bridge.directChildren?.(id) ?? []) {
|
|
36
|
+
if (typeof child.provider_session_id !== "string" || !child.provider_session_id.trim()) throw Object.assign(new Error("Codex child has no native identity"), { code: "invalid_session_topology" });
|
|
37
|
+
sessions.add(child.provider_session_id); remember(child.provider_session_id);
|
|
38
|
+
}
|
|
39
|
+
const unsettled = [], observations = [];
|
|
40
|
+
for (const sessionId of sessions) {
|
|
41
|
+
const observed = await inspectSessionWithBridge(bridge, { ...options, sessionId });
|
|
42
|
+
for (const child of observed.descendants ?? []) { sessions.add(child.provider_session_id); remember(child.provider_session_id); }
|
|
43
|
+
const status = observed.status?.type ?? observed.status;
|
|
44
|
+
const stored = ["notLoaded", "systemError"].includes(status) ? await bridge.request("thread/turns/list", { threadId: sessionId, limit: 1, sortDirection: "desc", itemsView: "summary" }, 5_000) : null;
|
|
45
|
+
const settled = settledCodexSession(status, stored?.data?.[0] ?? null, active());
|
|
46
|
+
if (!settled) unsettled.push(sessionId);
|
|
47
|
+
observations.push({ ...observed, settled });
|
|
48
|
+
}
|
|
49
|
+
return { sessions: unsettled.reverse(), active: Boolean(active()), observations };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function inspectCodexTree(bridge, options, { remember = () => {}, active = () => null } = {}) {
|
|
53
|
+
const sessions = new Set([options.sessionId]);
|
|
54
|
+
const observed = await observeCodexSessions(bridge, options, sessions, active, remember);
|
|
55
|
+
return { ...observed.observations[0], settled: !observed.active && !observed.sessions.length, unsettled_sessions: observed.sessions, tree_session_ids: [...sessions], tree_observations: observed.observations.map(item => ({ provider_session_id: item.provider_session_id, status: item.status, settled: item.settled })) };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function cancelCodexTree(bridge, options, { remember = () => {}, active = () => null, timeoutMs = 5_000 } = {}) {
|
|
59
|
+
const sessions = new Set([options.sessionId]);
|
|
60
|
+
const settlement = await waitForSettlement({
|
|
61
|
+
timeoutMs, knownSessions: () => [...sessions],
|
|
62
|
+
cancel: async sessionId => {
|
|
63
|
+
try { await cancelSessionWithBridge(bridge, { ...options, sessionId, turnId: null }); }
|
|
64
|
+
catch (error) { if (error.code !== "turn_not_active") throw error; }
|
|
65
|
+
},
|
|
66
|
+
observe: () => observeCodexSessions(bridge, options, sessions, active, remember)
|
|
67
|
+
});
|
|
68
|
+
return { harness: "codex-desktop", provider_session_id: options.sessionId, adapter_session_id: options.sessionId, cancelled: true, settled: true, tree_session_ids: [...sessions], cleanup_errors: settlement.cleanup_errors, tree_observations: settlement.observations.map(item => ({ provider_session_id: item.provider_session_id, status: item.status, settled: item.settled })) };
|
|
69
|
+
}
|
|
70
|
+
|
|
32
71
|
export async function callDaemon(stateDir, operation, params = {}, timeoutMs = 30_000) {
|
|
33
72
|
const target = locations(stateDir); const request = { schema_id: requestSchema, id: process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation, params };
|
|
34
73
|
return await new Promise((resolve, reject) => {
|
|
@@ -43,6 +82,7 @@ export async function callDaemon(stateDir, operation, params = {}, timeoutMs = 3
|
|
|
43
82
|
|
|
44
83
|
export async function startDaemon(options) {
|
|
45
84
|
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") } : {} };
|
|
85
|
+
await prepareRuntimeOwner(config, paths.dir);
|
|
46
86
|
await mkdir(paths.dir, { recursive: true, mode: 0o700 }); await chmod(paths.dir, 0o700);
|
|
47
87
|
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; }
|
|
48
88
|
const previous = await readState(paths.state); 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" });
|
|
@@ -70,31 +110,32 @@ export async function serveDaemon(stateDir) {
|
|
|
70
110
|
if (active && !params.cancelTree) throw Object.assign(new Error("productive operation is running"), { code: "tree_not_settled" });
|
|
71
111
|
await waitForSettlement({
|
|
72
112
|
knownSessions: () => [...sessions],
|
|
73
|
-
observe:
|
|
74
|
-
const unsettled = [];
|
|
75
|
-
// Set iteration also visits descendants discovered during this pass.
|
|
76
|
-
for (const sessionId of sessions) {
|
|
77
|
-
const observed = await inspectSessionWithBridge(bridge, { ...state.config, sessionId });
|
|
78
|
-
for (const child of observed.descendants ?? []) sessions.add(child.provider_session_id);
|
|
79
|
-
const status = observed.status?.type ?? observed.status;
|
|
80
|
-
const stored = ["notLoaded", "systemError"].includes(status) ? await bridge.request("thread/turns/list", { threadId: sessionId, limit: 1, sortDirection: "desc", itemsView: "summary" }, 5_000) : null;
|
|
81
|
-
if (!settledCodexSession(status, stored?.data?.[0] ?? null, active)) unsettled.push(sessionId);
|
|
82
|
-
}
|
|
83
|
-
return { sessions: unsettled.reverse(), active: Boolean(active), active_operation: active };
|
|
84
|
-
},
|
|
113
|
+
observe: () => observeCodexSessions(bridge, state.config, sessions, () => active),
|
|
85
114
|
...(params.cancelTree ? { cancel: async sessionId => { try { await cancelSessionWithBridge(bridge, { ...state.config, sessionId }); } catch (error) { if (error.code !== "turn_not_active") throw error; } } } : {})
|
|
86
115
|
});
|
|
116
|
+
await save({ active_tree: false });
|
|
87
117
|
return { stopped: true, clean: true, _shutdown: true };
|
|
88
118
|
}
|
|
89
119
|
if (active && !["session.inspect", "session.cancel"].includes(operation)) throw Object.assign(new Error(`${active} is in progress`), { code: "operation_busy", retryable: true });
|
|
90
120
|
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 }); } }
|
|
91
121
|
if (operation === "session.prompt") { sessions.add(params.sessionId); active = operation; try { await save({ active_operation: active }); return await promptSessionWithBridge(bridge, { ...state.config, ...params, assertDispatch }); } finally { active = null; await save({ active_operation: null }); } }
|
|
92
122
|
if (operation === "session.start") { sessions.add(params.sessionId); active = operation; try { await save({ active_operation: active }); return await startSessionWithBridge(bridge, { ...state.config, ...params, assertDispatch }); } finally { active = null; await save({ active_operation: null }); } }
|
|
93
|
-
if (operation === "session.inspect"
|
|
94
|
-
|
|
123
|
+
if (operation === "session.inspect" || operation === "session.resume") {
|
|
124
|
+
const owned = new Set(sessions);
|
|
125
|
+
for (const id of owned) for (const child of bridge.directChildren?.(id) ?? []) owned.add(child.provider_session_id);
|
|
126
|
+
if (!owned.has(params.sessionId)) throw Object.assign(new Error("inspection Session is not owned by this daemon"), { code: "session_identity_mismatch" });
|
|
127
|
+
try { return await inspectCodexTree(bridge, { ...state.config, ...params }, { remember: id => sessions.add(id), active: () => active }); }
|
|
128
|
+
finally { await save(); }
|
|
129
|
+
}
|
|
130
|
+
if (operation === "session.cancel") {
|
|
131
|
+
if (!sessions.has(params.sessionId)) throw Object.assign(new Error("cancellation Session is not owned by this daemon"), { code: "session_identity_mismatch" });
|
|
132
|
+
cancelPendingDispatch(bridge);
|
|
133
|
+
try { return await cancelCodexTree(bridge, { ...state.config, ...params }, { remember: id => sessions.add(id), active: () => active }); }
|
|
134
|
+
finally { await save(); }
|
|
135
|
+
}
|
|
95
136
|
throw Object.assign(new Error(`unknown operation: ${operation}`), { code: "unknown_operation" });
|
|
96
137
|
};
|
|
97
|
-
await removeSocket(paths.socket); server = net.createServer((connection) => { connection.setEncoding("utf8"); let buffer = ""; connection.on("data", (chunk) => { buffer += chunk; const at = buffer.indexOf("\n"); if (at < 0) return; const line = buffer.slice(0, at); buffer = buffer.slice(at + 1); void (async () => { let request; try { request = JSON.parse(line); if (request.schema_id !== requestSchema || !request.id || !request.operation) throw Object.assign(new Error("invalid daemon request"), { code: "daemon_protocol" }); const result = await durableDaemonDispatch(paths.dir, request, () => dispatch(request.operation, request.params ?? {})); connection.end(`${JSON.stringify({ schema_id: responseSchema, id: request.id, ok: true, result: { ...result, _shutdown: undefined } })}\n`); if (result._shutdown) { await bridge.close(); await save({ shutdown_state: "clean", stopped_at: new Date().toISOString() }); await finishDaemonProcess(state.config, state.resource_process); server.close(() => process.exit(0)); await removeSocket(paths.socket); } } catch (error) { if (connection.writableEnded) { await save({ shutdown_state: "cleanup_failed", cleanup_error: errorPayload(error) }); return; } connection.end(`${JSON.stringify({ schema_id: responseSchema, id: request?.id ?? null, ok: false, error: errorPayload(error) })}\n`); } })(); }); });
|
|
138
|
+
await removeSocket(paths.socket); server = net.createServer((connection) => { connection.setEncoding("utf8"); let buffer = ""; connection.on("data", (chunk) => { buffer += chunk; const at = buffer.indexOf("\n"); if (at < 0) return; const line = buffer.slice(0, at); buffer = buffer.slice(at + 1); void (async () => { let request; try { request = JSON.parse(line); if (request.schema_id !== requestSchema || !request.id || !request.operation) throw Object.assign(new Error("invalid daemon request"), { code: "daemon_protocol" }); const result = await durableDaemonDispatch(paths.dir, request, () => dispatch(request.operation, request.params ?? {}), () => dispatch("session.inspect", request.params ?? {})); connection.end(`${JSON.stringify({ schema_id: responseSchema, id: request.id, ok: true, result: { ...result, _shutdown: undefined } })}\n`); if (result._shutdown) { await bridge.close(); await save({ shutdown_state: "clean", stopped_at: new Date().toISOString() }); await finishDaemonProcess(state.config, state.resource_process); server.close(() => process.exit(0)); await removeSocket(paths.socket); } } catch (error) { if (connection.writableEnded) { await save({ shutdown_state: "cleanup_failed", cleanup_error: errorPayload(error) }); return; } connection.end(`${JSON.stringify({ schema_id: responseSchema, id: request?.id ?? null, ok: false, error: errorPayload(error) })}\n`); } })(); }); });
|
|
98
139
|
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(() => {});
|
|
99
140
|
}
|
|
100
141
|
import { authorizeRetainedDaemonResume } from "./driver-recovery.mjs";
|
|
@@ -103,11 +103,19 @@ export class CodexBridge {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
// Collaboration child Threads are provider facts. Keep only objects that
|
|
106
|
-
// explicitly carry
|
|
107
|
-
//
|
|
106
|
+
// explicitly carry a parent relationship, including native spawn receipts.
|
|
107
|
+
// A wait/send receiver is not necessarily a child of its sender.
|
|
108
108
|
observeTopology(value, depth = 0) {
|
|
109
109
|
if (!value || typeof value !== "object" || depth > 8) return;
|
|
110
110
|
if (Array.isArray(value)) { for (const item of value) this.observeTopology(item, depth + 1); return; }
|
|
111
|
+
if (value.type === "collabAgentToolCall" && value.tool === "spawnAgent" &&
|
|
112
|
+
typeof value.senderThreadId === "string" && value.senderThreadId && Array.isArray(value.receiverThreadIds)) {
|
|
113
|
+
for (const child of value.receiverThreadIds) {
|
|
114
|
+
if (typeof child !== "string" || !child || child === value.senderThreadId) continue;
|
|
115
|
+
this.descendants.set(child, { provider_session_id: child, parent_provider_session_id: value.senderThreadId,
|
|
116
|
+
status: value.agentsStates?.[child]?.status ?? "unknown", source: "codex.app_server" });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
111
119
|
const id = value.threadId ?? value.id ?? null;
|
|
112
120
|
const parent = value.parentThreadId ?? value.parent_thread_id ?? null;
|
|
113
121
|
if (typeof id === "string" && id && typeof parent === "string" && parent) {
|
|
@@ -8,7 +8,7 @@ import { durableDaemonDispatch, inspectDaemonOperation } from "./daemon-operatio
|
|
|
8
8
|
import { observedTimeout } from "./observation-clock.mjs";
|
|
9
9
|
import { writeJsonAtomic } from "./runner-events.mjs";
|
|
10
10
|
import { errorRecord } from "./operation-errors.mjs";
|
|
11
|
-
import { cleanupFailedStart, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, heartbeatDaemonProcess, registerDaemonProcess } from "./managed-daemon.mjs";
|
|
11
|
+
import { prepareRuntimeOwner, cleanupFailedStart, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, heartbeatDaemonProcess, registerDaemonProcess } from "./managed-daemon.mjs";
|
|
12
12
|
|
|
13
13
|
const REQUEST = "dd-droid/daemon-request@1", RESPONSE = "dd-droid/daemon-response@1";
|
|
14
14
|
async function removeSocket(file) { try { if (!(await lstat(file)).isSocket()) throw new DroidError("unsafe_socket", "Refusing to replace non-socket daemon path"); await unlink(file); } catch (error) { if (error.code !== "ENOENT") throw error; } }
|
|
@@ -29,6 +29,7 @@ function sameConfig(a, b) { return ["cwd", "bin", "model", "reasoning", "mode",
|
|
|
29
29
|
export async function startDaemon(options) {
|
|
30
30
|
const paths = droidPaths(options.stateDir), noFlow = options.noFlow === true || (!options.ddFlowBin && !options.ddFlowHome);
|
|
31
31
|
const config = { stateDir: paths.dir, home: paths.home, bin: options.bin ?? process.env.DD_DROID_BIN ?? "droid", cwd: absolute(options.cwd, "--cwd"), projectRoot: absolute(options.projectRoot ?? options.cwd, "--project-root"), journal: absolute(options.journal ?? `${paths.dir}/events.jsonl`, "--journal"), entryPath: absolute(options.entryPath, "adapter entry"), authHome: absolute(options.authHome ?? os.homedir(), "auth home"), model: options.model ?? "gpt-5.6-sol", reasoning: options.reasoning ?? "high", mode: options.mode ?? "auto-high", provider: options.provider ?? "openai", noFlow, 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 ?? null, inactivityMs: options.timeoutMs ?? 600_000 };
|
|
32
|
+
await prepareRuntimeOwner(config, paths.dir);
|
|
32
33
|
config.cwd = await realpath(config.cwd); config.projectRoot = await realpath(config.projectRoot);
|
|
33
34
|
if (config.mode !== "auto-high") throw new DroidError("droid_mode_unsupported", "The qualified Droid profile requires auto-high");
|
|
34
35
|
await mkdir(paths.dir, { recursive: true, mode: 0o700 }); await chmod(paths.dir, 0o700);
|
|
@@ -107,7 +108,7 @@ export async function serveDaemon(stateDir) {
|
|
|
107
108
|
result = { ...result, ...terminal };
|
|
108
109
|
}
|
|
109
110
|
}
|
|
110
|
-
} else result = await durableDaemonDispatch(paths.dir, request, () => dispatch(request)); const { _shutdown, ...receipt } = result;
|
|
111
|
+
} else result = await durableDaemonDispatch(paths.dir, request, () => dispatch(request), () => dispatch({ operation: "session.inspect", params: request.params ?? {} })); const { _shutdown, ...receipt } = result;
|
|
111
112
|
// Close the listening endpoint before replying. `server.close()` keeps
|
|
112
113
|
// this already accepted connection alive, so the caller receives its
|
|
113
114
|
// durable stop receipt while the Unix pathname is atomically retired.
|
|
@@ -11,7 +11,7 @@ import path from "node:path";
|
|
|
11
11
|
|
|
12
12
|
import { AcpBridge } from "./dd-zcode.mjs";
|
|
13
13
|
import { cancelSessionWithBridge, createSessionWithBridge, doctor, forkSessionWithBridge, inspectSessionWithBridge, promptSessionWithBridge } from "./dd-grok.mjs";
|
|
14
|
-
import { cleanupFailedStart, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, heartbeatDaemonProcess, registerDaemonProcess, stopProcessGroup } from "./managed-daemon.mjs";
|
|
14
|
+
import { prepareRuntimeOwner, cleanupFailedStart, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, heartbeatDaemonProcess, registerDaemonProcess, stopProcessGroup } from "./managed-daemon.mjs";
|
|
15
15
|
|
|
16
16
|
const REQUEST_SCHEMA = "dd-grok/daemon-request@1";
|
|
17
17
|
const RESPONSE_SCHEMA = "dd-grok/daemon-response@1";
|
|
@@ -168,6 +168,7 @@ export async function callDaemon(stateDir, operation, params = {}, timeoutMs = 3
|
|
|
168
168
|
function sameConfig(state, config) { return ["cwd", "journal", "bin", "authSource", "model", "reasoning", "mode", "ddFlowBin", "ddFlowHome", "projectRoot", "sessionArchive", "resourceHome", "noFlow"].every((key) => (state.config?.[key] ?? null) === (config[key] ?? null)); }
|
|
169
169
|
export async function startDaemon(options) {
|
|
170
170
|
const paths = locations(options.stateDir); const archive = options.sessionArchive ? await readSessionArchiveManifest(options.sessionArchive) : null; const archiveCwd = archive ? absolute(archive.manifest.source_cwd, "Grok Build archive source cwd") : null; const cwd = options.cwd ? absolute(options.cwd, "--cwd") : archiveCwd; if (!cwd) throw new DaemonError("invalid_path", "--cwd must be an absolute path when no session archive is supplied"); if (archiveCwd && cwd !== archiveCwd) throw new DaemonError("session_archive_cwd_mismatch", "Grok Build session archives must start from their recorded source cwd", false, { archive_cwd: archiveCwd, requested_cwd: cwd }); const noFlow = options.noFlow === true; const authSource = options.authPath ? absolute(options.authPath, "--auth-path") : path.join(os.homedir(), ".grok", "auth.json"); const config = { cwd, journal: absolute(options.journal, "--journal"), bin: options.bin ?? process.env.DD_GROK_BIN ?? "grok", authSource, authPath: path.join(paths.home, "auth.json"), model: String(options.model ?? ""), reasoning: String(options.reasoning ?? ""), mode: options.mode ?? "bypassPermissions", ddFlowBin: noFlow ? null : absolute(options.ddFlowBin, "--dd-flow-bin"), ddFlowHome: noFlow ? null : absolute(options.ddFlowHome, "--dd-flow-home"), projectRoot: noFlow ? cwd : absolute(options.projectRoot, "--project-root"), 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: archive?.source ?? null, grokHome: paths.home, noFlow };
|
|
171
|
+
await prepareRuntimeOwner(config, paths.dir);
|
|
171
172
|
if (!config.model || !config.reasoning) throw new DaemonError("profile_required", "--model and --reasoning are required");
|
|
172
173
|
await mkdir(paths.dir, { recursive: true, mode: 0o700 }); await chmod(paths.dir, 0o700);
|
|
173
174
|
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 (error.code !== "daemon_not_running" && error.code !== "daemon_timeout") throw error; }
|
|
@@ -214,6 +215,11 @@ export class Runtime {
|
|
|
214
215
|
topology(root) { return [...this.descendants.values()].filter((child) => child.parent_provider_session_id === root); }
|
|
215
216
|
observeToolCall(message) { const sessionId = message?.params?.sessionId; const update = message?.params?.update; const id = update?.toolCallId; if (!sessionId || !id) return; this.toolEvents ??= new Map(); const events = this.toolEvents.get(sessionId) ?? { calls: new Map(), failed: new Set() }; if (update.sessionUpdate === "tool_call") events.calls.set(id, update?._meta?.["x.ai/tool"]?.name ?? update?._meta?.claudeCode?.toolName ?? update.title?.split(":", 1)[0] ?? "unknown"); if (update.sessionUpdate === "tool_call_update" && ["failed", "error"].includes(update.status)) events.failed.add(id); this.toolEvents.set(sessionId, events); const by_tool = {}; for (const name of events.calls.values()) by_tool[name] = (by_tool[name] ?? 0) + 1; this.toolUsage.set(sessionId, { total: events.calls.size, failures: events.failed.size, by_tool }); }
|
|
216
217
|
running(result) { return result?.evidence?.subagents?.subagents ?? result?.subagents?.subagents ?? []; }
|
|
218
|
+
treeRunning(result) {
|
|
219
|
+
const evidence = result?.evidence ?? result;
|
|
220
|
+
const children = evidence?.subagents?.subagents;
|
|
221
|
+
return this.rootRunning({ ...result, ...evidence }) || !Array.isArray(children) || children.length > 0 || Boolean(evidence?.subagents?.observation_error);
|
|
222
|
+
}
|
|
217
223
|
rootRunning(result) {
|
|
218
224
|
const status = String(result?.info?.status ?? result?.info?.state ?? result?.info?.data?.status ?? "unknown").toLowerCase();
|
|
219
225
|
// Grok 1.0.17 omits status from session/info. Only a durable native
|
|
@@ -230,18 +236,18 @@ export class Runtime {
|
|
|
230
236
|
let observed = await inspectSessionWithBridge(this.bridge, this.options({ sessionId: id, liveSession: true }));
|
|
231
237
|
this.track(observed);
|
|
232
238
|
const rootActive = Boolean(this.active);
|
|
233
|
-
if (cancel && (rootActive || this.
|
|
239
|
+
if (cancel && (rootActive || this.treeRunning(observed))) {
|
|
234
240
|
await cancelSessionWithBridge(this.bridge, this.options({ sessionId: id, liveSession: true }));
|
|
235
241
|
observed = await inspectSessionWithBridge(this.bridge, this.options({ sessionId: id, liveSession: true }));
|
|
236
242
|
this.track(observed);
|
|
237
243
|
}
|
|
238
|
-
if (this.active || this.
|
|
244
|
+
if (this.active || this.treeRunning(observed)) running.push(id);
|
|
239
245
|
}
|
|
240
246
|
await this.persist({ active_tree: running.length > 0 || Boolean(this.active) });
|
|
241
247
|
return running;
|
|
242
248
|
}
|
|
243
249
|
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 }); }
|
|
244
|
-
async productive(name, task) { if (this.active) throw new DaemonError("operation_busy", `${this.active} is already running`, true); this.active = name; for (const session of this.sessions.values()) session.native_root_receipt = null; try { await this.persist({ active_tree: true, active_operation: name }); const result = await task(); this.track(result); const session = this.sessions.get(result.provider_session_id); if (session && (["session.create", "session.fork"].includes(name) || ["end_turn", "cancelled"].includes(result.turn?.stopReason))) session.native_root_receipt = { kind: name === "session.prompt" ? result.turn.stopReason : "created", daemon_id: this.state.daemon_id }; await this.persist({ active_tree: this.
|
|
250
|
+
async productive(name, task) { if (this.active) throw new DaemonError("operation_busy", `${this.active} is already running`, true); this.active = name; for (const session of this.sessions.values()) session.native_root_receipt = null; try { await this.persist({ active_tree: true, active_operation: name }); const result = await task(); this.track(result); const session = this.sessions.get(result.provider_session_id); if (session && (["session.create", "session.fork"].includes(name) || ["end_turn", "cancelled"].includes(result.turn?.stopReason))) session.native_root_receipt = { kind: name === "session.prompt" ? result.turn.stopReason : "created", daemon_id: this.state.daemon_id }; await this.persist({ active_tree: this.treeRunning(result), active_operation: null }); return { ...result, descendants: this.topology(result.provider_session_id) }; } catch (error) { await this.persist({ active_tree: true, active_operation: null }); throw error; } finally { this.active = null; } }
|
|
245
251
|
async dispatch(operation, params) {
|
|
246
252
|
const assertDispatch = captureDispatchGuard(this);
|
|
247
253
|
if (operation === "daemon.status") { let observationError; if (!this.active) try { await this.refreshTree(); } catch (error) { observationError = errorPayload(error); } return { observation_error: observationError ?? null, 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 }; }
|
|
@@ -249,7 +255,7 @@ export class Runtime {
|
|
|
249
255
|
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; });
|
|
250
256
|
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; }); }
|
|
251
257
|
if (operation === "session.fork") { await this.requireSettled(); return await this.productive(operation, async () => { const result = await forkSessionWithBridge(this.bridge, this.options(params)); this.loadedSessionId = params.sessionId; return result; }); }
|
|
252
|
-
if (operation === "session.inspect") { const result = await inspectSessionWithBridge(this.bridge, this.options({ ...params, liveSession: true })); this.track(result); await this.persist(); return { ...result, descendants: this.topology(result.provider_session_id) }; }
|
|
258
|
+
if (operation === "session.inspect" || operation === "session.resume") { const result = await inspectSessionWithBridge(this.bridge, this.options({ ...params, liveSession: true })); this.track(result); await this.persist(); return { ...result, settled: !this.active && !this.treeRunning(result), descendants: this.topology(result.provider_session_id) }; }
|
|
253
259
|
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; }
|
|
254
260
|
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); }); }
|
|
255
261
|
if (operation === "daemon.stop") {
|
|
@@ -277,6 +283,6 @@ export async function serveDaemon(stateDir) {
|
|
|
277
283
|
const rootProviderSessionId = session?.root_provider_session_id ?? sessionId;
|
|
278
284
|
await observeAcpToolCall(state.config, { daemonId: state.daemon_id, rootProviderSessionId, parentProviderSessionId: session?.parent_provider_session_id ?? null, observedProfile: bridge.modelProfiles?.get(sessionId) ?? {} }, message);
|
|
279
285
|
} }); const initialized = await bridge.start(); runtime = new Runtime(paths, state, bridge, initialized);
|
|
280
|
-
await removeSocket(paths.socket, paths.dir); const server = net.createServer((connection) => { connection.setEncoding("utf8"); let buffer = ""; connection.on("data", (chunk) => { buffer += chunk; const newline = buffer.indexOf("\n"); if (newline < 0) return; const line = buffer.slice(0, newline); buffer = buffer.slice(newline + 1); void (async () => { let request; try { request = JSON.parse(line); if (request.schema_id !== REQUEST_SCHEMA || !request.id || !request.operation) throw new DaemonError("daemon_protocol_mismatch", "invalid daemon request"); const result = await durableDaemonDispatch(paths.dir, request, () => runtime.dispatch(request.operation, request.params ?? {})); connection.end(`${JSON.stringify({ schema_id: RESPONSE_SCHEMA, id: request.id, ok: true, result: { ...result, _shutdown: undefined } })}\n`); if (result?._shutdown) setImmediate(() => void runtime.shutdown().then(() => process.exit(0)).catch(error => runtime.persist({ shutdown_state: "cleanup_failed", cleanup_error: errorPayload(error) }))); } catch (error) { connection.end(`${JSON.stringify({ schema_id: RESPONSE_SCHEMA, id: request?.id ?? null, ok: false, error: errorPayload(error) })}\n`); } })(); }); }); runtime.server = server; 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 }); return await new Promise(() => {});
|
|
286
|
+
await removeSocket(paths.socket, paths.dir); const server = net.createServer((connection) => { connection.setEncoding("utf8"); let buffer = ""; connection.on("data", (chunk) => { buffer += chunk; const newline = buffer.indexOf("\n"); if (newline < 0) return; const line = buffer.slice(0, newline); buffer = buffer.slice(newline + 1); void (async () => { let request; try { request = JSON.parse(line); if (request.schema_id !== REQUEST_SCHEMA || !request.id || !request.operation) throw new DaemonError("daemon_protocol_mismatch", "invalid daemon request"); const result = await durableDaemonDispatch(paths.dir, request, () => runtime.dispatch(request.operation, request.params ?? {}), () => runtime.dispatch("session.inspect", request.params ?? {})); connection.end(`${JSON.stringify({ schema_id: RESPONSE_SCHEMA, id: request.id, ok: true, result: { ...result, _shutdown: undefined } })}\n`); if (result?._shutdown) setImmediate(() => void runtime.shutdown().then(() => process.exit(0)).catch(error => runtime.persist({ shutdown_state: "cleanup_failed", cleanup_error: errorPayload(error) }))); } catch (error) { connection.end(`${JSON.stringify({ schema_id: RESPONSE_SCHEMA, id: request?.id ?? null, ok: false, error: errorPayload(error) })}\n`); } })(); }); }); runtime.server = server; 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 }); return await new Promise(() => {});
|
|
281
287
|
}
|
|
282
288
|
import { authorizeRetainedDaemonResume } from "./driver-recovery.mjs";
|
|
@@ -10,7 +10,7 @@ import { checkObservedProfile, observeModel } from "./model-observations.mjs";
|
|
|
10
10
|
import os from "node:os";
|
|
11
11
|
import path from "node:path";
|
|
12
12
|
import { OpenCodeClient, OpenCodeError, canonicalHistory, historyDigest, usageSnapshot } from "./dd-opencode.mjs";
|
|
13
|
-
import { cleanupFailedStart, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, heartbeatDaemonProcess, registerDaemonProcess, stopProcessGroup } from "./managed-daemon.mjs";
|
|
13
|
+
import { prepareRuntimeOwner, cleanupFailedStart, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, heartbeatDaemonProcess, registerDaemonProcess, stopProcessGroup } from "./managed-daemon.mjs";
|
|
14
14
|
|
|
15
15
|
const REQUEST_SCHEMA = "dd-opencode/daemon-request@1", RESPONSE_SCHEMA = "dd-opencode/daemon-response@1", STATE_SCHEMA = "dd-opencode/daemon-state@1", ARCHIVE_SCHEMA = "dd-opencode/session-archive@1";
|
|
16
16
|
const OPENCODE_BASELINE = Object.freeze({ version: "1.18.25", api_fingerprint: "dfb7d42a555389f0c662fa2b4a8af1d61633c96710cf54bce3ff2404e2e7d896" });
|
|
@@ -50,6 +50,7 @@ function sameConfig(state, config) { return ["cwd","journal","bin","provider","m
|
|
|
50
50
|
export async function startDaemon(options) {
|
|
51
51
|
const paths = locations(options.stateDir); await mkdir(paths.dir, { recursive: true, mode: 0o700 }); await chmod(paths.dir, 0o700);
|
|
52
52
|
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 };
|
|
53
|
+
await prepareRuntimeOwner(config, paths.dir);
|
|
53
54
|
if (!config.provider || !config.model) throw new DaemonError("profile_required", "--provider and --model are required");
|
|
54
55
|
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; }
|
|
55
56
|
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");
|
|
@@ -102,7 +103,7 @@ export class Runtime {
|
|
|
102
103
|
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 }; }
|
|
103
104
|
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 }; }
|
|
104
105
|
if (operation === "session.create") return this.productive(operation, async () => { const info = await this.client.create(params.title); this.sessions.set(info.id, { provider_session_id: info.id, adapter_session_id: info.id, parent_provider_session_id: info.parentID ?? null, root_provider_session_id: info.id, cwd: info.directory }); const result = await this.describe(info.id); await this.reconcileUsage(result); return result; });
|
|
105
|
-
if (operation === "session.inspect") { const result = await this.describe(params.sessionId); await this.persist(); return result; }
|
|
106
|
+
if (operation === "session.inspect" || operation === "session.resume") { const result = await this.describe(params.sessionId); await this.persist(); return result; }
|
|
106
107
|
if (operation === "session.prompt") { await this.requireSettled(params.sessionId); return this.productive(operation, async () => { assertDispatch(); const reply = await this.client.prompt(params.sessionId, { prompt: params.prompt, provider: this.state.config.provider, model: this.state.config.model, variant: this.state.config.variant, agent: this.state.config.agent,
|
|
107
108
|
assertDispatch,
|
|
108
109
|
onModelMessages: (id, messages, parent) => this.observeModels(id, messages, parent),
|
|
@@ -154,7 +155,7 @@ export async function serveDaemon(stateDir) {
|
|
|
154
155
|
child = spawn(target.command, target.args, { cwd: state.config.cwd, env: isolatedEnv(state.config), detached: true, stdio: ["ignore", "inherit", "inherit"] });
|
|
155
156
|
await confirmDaemonProcess(state.config, providerProcess, child);
|
|
156
157
|
state.provider_process = providerProcess; state.provider_pid = child.pid; await writeJson(paths.state, state);
|
|
157
|
-
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); const server = net.createServer(connection => { connection.setEncoding("utf8"); let buffer = ""; connection.on("data", chunk => { buffer += chunk; const i = buffer.indexOf("\n"); if (i < 0) return; void (async () => { let request; try { request = JSON.parse(buffer.slice(0, i)); const result = await durableDaemonDispatch(paths.dir, request, () => runtime.dispatch(request.operation, request.params ?? {})); connection.end(`${JSON.stringify({ schema_id: RESPONSE_SCHEMA, id: request.id, ok: true, result: { ...result, _shutdown: undefined } })}\n`); if (result?._shutdown) setImmediate(() => runtime.shutdown().then(() => process.exit(0)).catch(error => runtime.persist({ shutdown_state: "cleanup_failed", cleanup_error: errorPayload(error) }))); } catch (error) { connection.end(`${JSON.stringify({ schema_id: RESPONSE_SCHEMA, id: request?.id ?? null, ok: false, error: errorPayload(error) })}\n`); } })(); }); }); runtime.server = server; 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(() => {});
|
|
158
|
+
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); const server = net.createServer(connection => { connection.setEncoding("utf8"); let buffer = ""; connection.on("data", chunk => { buffer += chunk; const i = buffer.indexOf("\n"); if (i < 0) return; void (async () => { let request; try { request = JSON.parse(buffer.slice(0, i)); const result = await durableDaemonDispatch(paths.dir, request, () => runtime.dispatch(request.operation, request.params ?? {}), () => runtime.dispatch("session.inspect", request.params ?? {})); connection.end(`${JSON.stringify({ schema_id: RESPONSE_SCHEMA, id: request.id, ok: true, result: { ...result, _shutdown: undefined } })}\n`); if (result?._shutdown) setImmediate(() => runtime.shutdown().then(() => process.exit(0)).catch(error => runtime.persist({ shutdown_state: "cleanup_failed", cleanup_error: errorPayload(error) }))); } catch (error) { connection.end(`${JSON.stringify({ schema_id: RESPONSE_SCHEMA, id: request?.id ?? null, ok: false, error: errorPayload(error) })}\n`); } })(); }); }); runtime.server = server; 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(() => {});
|
|
158
159
|
} catch (error) {
|
|
159
160
|
await cleanupFailedStart(state.config, providerProcess, child, error); throw error;
|
|
160
161
|
}
|