@deksden-com/dd-flow-cli 0.9.0-beta.22 → 0.9.0-beta.25

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.
Files changed (46) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +9 -0
  3. package/dist/build-info.json +3 -3
  4. package/dist/cli/help.js +1 -0
  5. package/dist/cli/run-cli.js +3 -0
  6. package/dist/harness-runtime/bin/dd-agy.mjs +34 -0
  7. package/dist/harness-runtime/bin/dd-codex.mjs +22 -0
  8. package/dist/harness-runtime/bin/dd-droid.mjs +30 -0
  9. package/dist/harness-runtime/bin/dd-grok.mjs +28 -0
  10. package/dist/harness-runtime/bin/dd-opencode.mjs +18 -0
  11. package/dist/harness-runtime/bin/dd-zcode.mjs +62 -0
  12. package/dist/harness-runtime/lib/daemon-operations.mjs +78 -0
  13. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +259 -0
  14. package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
  15. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +93 -0
  16. package/dist/harness-runtime/lib/dd-codex.mjs +387 -0
  17. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +123 -0
  18. package/dist/harness-runtime/lib/dd-droid.mjs +435 -0
  19. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +282 -0
  20. package/dist/harness-runtime/lib/dd-grok.mjs +168 -0
  21. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +157 -0
  22. package/dist/harness-runtime/lib/dd-opencode.mjs +80 -0
  23. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +419 -0
  24. package/dist/harness-runtime/lib/dd-zcode.mjs +598 -0
  25. package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
  26. package/dist/harness-runtime/lib/driver-recovery.mjs +129 -0
  27. package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
  28. package/dist/harness-runtime/lib/managed-daemon.mjs +119 -0
  29. package/dist/harness-runtime/lib/model-observations.mjs +77 -0
  30. package/dist/harness-runtime/lib/observation-clock.mjs +33 -0
  31. package/dist/harness-runtime/lib/operation-context.mjs +5 -0
  32. package/dist/harness-runtime/lib/operation-errors.mjs +19 -0
  33. package/dist/harness-runtime/lib/process-json.mjs +61 -0
  34. package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
  35. package/dist/harness-runtime/lib/runner-events.mjs +200 -0
  36. package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
  37. package/dist/harness-runtime/lib/session-settlement.mjs +28 -0
  38. package/dist/services/code-checks.js +25 -11
  39. package/dist/services/execution-policy.js +61 -0
  40. package/dist/services/external-work-launch.js +76 -0
  41. package/dist/services/harness-adapter.js +31 -14
  42. package/dist/services/runs.js +2 -1
  43. package/dist/services/stage-context.js +2 -2
  44. package/dist/services/vnext-execution-profile.js +38 -3
  45. package/dist/storage/database.js +21 -0
  46. package/package.json +9 -3
@@ -0,0 +1,259 @@
1
+ import { captureDispatchGuard, cancelPendingDispatch } from "./dispatch-fence.mjs";
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import { appendFile, chmod, lstat, mkdir, open, readFile, realpath, rename, unlink, writeFile } from "node:fs/promises";
4
+ import { spawn } from "node:child_process";
5
+ import net from "node:net";
6
+ import { observedTimeout } from "./observation-clock.mjs";
7
+ import { durableDaemonDispatch } from "./daemon-operations.mjs";
8
+ import path from "node:path";
9
+ import { observeModel } from "./model-observations.mjs";
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";
12
+
13
+ const REQUEST_SCHEMA = "dd-agy/daemon-request@1", RESPONSE_SCHEMA = "dd-agy/daemon-response@1", STATE_SCHEMA = "dd-agy/daemon-state@1";
14
+ class DaemonError extends AgyError {}
15
+ const absolute = (value, label) => { if (!value || !path.isAbsolute(value)) throw new DaemonError("invalid_path", `${label} must be an absolute path`); return path.resolve(value); };
16
+ function locations(stateDir) {
17
+ const dir = absolute(stateDir, "--state-dir"), digest = createHash("sha256").update(dir).digest("hex").slice(0, 22), local = path.join(dir, "daemon.sock");
18
+ // `tsx` creates Unix-domain sockets below TMPDIR. An evaluation path is
19
+ // intentionally descriptive and can exceed the platform's socket-path cap,
20
+ // so provider temp files must not inherit that path.
21
+ return { dir, socket: Buffer.byteLength(local) < 100 ? local : `/tmp/dd-agy-${digest}.sock`, state: path.join(dir, "daemon.json"), journal: path.join(dir, "events.jsonl"), log: path.join(dir, "daemon.log"), gemini: path.join(dir, "gemini"), runtime: path.join(dir, "gemini", "runtime"), config: path.join(dir, "gemini", "config"), temporary: `/tmp/dd-agy-tmp-${digest}` };
22
+ }
23
+ async function writeJson(file, value) { const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`; await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 }); await rename(temporary, file); }
24
+ async function readState(dir) { try { return JSON.parse(await readFile(locations(dir).state, "utf8")); } catch (error) { if (error.code === "ENOENT") return null; throw error; } }
25
+ async function removeSocket(paths) { try { if (!(await lstat(paths.socket)).isSocket()) throw new DaemonError("unsafe_socket", "refusing to replace a non-socket daemon path"); await unlink(paths.socket); } catch (error) { if (error.code !== "ENOENT") throw error; } }
26
+ const shell = value => `'${String(value).replaceAll("'", `'"'"'`)}'`;
27
+ function sanitizedEnv(config) { const env = { ...process.env, TMPDIR: config.temporary }; for (const key of ["GEMINI_API_KEY","GOOGLE_API_KEY","ANTHROPIC_API_KEY","OPENAI_API_KEY"]) delete env[key]; return env; }
28
+ function errorPayload(error) { return { code: error.code ?? "operation_failed", message: error.message ?? String(error), retryable: error.retryable === true, ...(error.details === undefined ? {} : { details: error.details }) }; }
29
+ async function forwardUsage(config, sessionId, result, tools, descendants) {
30
+ if (!config.ddFlowBin || !config.projectRoot) return;
31
+ const snapshot = usageSnapshot(result, tools), hasChildren = descendants.length > 0;
32
+ if (snapshot.total_tokens === null) return { status: "unavailable", reason: "provider_did_not_report_total_tokens" };
33
+ const payload = { provider_session_id: sessionId, daemon_id: config.daemonId, observed_at: new Date().toISOString(), usage_scope: hasChildren ? "unknown" : "physical_session", completeness: hasChildren ? "partial" : "complete", usage: { inputTokens: snapshot.input_tokens, outputTokens: snapshot.output_tokens, reasoningTokens: snapshot.reasoning_tokens, cacheReadInputTokens: snapshot.cache_read_tokens, totalTokens: snapshot.total_tokens }, tool_calls: snapshot.tool_calls };
34
+ const target = executable(config.ddFlowBin, ["agy", "usage", "ingest", "--project-root", config.projectRoot, "--json"]);
35
+ await new Promise((resolve, reject) => { const child = spawn(target.command, target.args, { env: { ...process.env, DD_FLOW_HOME: config.ddFlowHome }, stdio: ["pipe", "ignore", "pipe"] }); let stderr = ""; child.stderr.setEncoding("utf8").on("data", chunk => stderr += chunk); child.on("error", reject); child.on("close", code => code === 0 ? resolve() : reject(new DaemonError("agy_usage_ingest_failed", stderr.trim() || "dd-flow rejected Antigravity usage"))); child.stdin.end(`${JSON.stringify(payload)}\n`); });
36
+ return { status: "ingested" };
37
+ }
38
+
39
+ async function prepare(paths, config) {
40
+ for (const dir of [paths.dir, paths.gemini, paths.runtime, paths.config, paths.temporary]) { await mkdir(dir, { recursive: true, mode: 0o700 }); await chmod(dir, 0o700); }
41
+ if (config.noFlow) return;
42
+ 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`;
43
+ const hooks = { "dd-flow": {
44
+ PreToolUse: [{ matcher: "*", hooks: [{ type: "command", command: command.replace("__EVENT__", "PreToolUse"), timeout: 30 }] }],
45
+ PostToolUse: [{ matcher: "*", hooks: [{ type: "command", command: command.replace("__EVENT__", "PostToolUse"), timeout: 30 }] }],
46
+ Stop: [{ type: "command", command: command.replace("__EVENT__", "Stop"), timeout: 30 }]
47
+ } };
48
+ await writeJson(path.join(paths.config, "hooks.json"), hooks);
49
+ config.hooksSha256 = createHash("sha256").update(JSON.stringify(hooks)).digest("hex");
50
+ }
51
+
52
+ export async function callDaemon(stateDir, operation, params = {}, timeoutMs = 30_000) {
53
+ const paths = locations(stateDir), request = { schema_id: REQUEST_SCHEMA, id: process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation, params };
54
+ return await new Promise((resolve, reject) => {
55
+ const client = net.createConnection(paths.socket); let buffer = "", settled = false, timer, lastActivity = Date.now();
56
+ const finish = (error, result) => { if (settled) return; settled = true; if (timer) clearTimeout(timer); client.destroy(); error ? reject(error) : resolve(result); };
57
+ const arm = () => { timer = observedTimeout(async () => {
58
+ // Prompt RPCs return only at the end. Native events update this durable
59
+ // timestamp, so a moving timestamp proves productive provider activity.
60
+ if (operation !== "session.prompt") return finish(new DaemonError("daemon_timeout", `${operation} timed out`, true));
61
+ let status = null;
62
+ try {
63
+ status = await callDaemon(stateDir, "daemon.status", {}, Math.min(timeoutMs, 5_000));
64
+ const observed = Date.parse(status.last_activity_at ?? "");
65
+ if (status.active_tree && Number.isFinite(observed) && observed > lastActivity) { lastActivity = observed; return arm(); }
66
+ } catch { /* The original RPC retains the authoritative failure. */ }
67
+ finish(new DaemonError(status?.active_tree ? "subject_liveness_timeout" : "operation_observation_lost", `${operation} has no recent productive provider activity`, true, {
68
+ operation_id: request.id,
69
+ last_activity_at: status?.last_activity_at ?? null,
70
+ active_tree: status?.active_tree === true
71
+ }));
72
+ }, timeoutMs, { onGap: gap => { void appendFile(paths.journal, `${JSON.stringify({ kind: "observation_gap", payload: { ...gap, operation_id: request.id } })}\n`, { mode: 0o600 }).catch(() => {}); } }); };
73
+ arm(); client.setEncoding("utf8"); client.on("connect", () => client.write(`${JSON.stringify(request)}\n`)); client.on("data", chunk => { buffer += chunk; const index = buffer.indexOf("\n"); if (index < 0) return; try { const response = JSON.parse(buffer.slice(0, index)); response.ok ? finish(null, response.result) : finish(new DaemonError(response.error.code, response.error.message, response.error.retryable, response.error.details)); } catch (error) { finish(new DaemonError("daemon_protocol_mismatch", error.message)); } }); client.on("error", error => finish(new DaemonError("daemon_not_running", error.message, true))); client.on("end", () => finish(new DaemonError("daemon_connection_closed", `${operation} connection closed before a response`, true))); client.on("close", () => finish(new DaemonError("daemon_connection_closed", `${operation} connection closed before a response`, true)));
74
+ });
75
+ }
76
+
77
+ function sameConfig(state, config) { return ["cwd","bin","provider","model","reasoning","mode","projectRoot","ddFlowBin","ddFlowHome","resourceHome","noFlow"].every(key => (state.config?.[key] ?? null) === (config[key] ?? null)); }
78
+ export async function startDaemon(options) {
79
+ const paths = locations(options.stateDir); await mkdir(paths.dir, { recursive: true, mode: 0o700 }); await chmod(paths.dir, 0o700);
80
+ const previous = await readState(paths.dir), noFlow = options.noFlow === true, cwd = await realpath(absolute(options.cwd, "--cwd")), config = { cwd, 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 };
81
+ 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; }
82
+ if (previous?.shutdown_state === "clean") await authorizeRetainedDaemonResume(paths.dir, previous, options.sessionId, config);
83
+ if (previous?.shutdown_state === "running" && previous.active_tree) throw new DaemonError("invalid_harness_crash", "previous Antigravity daemon died with an active or unproven tree");
84
+ await removeSocket(paths); await prepare(paths, config);
85
+ const version = (await runAgy(config.bin, ["--version"], { cwd: config.cwd, env: sanitizedEnv(config), timeoutMs: 10_000 })).stdout.trim();
86
+ const resourceProcess = await registerDaemonProcess(config, { kind: "agy-daemon", owner: `agy:${path.basename(paths.dir)}`, operation: `agy-daemon:${path.basename(paths.dir)}`, stdout: paths.log, stderr: paths.log });
87
+ const resumedObservations = config.resumeSessionId && previous?.sessions?.some(session => session.provider_session_id === config.resumeSessionId) ? previous.session_observations ?? [] : [];
88
+ await writeJson(paths.state, { schema_id: STATE_SCHEMA, daemon_id: config.daemonId, shutdown_state: "starting", active_tree: false, versions: { agy: version }, config, sessions: [], session_observations: resumedObservations, resource_process: resourceProcess });
89
+ const log = await open(paths.log, "a", 0o600), child = spawn(process.execPath, [config.entryPath, "daemon", "serve", "--state-dir", paths.dir], { cwd: config.cwd, env: sanitizedEnv(config), detached: true, stdio: ["ignore", log.fd, log.fd] }); child.unref(); await log.close();
90
+ try {
91
+ await confirmDaemonProcess(config, resourceProcess, child);
92
+ const deadline = Date.now() + 30_000; while (Date.now() < deadline) { await new Promise(resolve => setTimeout(resolve, 100));
93
+ const persisted = await readState(paths.dir);
94
+ if (persisted?.shutdown_state === "failed") {
95
+ const failure = persisted.startup_error ?? {};
96
+ throw new DaemonError(failure.code ?? "daemon_start_failed", failure.message ?? "Antigravity daemon failed before becoming ready", failure.retryable === true, failure.details);
97
+ }
98
+ try { const status = await callDaemon(paths.dir, "daemon.status", {}, 1000); if (status.provider_ready) return status; } catch (error) { if (!["daemon_not_running","daemon_timeout"].includes(error.code)) throw error; }
99
+ }
100
+ throw new DaemonError("daemon_start_failed", "Antigravity daemon did not become ready", true);
101
+ } catch (error) {
102
+ await cleanupFailedStart(config, resourceProcess, child, error);
103
+ const persisted = await readState(paths.dir).catch(() => null);
104
+ if (persisted?.provider_process) {
105
+ try { await runtimeProcess(config, "stop", { id: persisted.provider_process.id, "lease-token": persisted.provider_process.lease_token }); }
106
+ catch (cleanup) { error.details = { ...error.details, provider_cleanup_failed: errorPayload(cleanup), provider_process_id: persisted.provider_process.id }; }
107
+ }
108
+ throw error;
109
+ }
110
+ }
111
+ 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); };
112
+
113
+ export class Runtime {
114
+ constructor(paths, state) { this.paths = paths; this.state = state; this.config = state.config; this.child = null; this.providerProcess = state.provider_process ?? null; this.buffer = ""; this.init = null; this.readyPromise = null; this.active = null; this.exited = false; this.toolCalls = new Map(); this.completedToolSteps = new Set(); this.toolIdentityComplete = true; this.descendants = new Map((state.descendants ?? []).map(child => [child.provider_session_id, child])); this.lastResult = null; this.sessionObservations = new Map(state.session_observations ?? []); this.startupError = null; this.persisting = Promise.resolve(); this.draining = Promise.resolve(); this.lastActivityAt = state.last_activity_at ?? null; this.usageIngest = null; this.lastProviderEvent = null; }
115
+ persist(patch = {}) { Object.assign(this.state, patch, { pid: process.pid, updated_at: new Date().toISOString(), last_activity_at: this.lastActivityAt, session_observations: [...this.sessionObservations], descendants: [...this.descendants.values()], sessions: this.init ? [{ provider_session_id: this.init.conversation_id, root_provider_session_id: this.init.conversation_id, cwd: this.config.cwd }] : [] }); const snapshot = structuredClone(this.state); this.persisting = this.persisting.then(() => writeJson(this.paths.state, snapshot)); for (const processRecord of [this.state.resource_process, this.providerProcess]) if (processRecord?.id && processRecord.lease_token) this.persisting = this.persisting.then(async () => { try { await heartbeatDaemonProcess(this.config, processRecord); } catch {} }); return this.persisting; }
116
+ async journal(kind, payload = {}) { await appendFile(this.paths.journal, `${JSON.stringify({ schema_id: "dd-agy/daemon-event@1", event_id: randomUUID(), observed_at: new Date().toISOString(), kind, ...payload })}\n`, { mode: 0o600 }); }
117
+ args(sessionId) { return ["--input-format", "stream-json", "--output-format", "stream-json", "--model", this.config.model, "--effort", this.config.reasoning, "--mode", this.config.mode, "--dangerously-skip-permissions", "--print-timeout", "6h", `--gemini_dir=${this.paths.gemini}`, "--app_data_dir=runtime", ...(sessionId ? ["--conversation", sessionId] : ["--new-project"])]; }
118
+ requireSessionIdentity(requestedId) {
119
+ const observedId = this.init?.conversation_id ?? null;
120
+ if (!requestedId || requestedId === observedId) return observedId;
121
+ throw new DaemonError("session_identity_mismatch", "requested Antigravity Session is not owned by this daemon", false, {
122
+ harness: "antigravity-cli", expected_session_id: requestedId, observed_session_id: observedId
123
+ });
124
+ }
125
+ markActivity() { this.lastActivityAt = new Date().toISOString(); }
126
+ observeProviderActivity(event) {
127
+ const fingerprint = createHash("sha256").update(JSON.stringify(event)).digest("hex");
128
+ if (this.lastProviderEvent === fingerprint) return;
129
+ this.lastProviderEvent = fingerprint;
130
+ if (["init", "step_update", "result"].includes(event.event)) this.markActivity();
131
+ }
132
+ async start(sessionId = null) {
133
+ if (this.child && !this.exited) {
134
+ await this.readyPromise;
135
+ this.requireSessionIdentity(sessionId);
136
+ return this.init;
137
+ }
138
+ const target = executable(this.config.bin, this.args(sessionId)); this.exited = false; this.buffer = ""; this.init = null;
139
+ let readyResolve, readyReject; this.readyPromise = new Promise((resolve, reject) => { readyResolve = resolve; readyReject = reject; });
140
+ // The provider can reject before durable registration finishes. Keep a
141
+ // handler attached during that small window; `start()` still returns the
142
+ // original rejection below.
143
+ void this.readyPromise.catch(() => {});
144
+ this.providerProcess = await registerDaemonProcess(this.config, { kind: "agy-provider", owner: `agy:${path.basename(this.paths.dir)}`, operation: `agy-provider:${path.basename(this.paths.dir)}`, stdout: this.paths.log, stderr: this.paths.log });
145
+ const providerProcess = this.providerProcess;
146
+ this.child = spawn(target.command, target.args, { cwd: this.config.cwd, env: sanitizedEnv(this.config), detached: true, stdio: ["pipe", "pipe", "pipe"] });
147
+ const providerChild = this.child;
148
+ let allowProviderFinalization;
149
+ const providerRegistered = new Promise(resolve => { allowProviderFinalization = resolve; });
150
+ this.child.stdout.setEncoding("utf8").on("data", chunk => { this.buffer += chunk; this.draining = this.draining.then(() => this.drain(readyResolve)).catch(error => { readyReject(error); if (this.active) { this.active.reject(error); this.active = null; } }); });
151
+ this.child.stderr.setEncoding("utf8").on("data", chunk => void this.journal("stderr", { text: String(chunk).slice(0, 4000) }));
152
+ this.child.on("error", error => { readyReject(error); if (this.active) { this.active.reject(error); this.active = null; } });
153
+ this.child.on("close", (code, signal) => { const interrupted = Boolean(this.active); this.exited = true; const beforeInit = !this.init;
154
+ const error = this.startupError ?? new DaemonError(beforeInit ? "agy_init_missing" : "agy_terminal_result_missing", `Antigravity exited ${beforeInit ? "before init" : "before result"} (${code ?? signal})`);
155
+ if (beforeInit) readyReject(error);
156
+ if (this.active) { this.active.reject(error); this.active = null; }
157
+ const patch = beforeInit ? { shutdown_state: "failed", startup_error: errorPayload(error), provider_exit: { code, signal }, active_tree: false } : { provider_exit: { code, signal }, active_tree: interrupted };
158
+ void providerRegistered.then(async () => {
159
+ try {
160
+ await stopProcessGroup(providerChild);
161
+ await this.persist(patch);
162
+ await finishDaemonProcess(this.config, providerProcess, beforeInit || interrupted ? "failed" : "stopped", beforeInit || interrupted ? "provider_interrupted" : "provider_stopped");
163
+ } catch (cleanup) { await this.persist({ shutdown_state: "cleanup_failed", active_tree: true, cleanup_error: errorPayload(cleanup) }); }
164
+ }).finally(() => { if (beforeInit) this.server?.close(); });
165
+ });
166
+ try { await confirmDaemonProcess(this.config, providerProcess, this.child); await this.persist({ provider_process: providerProcess, provider_pid: this.child.pid }); }
167
+ catch (error) { await cleanupFailedStart(this.config, providerProcess, this.child, error); throw error; }
168
+ finally { allowProviderFinalization(); }
169
+ return await Promise.race([this.readyPromise, new Promise((_, reject) => setTimeout(() => reject(new DaemonError("agy_init_timeout", "Antigravity init timed out", true)), 30_000))]);
170
+ }
171
+ async drain(readyResolve) { let index; while ((index = this.buffer.indexOf("\n")) >= 0) { const line = this.buffer.slice(0, index).trim(); this.buffer = this.buffer.slice(index + 1); if (!line) continue; let event; try { event = JSON.parse(line); } catch { await this.journal("invalid_json", { line: line.slice(0, 1000) }); continue; }
172
+ // Record an early provider rejection before journalling: a short-lived
173
+ // child can close while the asynchronous write is still pending.
174
+ if (!this.init && event.event === "result" && event.result?.status === "ERROR") this.startupError = new DaemonError("agy_provider_rejected", String(event.result.error ?? "Antigravity rejected the Session before initialization"), false, { provider_result: event.result });
175
+ this.observeProviderActivity(event); await this.journal("provider_event", { event }); if (event.event === "init") { this.init = { conversation_id: event.conversation_id, ...(event.init ?? {}) }; if (!this.init.conversation_id) throw new DaemonError("agy_init_missing", "Antigravity init has no conversation ID"); const requested = { provider: this.config.provider, model: this.config.model, reasoning: this.config.reasoning, mode: this.config.mode, permission_mode: "always-proceed" }; this.profile = assertProfile(requested, observedProfile(this.init, requested)); await observeModel({ journal: this.config.journal, harness: "antigravity-cli", sessionId: this.init.conversation_id, requested, observed: this.profile.observed, source: "native.init" }); await this.persist({ shutdown_state: "running", active_tree: false }); readyResolve(this.init); } else if (event.event === "step_update") this.observeStep(event.step_update ?? {}); else if (event.event === "result") { if (this.startupError) throw this.startupError; await this.finishTurn(event.result ?? {}); } } }
176
+ observeStep(step) { if (step.step_type === "tool" && ["DONE", "ERROR"].includes(step.state)) { const stepId = step.step_id ?? step.stepId ?? step.id ?? step.tool_call_id ?? step.toolCallId ?? null; const duplicate = stepId && this.completedToolSteps.has(String(stepId)); if (!duplicate) { if (stepId) this.completedToolSteps.add(String(stepId)); else this.toolIdentityComplete = false; const name = step.tool_name ?? step.tool_info?.name ?? "unknown", current = this.toolCalls.get(name) ?? { total: 0, failures: 0 }; current.total += 1; if (step.state === "ERROR" || step.tool_info?.error) current.failures += 1; this.toolCalls.set(name, current); } } for (const child of step.subagent_info?.subagents ?? []) if (child.conversation_id) { const prior = this.descendants.get(child.conversation_id); this.descendants.set(child.conversation_id, { provider_session_id: child.conversation_id, parent_provider_session_id: this.init?.conversation_id ?? null, role: child.role ?? null, subagent_type: child.type_name ?? null, log_uri: child.log_uri ?? null, workspace_uris: child.workspace_uris ?? [], status: prior?.status ?? "unknown" }); } }
177
+ toolSnapshot() { const byName = {}, failuresByName = {}; let total = 0, failures = 0; for (const [name, value] of this.toolCalls) { byName[name] = value.total; failuresByName[name] = value.failures; total += value.total; failures += value.failures; } return { total, failures, by_name: byName, failures_by_name: failuresByName, completeness: this.toolIdentityComplete ? "complete" : "partial" }; }
178
+ async finishTurn(result) {
179
+ if (result.conversation_id && result.conversation_id !== this.init?.conversation_id) {
180
+ await this.journal("foreign_terminal_observed", { conversation_id: result.conversation_id, result });
181
+ const child = this.descendants.get(result.conversation_id);
182
+ if (child) this.descendants.set(result.conversation_id, { ...child, status: result.status === "SUCCESS" ? "completed" : result.status === "ERROR" ? "failed" : "unknown" });
183
+ return;
184
+ }
185
+ this.lastResult = result; const current = this.active; this.active = null; this.markActivity();
186
+ const rootStop = this.sessionObservations.get(this.init?.conversation_id)?.stop;
187
+ await this.persist({ active_tree: result.status === "RUNNING" || rootStop?.fullyIdle === false, last_result: { status: result.status ?? null, error: result.error ?? null } });
188
+ try { this.usageIngest = await forwardUsage(this.config, result.conversation_id ?? this.init.conversation_id, result, this.toolSnapshot(), [...this.descendants.values()]); }
189
+ catch (error) { this.usageIngest = { status: "failed", error: error.message ?? String(error) }; await this.journal("usage_ingest_failed", this.usageIngest); }
190
+ if (!current) return;
191
+ if (result.status === "ERROR") {
192
+ const message = String(result.error ?? "Antigravity returned ERROR"); const normalized = message.toLowerCase();
193
+ const code = /quota|insufficient.?credit|billing|usage limit/.test(normalized) ? "agy_provider_quota_exhausted"
194
+ : /rate.?limit|too many requests|\b429\b/.test(normalized) ? "agy_provider_rate_limited"
195
+ : /auth|unauthori[sz]ed|forbidden|account/.test(normalized) ? "agy_provider_account"
196
+ : "agy_provider_failed";
197
+ return current.reject(new DaemonError(code, message, ["agy_provider_quota_exhausted", "agy_provider_rate_limited"].includes(code), { provider_result: result, usage_ingest: this.usageIngest }));
198
+ }
199
+ current.resolve(this.receipt(result));
200
+ }
201
+ receipt(result = this.lastResult) {
202
+ const assistantText = typeof result?.response === "string" ? result.response : typeof result?.text === "string" ? result.text : null;
203
+ const root = this.init?.conversation_id; const observation = this.sessionObservations.get(root);
204
+ const treeSettled = [...this.descendants.values()].every(child => ["completed", "failed", "cancelled"].includes(child.status));
205
+ return { harness: "antigravity-cli", runtime_family: "antigravity", provider_session_id: root ?? null, adapter_session_id: root ?? null, cwd: this.config.cwd, profile: this.profile, result, ...(assistantText === null ? {} : { assistant_text: assistantText }), usage: usageSnapshot(result ?? {}, this.toolSnapshot()), usage_ingest: this.usageIngest, descendants: [...this.descendants.values()], transcript_path: observation?.transcript_path ?? null, settled: Boolean(!this.active && result && result.status !== "RUNNING" && treeSettled && (this.descendants.size === 0 || observation?.stop?.fullyIdle === true)) };
206
+ }
207
+ async zeroUsage() { await forwardUsage(this.config, this.init.conversation_id, { usage: {} }, this.toolSnapshot(), []); }
208
+ async prompt(text, assertDispatch = captureDispatchGuard(this)) { if (!text) throw new DaemonError("prompt_required", "prompt is required"); await this.start(); if (this.active) throw new DaemonError("operation_in_progress", "an Antigravity turn is already running"); if (this.lastResult && !this.receipt().settled) throw new DaemonError("tree_not_settled", "the prior Antigravity agent tree is not fully settled"); const root = this.init?.conversation_id; const prior = this.sessionObservations.get(root) ?? {}; this.sessionObservations.set(root, { ...prior, execution_floor: prior.stop?.executionNum ?? prior.execution_floor ?? null, stop: null }); this.lastProviderEvent = null; await this.persist({ active_tree: true }); return await new Promise((resolve, reject) => { assertDispatch(); this.active = { resolve, reject }; this.child.stdin.write(`${JSON.stringify({ event: "user", message: { content: text } })}\n`, error => { if (error) { this.active = null; reject(error); } }); }); }
209
+ async childForHook(conversationId) {
210
+ if (conversationId === this.init?.conversation_id) return null;
211
+ const deadline = Date.now() + 1_000;
212
+ while (!this.descendants.has(conversationId) && Date.now() < deadline) await new Promise(resolve => setTimeout(resolve, 10));
213
+ const child = this.descendants.get(conversationId);
214
+ if (!child) throw new DaemonError("agy_child_identity_unconfirmed", "Antigravity hook has no confirmed native parent", true, { conversation_id: conversationId });
215
+ return child;
216
+ }
217
+ async observeHook(event, payload) { const conversationId = payload.conversationId ?? this.init?.conversation_id; const child = await this.childForHook(conversationId); const prior = this.sessionObservations.get(conversationId) ?? {}; const execution = Number.isInteger(payload.executionNum) ? payload.executionNum : null; const floor = prior.execution_floor; const oldExecution = prior.stop?.executionNum; const stale = event === "Stop" && execution !== null && ((Number.isInteger(floor) && execution <= floor) || (Number.isInteger(oldExecution) && execution < oldExecution)); const fingerprint = createHash("sha256").update(JSON.stringify({ event, payload })).digest("hex"); if (!stale && prior.last_hook !== fingerprint) this.markActivity(); if (conversationId && !stale) this.sessionObservations.set(conversationId, { ...prior, last_hook: fingerprint, ...(payload.transcriptPath ? { transcript_path: payload.transcriptPath } : {}), ...(event === "Stop" ? { stop: { fullyIdle: payload.fullyIdle === true, terminationReason: payload.terminationReason ?? null, executionNum: payload.executionNum ?? null } } : {}) }); if (child && !stale && event === "Stop" && payload.fullyIdle === true) this.descendants.set(conversationId, { ...child, status: "completed" }); const parent = child?.parent_provider_session_id ?? null;
218
+ const observed = payload.model ? observedProfile(payload) : {};
219
+ 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" });
220
+ const eventId = createHash("sha256").update(JSON.stringify({ daemon: this.config.daemonId, event, conversationId, execution: payload.executionNum ?? null, step: payload.stepIdx ?? null, tool: payload.toolCall?.name ?? null, args: payload.toolCall?.args ?? null })).digest("hex"); await this.persist(); await this.journal("hook", { stale, hook_event: event, event_id: eventId, conversation_id: conversationId, fully_idle: payload.fullyIdle ?? null }); 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 };
221
+ }
222
+ async cancel() { cancelPendingDispatch(this); if (!this.child || this.exited) return { ...this.receipt(), settled: Boolean(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.persist({ active_tree: false }); return { ...this.receipt(), cancelled: true, settled: Boolean(this.exited) };
223
+ }
224
+ async close(cancelTree) { if (cancelTree) cancelPendingDispatch(this); if (!cancelTree && (this.active || this.state.active_tree || (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(); }
225
+ async abortProvider(reason) {
226
+ const child = this.child, providerProcess = this.providerProcess;
227
+ await stopProcessGroup(child);
228
+ await finishDaemonProcess(this.config, providerProcess, "failed", reason);
229
+ }
230
+ async finishResource(state = "stopped", reason = "daemon_stopped") { await finishDaemonProcess(this.config, this.state.resource_process, state, reason); }
231
+ }
232
+
233
+ export async function serveDaemon(stateDir) {
234
+ const paths = locations(stateDir), state = await readState(paths.dir); if (!state?.config) throw new DaemonError("daemon_state_missing", "Antigravity daemon state is missing");
235
+ 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;
236
+ await new Promise((resolve, reject) => { server.once("error", reject); server.listen(paths.socket, resolve); }); await chmod(paths.socket, 0o600);
237
+ try { await runtime.start(state.config.resumeSessionId); runtime.requireSessionIdentity(state.config.resumeSessionId); }
238
+ catch (error) {
239
+ await runtime.abortProvider("daemon_start_failed");
240
+ await runtime.persist({ shutdown_state: "failed", startup_error: errorPayload(error), active_tree: false }); await runtime.finishResource("failed", "daemon_start_failed");
241
+ await new Promise(resolve => server.close(resolve));
242
+ throw error;
243
+ }
244
+ await new Promise(resolve => server.once("close", resolve));
245
+ }
246
+
247
+ async function dispatch(runtime, request) {
248
+ if (request.schema_id !== REQUEST_SCHEMA) throw new DaemonError("daemon_protocol_mismatch", "unsupported dd-agy request schema"); const params = request.params ?? {};
249
+ if (request.operation === "daemon.status") return { daemon_id: runtime.config.daemonId, shutdown_state: runtime.state.shutdown_state, active_tree: runtime.state.active_tree, last_activity_at: runtime.lastActivityAt, provider_ready: Boolean(runtime.init && !runtime.exited), pid: process.pid, provider_pid: runtime.child?.pid ?? null, versions: runtime.state.versions, config: runtime.config, sessions: runtime.state.sessions ?? [], receipt: runtime.receipt() };
250
+ if (request.operation === "session.create") { if (params.sessionId) throw new DaemonError("invalid_create", "create does not accept a Session ID"); if (params.prompt) throw new DaemonError("invalid_create", "create does not execute a prompt; use session.prompt"); await runtime.start(); await runtime.zeroUsage(); return runtime.receipt(); }
251
+ if (request.operation === "session.prompt") { const assertDispatch = captureDispatchGuard(runtime); await runtime.start(params.sessionId ?? null); runtime.requireSessionIdentity(params.sessionId); return await runtime.prompt(params.prompt, assertDispatch); }
252
+ if (request.operation === "session.inspect") { runtime.requireSessionIdentity(params.sessionId); return runtime.receipt(); }
253
+ if (request.operation === "session.resume") { await runtime.start(params.sessionId ?? runtime.init?.conversation_id ?? null); runtime.requireSessionIdentity(params.sessionId); return runtime.receipt(); }
254
+ if (request.operation === "session.cancel") { runtime.requireSessionIdentity(params.sessionId); return await runtime.cancel(); }
255
+ if (request.operation === "hook.observe") return await runtime.observeHook(params.event, params.payload ?? {});
256
+ if (request.operation === "daemon.stop") { await runtime.close(params.cancelTree === true); await runtime.persist({ shutdown_state: "clean", active_tree: false }); await runtime.finishResource(); setImmediate(() => runtime.server?.close()); return { stopped: true, clean: true, settled: true, daemon_id: runtime.config.daemonId }; }
257
+ throw new DaemonError("unknown_operation", `unknown dd-agy daemon operation: ${request.operation}`);
258
+ }
259
+ import { authorizeRetainedDaemonResume } from "./driver-recovery.mjs";
@@ -0,0 +1,59 @@
1
+ import { spawn } from "node:child_process";
2
+ import { mkdtemp, rm } from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+
6
+ import { checkObservedProfile } from "./model-observations.mjs";
7
+
8
+ const DEFAULT_MODEL = "gemini-3.1-pro-high";
9
+
10
+ export class AgyError extends Error {
11
+ constructor(code, message, retryable = false, details) { super(message); this.code = code; this.retryable = retryable; this.details = details; }
12
+ }
13
+
14
+ export function executable(bin, args = []) {
15
+ return /\.[cm]?js$/.test(bin) ? { command: process.execPath, args: [bin, ...args] } : { command: bin, args };
16
+ }
17
+
18
+ export async function runAgy(bin, args, options = {}) {
19
+ const target = executable(bin, args);
20
+ return await new Promise((resolve, reject) => {
21
+ const child = spawn(target.command, target.args, { cwd: options.cwd, env: options.env ?? process.env, stdio: ["pipe", "pipe", "pipe"] });
22
+ let stdout = "", stderr = "", settled = false;
23
+ const timer = setTimeout(() => { child.kill("SIGTERM"); finish(new AgyError("agy_timeout", `${args[0] ?? "agy"} timed out`, true)); }, options.timeoutMs ?? 30_000);
24
+ const finish = (error, value) => { if (settled) return; settled = true; clearTimeout(timer); error ? reject(error) : resolve(value); };
25
+ child.stdout.setEncoding("utf8").on("data", chunk => stdout += chunk);
26
+ child.stderr.setEncoding("utf8").on("data", chunk => stderr += chunk);
27
+ child.on("error", error => finish(error));
28
+ child.on("close", code => code === 0 ? finish(null, { stdout, stderr, code }) : finish(new AgyError("agy_cli_failed", stderr.trim() || stdout.trim() || `agy exited ${code}`, false, { exit_code: code })));
29
+ child.stdin.end(options.stdin ?? "");
30
+ });
31
+ }
32
+
33
+ export function usageSnapshot(result = {}, toolCalls = {}) {
34
+ const usage = result.usage && typeof result.usage === "object" ? result.usage : {};
35
+ const numberOrNull = value => value == null || !Number.isFinite(Number(value)) ? null : Number(value);
36
+ return {
37
+ // A missing counter is unknown, not a measured zero.
38
+ input_tokens: numberOrNull(usage.input_tokens), output_tokens: numberOrNull(usage.output_tokens),
39
+ reasoning_tokens: numberOrNull(usage.thinking_tokens), cache_read_tokens: numberOrNull(usage.cache_read_tokens),
40
+ total_tokens: numberOrNull(usage.total_tokens), tool_calls: toolCalls
41
+ };
42
+ }
43
+
44
+ export function observedProfile(init = {}, requested = {}) {
45
+ return { provider: init.provider ?? null, model: init.model ?? null, reasoning: init.reasoning ?? null, mode: init.mode ?? null, permission_mode: init.permission_mode ?? null };
46
+ }
47
+
48
+ export function assertProfile(requested, observed) { return checkObservedProfile(requested, observed, { required: requested.permission_mode ? ["permission_mode"] : [] }); }
49
+
50
+ export async function doctor(options = {}) {
51
+ const bin = options.bin ?? process.env.DD_AGY_BIN ?? "agy", temporary = await mkdtemp(path.join(os.tmpdir(), "dd-agy-doctor-"));
52
+ try {
53
+ const version = (await runAgy(bin, ["--version"], { timeoutMs: 10_000 })).stdout.trim();
54
+ const models = await runAgy(bin, [`--gemini_dir=${temporary}`, "--app_data_dir=runtime", "models"], { timeoutMs: options.timeoutMs ?? 30_000 });
55
+ const modelIds = models.stdout.split(/\r?\n/).map(line => line.split(/\s+/)[0]).filter(Boolean);
56
+ if (!modelIds.includes(options.model ?? DEFAULT_MODEL)) throw new AgyError("agy_model_unavailable", `required Antigravity model is unavailable: ${options.model ?? DEFAULT_MODEL}`);
57
+ return { compatible: true, observed_runtime: { agy: version, dd_harness_contract: "dd-agy-harness@1" }, isolation: { gemini_dir: "verified", app_data_dir: "verified" }, auth: "available", models: modelIds };
58
+ } finally { await rm(temporary, { recursive: true, force: true }); }
59
+ }
@@ -0,0 +1,93 @@
1
+ import { captureDispatchGuard, cancelPendingDispatch } from "./dispatch-fence.mjs";
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import { chmod, lstat, mkdir, open, readFile, rename, unlink, writeFile } from "node:fs/promises";
4
+ import net from "node:net";
5
+ import { durableDaemonDispatch } from "./daemon-operations.mjs";
6
+ import { observedTimeout } from "./observation-clock.mjs";
7
+ import path from "node:path";
8
+ import { spawn } from "node:child_process";
9
+ import { waitForSettlement } from "./session-settlement.mjs";
10
+ import { errorRecord } from "./operation-errors.mjs";
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";
13
+
14
+ const requestSchema = "dd-codex/daemon-request@1";
15
+ const responseSchema = "dd-codex/daemon-response@1";
16
+ const stateSchema = "dd-codex/daemon-state@1";
17
+ const absolute = (value, label) => { if (!value || !path.isAbsolute(value)) throw Object.assign(new Error(`${label} must be an absolute path`), { code: "invalid_path" }); return path.resolve(value); };
18
+ function locations(stateDir) { const dir = absolute(stateDir, "--state-dir"); const local = path.join(dir, "daemon.sock"); return { dir, socket: Buffer.byteLength(local) < 100 ? local : `/tmp/dd-codex-${createHash("sha256").update(dir).digest("hex").slice(0, 24)}.sock`, state: path.join(dir, "daemon.json"), log: path.join(dir, "daemon.log") }; }
19
+ async function readState(file) { try { return JSON.parse(await readFile(file, "utf8")); } catch (error) { if (error.code === "ENOENT") return null; throw error; } }
20
+ async function writeState(file, value) { const temp = `${file}.${process.pid}.tmp`; await writeFile(temp, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 }); await rename(temp, file); }
21
+ async function removeSocket(socket) { try { const info = await lstat(socket); if (!info.isSocket()) throw Object.assign(new Error("refusing to remove non-socket daemon path"), { code: "unsafe_socket" }); await unlink(socket); } catch (error) { if (error.code !== "ENOENT") throw error; } }
22
+ const errorPayload = errorRecord;
23
+
24
+ export async function callDaemon(stateDir, operation, params = {}, timeoutMs = 30_000) {
25
+ const target = locations(stateDir); const request = { schema_id: requestSchema, id: process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation, params };
26
+ return await new Promise((resolve, reject) => {
27
+ const client = net.createConnection(target.socket); let buffer = ""; let settled = false; const timer = operation === "session.prompt" ? null : observedTimeout(() => finish(Object.assign(new Error(`${operation} timed out`), { code: "daemon_timeout", retryable: true })), timeoutMs);
28
+ const finish = (error, value) => { if (settled) return; settled = true; clearTimeout(timer); client.destroy(); error ? reject(error) : resolve(value); };
29
+ client.setEncoding("utf8"); client.on("connect", () => client.write(`${JSON.stringify(request)}\n`)); client.on("error", (error) => finish(Object.assign(error, { code: "daemon_not_running", retryable: true })));
30
+ client.on("end", () => finish(Object.assign(new Error("daemon closed the request without a response"), { code: "daemon_connection_closed", retryable: true })));
31
+ client.on("close", () => finish(Object.assign(new Error("daemon connection closed without a response"), { code: "daemon_connection_closed", retryable: true })));
32
+ client.on("data", (chunk) => { buffer += chunk; const at = buffer.indexOf("\n"); if (at < 0) return; try { const response = JSON.parse(buffer.slice(0, at)); if (response.schema_id !== responseSchema || response.id !== request.id) throw new Error("invalid daemon response"); response.ok ? finish(null, response.result) : finish(Object.assign(new Error(response.error?.message ?? "daemon error"), response.error)); } catch (error) { finish(Object.assign(error, { code: "daemon_protocol" })); } });
33
+ });
34
+ }
35
+
36
+ export async function startDaemon(options) {
37
+ 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") } : {} };
38
+ await mkdir(paths.dir, { recursive: true, mode: 0o700 }); await chmod(paths.dir, 0o700);
39
+ 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; }
40
+ 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" });
41
+ if (previous?.shutdown_state === "clean") await authorizeRetainedDaemonResume(paths.dir, previous, options.sessionId, config);
42
+ 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);
43
+ 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();
44
+ try { await confirmDaemonProcess(config, resourceProcess, child); const deadline = Date.now() + 15_000; while (Date.now() < deadline) { await new Promise((resolve) => setTimeout(resolve, 100)); try { return await callDaemon(paths.dir, "daemon.status", {}, 1_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" }); }
45
+ catch (error) { await cleanupFailedStart(config, resourceProcess, child, error); throw error; }
46
+ }
47
+ 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); }
48
+
49
+ export async function serveDaemon(stateDir) {
50
+ 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" });
51
+ const bridge = new CodexBridge({ ...state.config, env: { ...state.config.env, DD_FLOW_DAEMON_ID: state.daemon_id } }); await bridge.start(); const sessions = new Set(state.sessions ?? []); let active = null; let server; let persist = Promise.resolve();
52
+ 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)).then(() => heartbeatDaemonProcess(snapshot.config, snapshot.resource_process).catch(() => {})); return persist; };
53
+ const dispatch = async (operation, params) => {
54
+ const assertDispatch = captureDispatchGuard(bridge);
55
+ if (operation === "daemon.status") return { daemon_id: state.daemon_id, pid: process.pid, cwd: state.cwd, journal: state.journal, config: state.config, sessions: [...sessions], active_operation: active, shutdown_state: state.shutdown_state };
56
+ // The runner may need to retire an orphaned daemon after its caller was
57
+ // interrupted. `daemon.stop` is the one control operation that remains
58
+ // available while a request is marked active; callers must first prove the
59
+ // provider Session itself is idle.
60
+ if (operation === "daemon.stop") {
61
+ if (params.cancelTree) cancelPendingDispatch(bridge);
62
+ if (active && !params.cancelTree) throw Object.assign(new Error("productive operation is running"), { code: "tree_not_settled" });
63
+ await waitForSettlement({
64
+ knownSessions: () => [...sessions],
65
+ observe: async () => {
66
+ const unsettled = [];
67
+ // Set iteration also visits descendants discovered during this pass.
68
+ for (const sessionId of sessions) {
69
+ const observed = await inspectSessionWithBridge(bridge, { ...state.config, sessionId });
70
+ for (const child of observed.descendants ?? []) sessions.add(child.provider_session_id);
71
+ const status = observed.status?.type ?? observed.status;
72
+ const stored = status === "notLoaded" ? await bridge.request("thread/turns/list", { threadId: sessionId, limit: 1, sortDirection: "desc", itemsView: "summary" }, 5_000) : null;
73
+ const terminalUnloaded = status === "notLoaded" && Array.isArray(stored?.data) && (stored.data.length === 0 || ["completed", "interrupted", "failed", "cancelled"].includes(stored.data[0]?.status));
74
+ if (status !== "idle" && !terminalUnloaded) unsettled.push(sessionId);
75
+ }
76
+ return { sessions: unsettled.reverse(), active: Boolean(active), active_operation: active };
77
+ },
78
+ ...(params.cancelTree ? { cancel: async sessionId => { try { await cancelSessionWithBridge(bridge, { ...state.config, sessionId }); } catch (error) { if (error.code !== "turn_not_active") throw error; } } } : {})
79
+ });
80
+ return { stopped: true, clean: true, _shutdown: true };
81
+ }
82
+ if (active && !["session.inspect", "session.cancel"].includes(operation)) throw Object.assign(new Error(`${active} is in progress`), { code: "operation_busy", retryable: true });
83
+ if (operation === "session.create") { active = operation; await save({ active_operation: active }); try { const result = await createSessionWithBridge(bridge, { ...state.config, ...params }); sessions.add(result.provider_session_id); return result; } finally { active = null; await save({ active_operation: null }); } }
84
+ if (operation === "session.prompt") { sessions.add(params.sessionId); active = operation; await save({ active_operation: active }); try { return await promptSessionWithBridge(bridge, { ...state.config, ...params, assertDispatch }); } finally { active = null; await save({ active_operation: null }); } }
85
+ if (operation === "session.start") { sessions.add(params.sessionId); active = operation; await save({ active_operation: active }); try { return await startSessionWithBridge(bridge, { ...state.config, ...params, assertDispatch }); } finally { active = null; await save({ active_operation: null }); } }
86
+ if (operation === "session.inspect") return await inspectSessionWithBridge(bridge, { ...state.config, ...params });
87
+ if (operation === "session.cancel") { if (!sessions.has(params.sessionId)) throw Object.assign(new Error("cancellation Session is not owned by this daemon"), { code: "session_identity_mismatch" }); cancelPendingDispatch(bridge); return await cancelSessionWithBridge(bridge, { ...state.config, ...params }); }
88
+ throw Object.assign(new Error(`unknown operation: ${operation}`), { code: "unknown_operation" });
89
+ };
90
+ 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`); } })(); }); });
91
+ 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(() => {});
92
+ }
93
+ import { authorizeRetainedDaemonResume } from "./driver-recovery.mjs";