@deksden-com/dd-flow-cli 0.9.0-beta.122 → 0.9.0-beta.124

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +23 -0
  3. package/dist/build-info.json +4 -4
  4. package/dist/cli/run-cli.js +3 -0
  5. package/dist/domain/stage-catalog.js +1 -1
  6. package/dist/harness-runtime/lib/codex-capacity-policy.d.mts +10 -0
  7. package/dist/harness-runtime/lib/codex-capacity-policy.mjs +89 -0
  8. package/dist/harness-runtime/lib/daemon-operations.mjs +100 -3
  9. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +117 -21
  10. package/dist/harness-runtime/lib/dd-agy.mjs +13 -3
  11. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +58 -13
  12. package/dist/harness-runtime/lib/dd-codex.mjs +16 -26
  13. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +19 -12
  14. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +42 -10
  15. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +57 -14
  16. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +25 -23
  17. package/dist/harness-runtime/lib/dd-zcode.mjs +24 -14
  18. package/dist/harness-runtime/lib/driver-recovery.mjs +12 -2
  19. package/dist/harness-runtime/lib/managed-daemon.d.mts +2 -0
  20. package/dist/harness-runtime/lib/managed-daemon.mjs +100 -31
  21. package/dist/harness-runtime/lib/model-observations.mjs +16 -9
  22. package/dist/harness-runtime/lib/tool-observations.d.mts +1 -0
  23. package/dist/harness-runtime/lib/tool-observations.mjs +52 -11
  24. package/dist/schemas/flow-run.schema.json +1 -0
  25. package/dist/schemas/plan-review-decision.schema.json +3 -3
  26. package/dist/services/code-checks.js +14 -18
  27. package/dist/services/codex-capacity.js +251 -31
  28. package/dist/services/controller-fanout.js +1 -0
  29. package/dist/services/eval-snapshots.js +13 -8
  30. package/dist/services/external-work-launch.js +36 -11
  31. package/dist/services/harness-adapter.js +21 -15
  32. package/dist/services/managed-processes.js +14 -6
  33. package/dist/services/merge-server.js +35 -12
  34. package/dist/services/review-copy.js +21 -0
  35. package/dist/services/review-deferral.js +23 -0
  36. package/dist/services/run-controller-adapter.js +33 -8
  37. package/dist/services/run-controller-recovery.js +26 -2
  38. package/dist/services/run-controller.js +91 -24
  39. package/dist/services/run-observations.js +7 -2
  40. package/dist/services/run-recovery-runtime.js +49 -3
  41. package/dist/services/runs.js +1 -1
  42. package/dist/services/stage-report-renderer.js +25 -2
  43. package/dist/services/verification-matrix.js +57 -8
  44. package/dist/services/vnext-code-review.js +22 -8
  45. package/dist/services/vnext-merge.js +20 -2
  46. package/dist/services/vnext-plan-review.js +87 -19
  47. package/dist/services/work-registry.js +11 -2
  48. package/package.json +1 -1
@@ -1,5 +1,7 @@
1
1
  import { captureDispatchGuard, cancelPendingDispatch } from "./dispatch-fence.mjs";
2
- import { ToolObservations, agyToolObservation } from "./tool-observations.mjs";
2
+ import { ToolObservations, agyToolObservation, agyTranscriptObservations } from "./tool-observations.mjs";
3
+ import { constants } from "node:fs";
4
+ import { fileURLToPath } from "node:url";
3
5
  import { createHash, randomUUID } from "node:crypto";
4
6
  import { appendFile, chmod, lstat, mkdir, open, readFile, realpath, rename, unlink, writeFile } from "node:fs/promises";
5
7
  import { spawn } from "node:child_process";
@@ -8,7 +10,7 @@ import { isDeepStrictEqual } from "node:util";
8
10
  import { ObservationClock } from "./observation-clock.mjs";
9
11
  import { hookBudget } from "./native-hook-command.mjs";
10
12
  import { agyWorkspaceHooks } from "./agy-hooks.mjs";
11
- import { durableDaemonDispatch } from "./daemon-operations.mjs";
13
+ import { durableDaemonDispatch, completeDaemonShutdown, assertDaemonDispatch, assertDaemonRestartable, trackDaemonConnections, retireDaemonEndpoint, retireFailedDaemon } from "./daemon-operations.mjs";
12
14
  import path from "node:path";
13
15
  import { observeModel } from "./model-observations.mjs";
14
16
  import { AGY_VERSION_TIMEOUT_MS, AgyError, assertProfile, executable, observedProfile, runAgy, usageSnapshot } from "./dd-agy.mjs";
@@ -16,6 +18,29 @@ import { prepareRuntimeOwner, cleanupFailedStart, cleanupFailedDaemonStart, conf
16
18
 
17
19
  const REQUEST_SCHEMA = "dd-agy/daemon-request@1", RESPONSE_SCHEMA = "dd-agy/daemon-response@1", STATE_SCHEMA = "dd-agy/daemon-state@1";
18
20
  class DaemonError extends AgyError {}
21
+ export async function captureAgyTranscript(runtimeRoot, child, rootSessionId, ownedSessionIds = [rootSessionId]) {
22
+ const session = { harness_id: "antigravity-cli", session_id: child.provider_session_id };
23
+ let handle;
24
+ try {
25
+ if (!rootSessionId || !child.provider_session_id || !ownedSessionIds.includes(child.parent_provider_session_id) || !child.log_uri || !/^[\w-]+$/.test(child.provider_session_id)) throw new Error("unbound native locator");
26
+ const expected = path.join(runtimeRoot, "brain", child.provider_session_id, ".system_generated", "logs", "transcript.jsonl");
27
+ const canonical = path.join(await realpath(runtimeRoot), "brain", child.provider_session_id, ".system_generated", "logs", "transcript.jsonl");
28
+ if (![expected, canonical].includes(fileURLToPath(child.log_uri)) || await realpath(expected) !== canonical) throw new Error("foreign or symlink native locator");
29
+ const before = await lstat(expected);
30
+ if (!before.isFile()) throw new Error("native locator is not a regular file");
31
+ handle = await open(expected, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
32
+ const stat = await handle.stat();
33
+ if (!before.isFile() || !stat.isFile() || stat.dev !== before.dev || stat.ino !== before.ino || await realpath(expected) !== canonical) throw new Error("native locator changed during open");
34
+ const maximum = 8 * 1024 * 1024, buffer = Buffer.alloc(Math.min(stat.size, maximum));
35
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
36
+ const bytes = buffer.subarray(0, bytesRead), prefix = bytes.subarray(0, bytes.lastIndexOf(10) + 1);
37
+ const source = { locator: child.log_uri, byte_start: 0, byte_end: prefix.length, sha256: createHash("sha256").update(prefix).digest("hex") };
38
+ const events = agyTranscriptObservations(prefix.toString("utf8"), child.provider_session_id, child.parent_provider_session_id ?? rootSessionId, source);
39
+ if (prefix.length !== bytesRead || stat.size > maximum) events.push({ session, reason: stat.size > maximum ? "agy_transcript_size_limit" : "agy_transcript_partial_tail" });
40
+ return events;
41
+ } catch (error) { return [{ session, reason: error.code === "ENOENT" ? "agy_transcript_missing" : "agy_transcript_locator_rejected" }]; }
42
+ finally { await handle?.close(); }
43
+ }
19
44
  const absolute = (value, label) => { if (!value || !path.isAbsolute(value)) throw new DaemonError("invalid_path", `${label} must be an absolute path`); return path.resolve(value); };
20
45
  function locations(stateDir) {
21
46
  const dir = absolute(stateDir, "--state-dir"), digest = createHash("sha256").update(dir).digest("hex").slice(0, 22), local = path.join(dir, "daemon.sock");
@@ -185,14 +210,16 @@ export async function startDaemon(options) {
185
210
  const paths = locations(options.stateDir); await mkdir(paths.dir, { recursive: true, mode: 0o700 }); await chmod(paths.dir, 0o700);
186
211
  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 };
187
212
  await prepareRuntimeOwner(config, paths.dir);
213
+ assertDaemonRestartable(previous);
188
214
  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; }
215
+ assertDaemonRestartable(await readState(paths.dir));
189
216
  if (previous?.shutdown_state === "clean") await authorizeRetainedDaemonResume(paths.dir, previous, options.sessionId, config);
190
217
  if (previous?.shutdown_state === "running" && previous.active_tree) throw new DaemonError("invalid_harness_crash", "previous Antigravity daemon died with an active or unproven tree");
191
218
  await removeSocket(paths); await prepare(paths, config);
192
219
  const version = (await runAgy(config.bin, ["--version"], { cwd: config.cwd, env: sanitizedEnv(config), timeoutMs: AGY_VERSION_TIMEOUT_MS })).stdout.trim();
193
220
  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 });
194
221
  const retained = retainedAgyState(previous, config.resumeSessionId);
195
- await writeJson(paths.state, { schema_id: STATE_SCHEMA, daemon_id: config.daemonId, shutdown_state: "starting", active_tree: false, unclaimed_activity: retained.unclaimed_activity ?? false, retained_tree_settlement: retained.retained_tree_settlement ?? null, versions: { agy: version }, config, sessions: [], session_observations: retained.session_observations ?? [], descendants: retained.descendants ?? [], tool_calls: retained.tool_calls ?? [], completed_tool_steps: retained.completed_tool_steps ?? [], tool_identity_complete: retained.tool_identity_complete !== false, turn_generation: retained.turn_generation ?? 0, last_result: retained.last_result ?? null, resource_process: resourceProcess });
222
+ await writeJson(paths.state, { schema_id: STATE_SCHEMA, daemon_id: config.daemonId, shutdown_state: "starting", active_tree: false, unclaimed_activity: retained.unclaimed_activity ?? false, retained_tree_settlement: retained.retained_tree_settlement ?? null, versions: { agy: version }, config, sessions: [], session_observations: retained.session_observations ?? [], descendants: retained.descendants ?? [], ...(Array.isArray(retained.tool_observations) ? { tool_observations: retained.tool_observations } : {}), tool_observation_gaps: retained.tool_observation_gaps ?? [], tool_calls: retained.tool_calls ?? [], completed_tool_steps: retained.completed_tool_steps ?? [], tool_identity_complete: retained.tool_identity_complete !== false, turn_generation: retained.turn_generation ?? 0, last_result: retained.last_result ?? null, resource_process: resourceProcess });
196
223
  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();
197
224
  try {
198
225
  await confirmDaemonProcess(config, resourceProcess, child);
@@ -210,11 +237,11 @@ export async function startDaemon(options) {
210
237
  throw error;
211
238
  }
212
239
  }
213
- 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); };
240
+ export const stopDaemon = async options => { const timeoutMs = options.timeoutMs ?? 30_000; return await confirmDaemonStopped(() => callDaemon(options.stateDir, "daemon.stop", { cancelTree: options.cancelTree === true }, timeoutMs), locations(options.stateDir).socket, timeoutMs, { readState: () => readState(options.stateDir) }); };
214
241
 
215
242
  export class Runtime {
216
243
  constructor(paths, state) { this.paths = paths; this.state = state; this.config = state.config; this.child = null; this.providerProcess = state.provider_process ?? null; this.providerFinalization = Promise.resolve(); this.buffer = ""; this.init = null; this.readyPromise = null; this.active = null; this.exited = false; this.toolObservations = new ToolObservations(); for (const reason of state.tool_observation_gaps ?? []) this.toolObservations.gaps.add(reason); for (const event of state.tool_observations ?? []) this.toolObservations.add(event); if (state.tool_calls?.length && !state.tool_observations) this.toolObservations.gaps.add("legacy_tools_not_attributed"); this.turnGeneration = state.turn_generation ?? 0; this.descendants = new Map((state.descendants ?? []).map(child => [child.provider_session_id, child])); this.lastResult = state.last_result ?? 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; }
217
- persist(patch = {}) { Object.assign(this.state, patch, { pid: process.pid, updated_at: new Date().toISOString(), last_activity_at: this.lastActivityAt, session_observations: [...this.sessionObservations], tool_observations: [...this.toolObservations.calls.values()], tool_observation_gaps: [...this.toolObservations.gaps], turn_generation: this.turnGeneration, 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); const queued = performance.now(); this.persisting = this.persisting.then(async () => { const started = performance.now(); await writeJson(this.paths.state, snapshot); this.persistTiming = { queue_ms: Math.round(started - queued), write_ms: Math.round(performance.now() - started) }; }); return this.persisting; }
244
+ persist(patch = {}) { Object.assign(this.state, patch, { pid: process.pid, updated_at: new Date().toISOString(), last_activity_at: this.lastActivityAt, session_observations: [...this.sessionObservations], tool_observations: [...this.toolObservations.calls.values(), ...this.toolObservations.sources.values()], tool_observation_gaps: [...this.toolObservations.gaps], turn_generation: this.turnGeneration, 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); const queued = performance.now(); this.persisting = this.persisting.catch(() => {}).then(async () => { const started = performance.now(); await writeJson(this.paths.state, snapshot); this.persistTiming = { queue_ms: Math.round(started - queued), write_ms: Math.round(performance.now() - started) }; }); return this.persisting; }
218
245
  async journal(kind, payload = {}) { const line = `${JSON.stringify({ schema_id: "dd-agy/daemon-event@1", event_id: randomUUID(), observed_at: new Date().toISOString(), kind, ...payload })}\n`; for (const journal of new Set([this.paths.journal, this.config.journal].filter(Boolean))) { await mkdir(path.dirname(journal), { recursive: true, mode: 0o700 }); await appendFile(journal, line, { mode: 0o600 }); } }
219
246
  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"])]; }
220
247
  requireSessionIdentity(requestedId) {
@@ -232,12 +259,16 @@ export class Runtime {
232
259
  if (["init", "step_update", "result"].includes(event.event)) this.markActivity();
233
260
  }
234
261
  async start(sessionId = null) {
262
+ if (this.drainingError) throw this.drainingError;
263
+ assertDaemonDispatch(this.paths?.dir);
235
264
  if (this.child && !this.exited) {
236
265
  await this.readyPromise;
237
266
  this.requireSessionIdentity(sessionId);
238
267
  return this.init;
239
268
  }
240
269
  await this.providerFinalization;
270
+ if (this.drainingError) throw this.drainingError;
271
+ assertDaemonDispatch(this.paths?.dir);
241
272
  const target = executable(this.config.bin, this.args(sessionId)); this.exited = false; this.buffer = ""; this.init = null;
242
273
  let readyResolve, readyReject; this.readyPromise = new Promise((resolve, reject) => { readyResolve = resolve; readyReject = reject; });
243
274
  // The provider can reject before durable registration finishes. Keep a
@@ -248,12 +279,12 @@ export class Runtime {
248
279
  const providerProcess = this.providerProcess;
249
280
  try { await this.persist({ provider_process: providerProcess }); }
250
281
  catch (error) { await cleanupFailedStart(this.config, providerProcess, null, error); throw error; }
251
- try { this.child = spawn(target.command, target.args, { cwd: this.config.cwd, env: sanitizedEnv(this.config), detached: true, stdio: ["pipe", "pipe", "pipe"] }); }
282
+ try { assertDaemonDispatch(this.paths?.dir); this.child = spawn(target.command, target.args, { cwd: this.config.cwd, env: sanitizedEnv(this.config), detached: true, stdio: ["pipe", "pipe", "pipe"] }); }
252
283
  catch (error) { await cleanupFailedStart(this.config, providerProcess, null, error); throw error; }
253
284
  const providerChild = this.child;
254
285
  let allowProviderFinalization;
255
286
  const providerRegistered = new Promise(resolve => { allowProviderFinalization = resolve; });
256
- 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; } }); });
287
+ this.child.stdout.setEncoding("utf8").on("data", chunk => { this.buffer += chunk; this.draining = this.draining.then(() => this.drain(readyResolve)).catch(error => { this.drainingError ??= error; readyReject(error); if (this.active) { this.active.reject(error); this.active = null; } }); });
257
288
  this.child.stderr.setEncoding("utf8").on("data", chunk => void this.journal("stderr", { text: String(chunk).slice(0, 4000) }));
258
289
  this.child.on("error", error => { readyReject(error); if (this.active) { this.active.reject(error); this.active = null; } });
259
290
  // A dead leader is not yet a finalized tree: stop must also join the
@@ -295,6 +326,26 @@ export class Runtime {
295
326
  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 ?? {}); } } }
296
327
  observeStep(step) { const sessionId = step.conversation_id ?? this.init?.conversation_id; const observation = this.sessionObservations.get(sessionId) ?? {}; if (Number.isInteger(step.step_index) && step.step_index > (observation.last_step_index ?? -1)) this.childActivity(sessionId); if (Number.isInteger(step.step_index)) this.sessionObservations.set(sessionId, { ...observation, last_step_index: Math.max(observation.last_step_index ?? -1, step.step_index) }); this.toolObservations.add(agyToolObservation(step, this.init?.conversation_id)); 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, { ...prior, provider_session_id: child.conversation_id, parent_provider_session_id: sessionId ?? 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" }); } }
297
328
  toolSnapshot() { const summary = this.toolObservations.summary(); const failures_by_name = {}; for (const event of this.toolObservations.calls.values()) if (event.status === "failed") failures_by_name[event.name] = (failures_by_name[event.name] ?? 0) + 1; return { ...summary, by_name: summary.by_tool, failures_by_name }; }
329
+ async captureChildTools() {
330
+ this.capturedToolFacts ??= new Map();
331
+ const root = this.init?.conversation_id ?? this.state.sessions?.[0]?.provider_session_id ?? this.lastResult?.conversation_id;
332
+ for (const child of this.descendants.values()) {
333
+ const ancestors = new Set(); let parent = child.parent_provider_session_id;
334
+ while (parent && parent !== root && this.descendants.has(parent) && !ancestors.has(parent)) {
335
+ ancestors.add(parent); parent = this.descendants.get(parent).parent_provider_session_id;
336
+ }
337
+ const events = parent === root && root
338
+ ? await captureAgyTranscript(this.paths.runtime, child, root, [root, ...ancestors])
339
+ : [{ session: { harness_id: "antigravity-cli", session_id: child.provider_session_id }, reason: "agy_transcript_ancestry_unproven" }];
340
+ for (const event of events) {
341
+ this.toolObservations.add(event);
342
+ const key = JSON.stringify([event.session, event.call_id, event.reason]), encoded = JSON.stringify(event);
343
+ if (this.capturedToolFacts.get(key) === encoded) continue;
344
+ await this.journal("tool_observation", { schema_id: "dd-flow/tool-observation@1", event });
345
+ this.capturedToolFacts.set(key, encoded);
346
+ }
347
+ }
348
+ }
298
349
  async finishTurn(result) {
299
350
  if (result.status === "RUNNING") { await this.persist({ active_tree: true }); return; }
300
351
  if (!["SUCCESS", "ERROR", "CANCELLED"].includes(result.status)) throw new DaemonError("agy_terminal_result_invalid", "Antigravity result has no recognized terminal status", false, { provider_result: result });
@@ -312,6 +363,7 @@ export class Runtime {
312
363
  // are not request IDs: native command rejections can report zero turns.
313
364
  // prompt() drains earlier events before accepting another input.
314
365
  if (!current && this.lastResult) { await this.journal("stale_terminal_observed", { result }); return; }
366
+ await this.captureChildTools();
315
367
  this.lastResult = result; this.markActivity();
316
368
  const rootStop = this.sessionObservations.get(this.init?.conversation_id)?.stop;
317
369
  const pendingTerminal = current && result.status === "SUCCESS";
@@ -380,6 +432,7 @@ export class Runtime {
380
432
  try {
381
433
  await this.persist({ active_tree: true, last_hook_rejection: null });
382
434
  assertDispatch();
435
+ assertDaemonDispatch(this.paths?.dir);
383
436
  if (this.active !== current) throw new DaemonError("operation_cancelled", "prompt reservation was superseded");
384
437
  this.child.stdin.write(`${JSON.stringify({ event: "user", message: { content: text } })}\n`, error => {
385
438
  if (error) { if (this.active === current) this.active = null; current.reject(error); }
@@ -438,37 +491,79 @@ export class Runtime {
438
491
  }
439
492
  async observeHook(event, payload, budget = null) { const conversationId = payload.conversationId ?? this.init?.conversation_id; const child = await this.childForHook(conversationId); budget?.remaining("identity"); const prior = this.sessionObservations.get(conversationId) ?? {}; const execution = Number.isInteger(payload.executionNum) ? payload.executionNum : null; const step = Number.isInteger(payload.stepIdx) ? payload.stepIdx : null; const floor = prior.step_floor; const stale = step !== null && ((Number.isInteger(floor) && step <= floor) || (child && Number.isInteger(prior.last_step_index) && step < prior.last_step_index)); if (stale) { await this.journal("stale_hook", { hook_event: event, conversation_id: conversationId, step_index: step }); return { stale: true }; } 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, ...(step !== null ? { last_step_index: Math.max(prior.last_step_index ?? -1, step) } : {}), last_hook: fingerprint, ...(event === "PreToolUse" ? { last_pre_hook: fingerprint } : {}), ...(payload.transcriptPath ? { transcript_path: payload.transcriptPath } : {}), ...(event === "Stop" ? { stop: { fullyIdle: payload.fullyIdle === true, terminationReason: payload.terminationReason ?? null, executionNum: payload.executionNum ?? null } } : {}) }); if (!stale && event === "PreToolUse" && prior.last_pre_hook !== fingerprint) this.childActivity(conversationId); if (child && !stale && event === "Stop" && payload.fullyIdle === true) this.descendants.set(conversationId, { ...this.descendants.get(conversationId), tree_settled: true, settlement_evidence: "child_fully_idle_stop" }); const terminalPending = this.active?.terminalResult === this.lastResult; if (!child && !stale && event === "Stop" && payload.fullyIdle === true && (!this.active || terminalPending) && ["SUCCESS", "ERROR", "CANCELLED"].includes(this.lastResult?.status)) for (const [id, descendant] of this.descendants) if (descendant.status === "unknown") this.descendants.set(id, { ...descendant, status: "settled_by_root", settlement_evidence: "root_fully_idle_stop" }); const parent = child?.parent_provider_session_id ?? null;
440
493
  const observed = payload.model ? observedProfile(payload) : {};
441
- 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" });
494
+ await observeModel({ journal: this.config.journal, harness: "antigravity-cli", sessionId: conversationId, parentSessionId: parent, requested: this.config, observed, source: { channel: "native.hook", scope: "configured", ...(!payload.model ? { non_asserting: true } : {}) }, evidence: payload.model ? "configured" : "unavailable", reason: payload.model ? null : "hook_omits_current_model" });
442
495
  const eventId = createHash("sha256").update(JSON.stringify({ daemon: this.config.daemonId, event, conversationId, 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(); 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, persistence_timing: this.persistTiming ?? null }); await this.completePendingTurn(); return { event_id: eventId, daemon_id: this.config.daemonId, launch_cwd: this.config.cwd, 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 };
443
496
  }
444
497
  async finalizeProviderForControl() {
445
498
  const pending = this.providerFinalization;
446
- try { await pending; }
447
- catch (error) {
448
- // A leaderless group may have exited naturally since the last check.
449
- // Only control may recheck it; startup never retries failed cleanup.
450
- if (this.providerFinalization !== pending) return await this.providerFinalization;
451
- if (error.code !== "process_group_ownership_unknown" || !this.retryProviderFinalization) throw error;
452
- this.providerFinalization = this.retryProviderFinalization(5_000);
453
- await this.providerFinalization;
499
+ try {
500
+ try { await pending; }
501
+ catch (error) {
502
+ // Control retries only retained physical/resource cleanup, never a
503
+ // missing mandatory stream record or a productive provider request.
504
+ if (this.providerFinalization !== pending) await this.providerFinalization;
505
+ else {
506
+ if (!this.retryProviderFinalization) throw error;
507
+ this.providerFinalization = this.retryProviderFinalization(5_000);
508
+ await this.providerFinalization;
509
+ }
510
+ }
511
+ } catch (error) {
512
+ if (!this.drainingError) throw error;
513
+ this.drainingError.cleanup_error = errorRecord(error);
514
+ this.drainingError.details = { ...this.drainingError.details, cleanup_error: this.drainingError.cleanup_error };
515
+ throw this.drainingError;
454
516
  }
517
+ if (this.drainingError) throw this.drainingError;
518
+ }
519
+ async prepareStop(cancelTree) {
520
+ const current = this.active, priorResult = this.lastResult;
521
+ const emptyTree = !current && !this.state.active_tree && !this.state.unclaimed_activity && !priorResult && !this.descendants.size;
522
+ if (cancelTree) await this.cancel();
523
+ else if (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");
524
+ // A physical kill is settlement, not a native clean terminal receipt.
525
+ // In particular, the previous Turn's SUCCESS cannot certify a cancelled
526
+ // in-flight Turn that exited without its own result.
527
+ const currentResult = !current || current.terminalResult === this.lastResult || this.lastResult !== priorResult;
528
+ const clean = emptyTree || (currentResult && this.receipt().settled);
529
+ return { clean, ...(clean ? {} : { native_settlement: { confirmed: false, reason: "native_terminal_unconfirmed" } }) };
455
530
  }
456
531
  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) };
457
532
  }
458
- 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; }
533
+ async close(cancelTree) { if (cancelTree) cancelPendingDispatch(this); if (!cancelTree && !this.exited && (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.finalizeProviderForControl(); }
459
534
  async abortProvider(reason) {
460
535
  const child = this.child, providerProcess = this.providerProcess;
461
536
  await stopProcessGroup(child);
462
537
  await this.finalizeProviderForControl();
463
538
  await finishDaemonProcess(this.config, providerProcess, "failed", reason);
464
539
  }
465
- async finishResource(state = "stopped", reason = "daemon_stopped") { await finishDaemonProcess(this.config, this.state.resource_process, state, reason); }
540
+ async finishResource(state = "stopped", reason = "daemon_stopped") { await this.captureChildTools(); await finishDaemonProcess(this.config, this.state.resource_process, state, reason); }
466
541
  }
467
542
 
468
543
  export async function serveDaemon(stateDir) {
469
544
  const paths = locations(stateDir), state = await readState(paths.dir); if (!state?.config) throw new DaemonError("daemon_state_missing", "Antigravity daemon state is missing");
470
545
  scheduleDaemonProcessHeartbeat(state.config, state.resource_process);
471
- const runtime = new Runtime(paths, state); await removeSocket(paths); const server = net.createServer(socket => { let buffer = ""; socket.on("error", () => {}); 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;
546
+ const runtime = new Runtime(paths, state); await removeSocket(paths);
547
+ const server = net.createServer(socket => {
548
+ let buffer = "", consumed = false; socket.setEncoding("utf8");
549
+ socket.on("data", chunk => {
550
+ if (consumed) return;
551
+ buffer += chunk; const at = buffer.indexOf("\n"); if (at < 0) return; consumed = true;
552
+ void (async () => {
553
+ let request;
554
+ try {
555
+ request = JSON.parse(buffer.slice(0, at));
556
+ const result = await durableDaemonDispatch(paths.dir, request, () => dispatch(runtime, request), () => dispatch(runtime, { operation: "session.inspect", params: request.params ?? {} }));
557
+ if (result._shutdown) retireDaemonEndpoint(server);
558
+ socket.end(`${JSON.stringify({ schema_id: RESPONSE_SCHEMA, id: request.id, ok: true, result: { ...result, _shutdown: undefined } })}\n`);
559
+ } catch (error) {
560
+ await retireFailedDaemon(server, state, error, patch => runtime.persist(patch));
561
+ socket.end(`${JSON.stringify({ schema_id: RESPONSE_SCHEMA, id: request?.id ?? null, ok: false, error: errorPayload(error) })}\n`);
562
+ }
563
+ })();
564
+ });
565
+ });
566
+ trackDaemonConnections(server); runtime.server = server;
472
567
  await new Promise((resolve, reject) => { server.once("error", reject); server.listen(paths.socket, resolve); }); await chmod(paths.socket, 0o600);
473
568
  try { await runtime.start(state.config.resumeSessionId); runtime.requireSessionIdentity(state.config.resumeSessionId); }
474
569
  catch (error) {
@@ -482,10 +577,11 @@ export async function serveDaemon(stateDir) {
482
577
 
483
578
  async function dispatch(runtime, request) {
484
579
  if (request.schema_id !== REQUEST_SCHEMA) throw new DaemonError("daemon_protocol_mismatch", "unsupported dd-agy request schema"); const params = request.params ?? {};
580
+ if (["session.create", "session.prompt", "session.start", "session.fork", "session.resume"].includes(request.operation)) assertDaemonDispatch(runtime.paths.dir);
485
581
  if (request.operation === "daemon.status") { await runtime.observeHookFailures(); return { daemon_id: runtime.config.daemonId, shutdown_state: runtime.state.shutdown_state, active_tree: runtime.state.active_tree, active_operation: runtime.active?.operationId ?? null, last_hook_rejection: runtime.state.last_hook_rejection ?? null, 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() }; }
486
582
  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(); }
487
583
  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, request.id); }
488
- if (request.operation === "session.inspect") { runtime.requireSessionIdentity(params.sessionId); return runtime.receipt(); }
584
+ if (request.operation === "session.inspect") { runtime.requireSessionIdentity(params.sessionId); await runtime.captureChildTools(); return runtime.receipt(); }
489
585
  if (request.operation === "session.resume") { await runtime.start(params.sessionId ?? runtime.init?.conversation_id ?? null); runtime.requireSessionIdentity(params.sessionId); return runtime.receipt(); }
490
586
  if (request.operation === "session.cancel") { runtime.requireSessionIdentity(params.sessionId); return await runtime.cancel(); }
491
587
  if (request.operation === "hook.observe") {
@@ -511,7 +607,7 @@ async function dispatch(runtime, request) {
511
607
  }
512
608
  catch (error) { await runtime.rejectHook(error, params.event, params.payload ?? {}, current); throw error; }
513
609
  }
514
- 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 }; }
610
+ if (request.operation === "daemon.stop") return await completeDaemonShutdown(runtime.paths.dir, runtime.state, patch => runtime.persist(patch), () => runtime.prepareStop(params.cancelTree === true), [["provider", () => runtime.close(false)], ["daemon_resource", () => runtime.finishResource()]]);
515
611
  throw new DaemonError("unknown_operation", `unknown dd-agy daemon operation: ${request.operation}`);
516
612
  }
517
613
  import { authorizeRetainedDaemonResume } from "./driver-recovery.mjs";
@@ -4,6 +4,8 @@ import os from "node:os";
4
4
  import path from "node:path";
5
5
 
6
6
  import { checkObservedProfile } from "./model-observations.mjs";
7
+ import { stopProcessGroup } from "./managed-daemon.mjs";
8
+ import { errorRecord } from "./operation-errors.mjs";
7
9
 
8
10
  const DEFAULT_MODEL = "gemini-3.1-pro-high";
9
11
  export const AGY_VERSION_TIMEOUT_MS = 30_000;
@@ -19,14 +21,22 @@ export function executable(bin, args = []) {
19
21
  export async function runAgy(bin, args, options = {}) {
20
22
  const target = executable(bin, args);
21
23
  return await new Promise((resolve, reject) => {
22
- const child = spawn(target.command, target.args, { cwd: options.cwd, env: options.env ?? process.env, stdio: ["pipe", "pipe", "pipe"] });
24
+ const child = spawn(target.command, target.args, { cwd: options.cwd, env: options.env ?? process.env, detached: process.platform !== "win32", stdio: ["pipe", "pipe", "pipe"] });
23
25
  let stdout = "", stderr = "", settled = false;
24
- const timer = setTimeout(() => { child.kill("SIGTERM"); finish(new AgyError("agy_timeout", `${args[0] ?? "agy"} timed out`, true)); }, options.timeoutMs ?? 30_000);
26
+ let timeoutError, inputError, cleanup;
27
+ const timer = setTimeout(() => {
28
+ timeoutError = new AgyError("agy_timeout", `${args[0] ?? "agy"} timed out`, true);
29
+ cleanup = stopProcessGroup(child, 1_000).catch(error => { timeoutError.cleanup_error = errorRecord(error); timeoutError.details = { cleanup_error: timeoutError.cleanup_error }; finish(timeoutError); });
30
+ }, options.timeoutMs ?? 30_000);
25
31
  const finish = (error, value) => { if (settled) return; settled = true; clearTimeout(timer); error ? reject(error) : resolve(value); };
26
32
  child.stdout.setEncoding("utf8").on("data", chunk => stdout += chunk);
27
33
  child.stderr.setEncoding("utf8").on("data", chunk => stderr += chunk);
28
34
  child.on("error", error => finish(error));
29
- 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 })));
35
+ child.on("close", async code => { await cleanup; if (timeoutError) finish(timeoutError); else if (inputError) finish(inputError); else if (code === 0) finish(null, { stdout, stderr, code }); else finish(new AgyError("agy_cli_failed", stderr.trim() || stdout.trim() || `agy exited ${code}`, false, { exit_code: code })); });
36
+ child.stdin.on("error", error => {
37
+ inputError ??= error;
38
+ cleanup ??= stopProcessGroup(child, 1_000).catch(cause => { inputError.details = { ...inputError.details, cleanup_error: errorRecord(cause) }; finish(timeoutError ?? inputError); });
39
+ });
30
40
  child.stdin.end(options.stdin ?? "");
31
41
  });
32
42
  }
@@ -2,14 +2,15 @@ import { captureDispatchGuard, cancelPendingDispatch } from "./dispatch-fence.mj
2
2
  import { createHash, randomUUID } from "node:crypto";
3
3
  import { chmod, lstat, mkdir, open, readFile, rename, unlink, writeFile } from "node:fs/promises";
4
4
  import net from "node:net";
5
- import { durableDaemonDispatch } from "./daemon-operations.mjs";
5
+ import { durableDaemonDispatch, completeDaemonShutdown, assertDaemonDispatch, assertDaemonRestartable, trackDaemonConnections, retireDaemonEndpoint, retireFailedDaemon } from "./daemon-operations.mjs";
6
6
  import { observedTimeout } from "./observation-clock.mjs";
7
+ import { parseCapacityOperation } from "./codex-capacity-policy.mjs";
7
8
  import path from "node:path";
8
9
  import { spawn } from "node:child_process";
9
10
  import { waitForSettlement } from "./session-settlement.mjs";
10
11
  import { errorRecord } from "./operation-errors.mjs";
11
12
  import { CodexBridge, cancelSessionWithBridge, createSessionWithBridge, inspectSessionWithBridge, promptSessionWithBridge, startSessionWithBridge } from "./dd-codex.mjs";
12
- import { prepareRuntimeOwner, cleanupFailedDaemonStart, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, scheduleDaemonProcessHeartbeat, registerDaemonProcess, startManagedBridge, stopProcessGroup } from "./managed-daemon.mjs";
13
+ import { assertDaemonOwnership, prepareRuntimeOwner, cleanupFailedDaemonStart, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, scheduleDaemonProcessHeartbeat, registerDaemonProcess, startManagedBridge, stopProcessGroup } from "./managed-daemon.mjs";
13
14
 
14
15
  const requestSchema = "dd-codex/daemon-request@1";
15
16
  const responseSchema = "dd-codex/daemon-response@1";
@@ -38,7 +39,7 @@ async function observeCodexSessions(bridge, options, sessions, active, remember
38
39
  }
39
40
  const unsettled = [], observations = [];
40
41
  for (const sessionId of sessions) {
41
- const observed = await inspectSessionWithBridge(bridge, { ...options, sessionId });
42
+ const observed = await inspectSessionWithBridge(bridge, { ...options, sessionId, turnId: sessionId === options.sessionId ? options.turnId : undefined });
42
43
  for (const child of observed.descendants ?? []) { sessions.add(child.provider_session_id); remember(child.provider_session_id); }
43
44
  const status = observed.status?.type ?? observed.status;
44
45
  const stored = ["notLoaded", "systemError"].includes(status) ? await bridge.request("thread/turns/list", { threadId: sessionId, limit: 1, sortDirection: "desc", itemsView: "summary" }, 5_000) : null;
@@ -84,24 +85,24 @@ export async function startDaemon(options) {
84
85
  const paths = locations(options.stateDir); const cwd = absolute(options.cwd, "--cwd"); const config = { cwd, journal: absolute(options.journal, "--journal"), bin: options.bin ?? null, ddFlowBin: options.ddFlowBin ? absolute(options.ddFlowBin, "--dd-flow-bin") : null, ddFlowHome: options.ddFlowHome ? absolute(options.ddFlowHome, "--dd-flow-home") : null, projectRoot: options.projectRoot ? absolute(options.projectRoot, "--project-root") : cwd, resourceHome: process.env.DD_FLOW_RESOURCE_HOME ? absolute(process.env.DD_FLOW_RESOURCE_HOME, "DD_FLOW_RESOURCE_HOME") : null, env: options.ddFlowHome ? { DD_FLOW_HOME: absolute(options.ddFlowHome, "--dd-flow-home"), ...(options.ddFlowBin ? { DD_FLOW_BIN: absolute(options.ddFlowBin, "--dd-flow-bin") } : {}) } : {} };
85
86
  await prepareRuntimeOwner(config, paths.dir);
86
87
  await mkdir(paths.dir, { recursive: true, mode: 0o700 }); await chmod(paths.dir, 0o700);
88
+ let previous = await readState(paths.state); assertDaemonRestartable(previous);
87
89
  try { const status = await callDaemon(paths.dir, "daemon.status", {}, 1_000); if (JSON.stringify(status.config) !== JSON.stringify(config)) throw Object.assign(new Error("running daemon has a different configuration"), { code: "daemon_config_mismatch" }); return { ...status, already_running: true }; } catch (error) { if (!["daemon_not_running", "daemon_timeout"].includes(error.code)) throw error; }
88
- 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" });
90
+ previous = await readState(paths.state); assertDaemonRestartable(previous);
91
+ if (previous?.shutdown_state === "running") throw Object.assign(new Error("previous dd-codex daemon died; do not reuse its state directory"), { code: "invalid_harness_crash" });
89
92
  if (previous?.shutdown_state === "clean") await authorizeRetainedDaemonResume(paths.dir, previous, options.sessionId, config);
90
93
  await removeSocket(paths.socket); const resourceProcess = await registerDaemonProcess(config, { kind: "codex-daemon", owner: `codex:${path.basename(paths.dir)}`, operation: `codex-daemon:${path.basename(paths.dir)}`, stdout: paths.log, stderr: paths.log }); const state = { schema_id: stateSchema, daemon_id: randomUUID(), pid: null, cwd: config.cwd, journal: config.journal, config, socket: paths.socket, started_at: new Date().toISOString(), shutdown_state: "starting", sessions: previous?.sessions ?? [], resource_process: resourceProcess }; await writeState(paths.state, state);
91
94
  const log = await open(paths.log, "a", 0o600); const child = spawn(process.execPath, [options.entryPath, "daemon", "serve", "--state-dir", paths.dir], { cwd: config.cwd, detached: true, stdio: ["ignore", log.fd, log.fd] }); child.unref(); await log.close();
92
95
  try { await confirmDaemonProcess(config, resourceProcess, child); const deadline = Date.now() + 60_000; while (Date.now() < deadline) { await new Promise((resolve) => setTimeout(resolve, 100)); try { return await callDaemon(paths.dir, "daemon.status", {}, 5_000); } catch (error) { if (!["daemon_not_running", "daemon_timeout"].includes(error.code)) throw error; } } throw Object.assign(new Error("daemon did not become ready"), { code: "daemon_start_failed" }); }
93
96
  catch (error) { await cleanupFailedDaemonStart(config, resourceProcess, child, error, () => readState(paths.state), state.daemon_id); throw error; }
94
97
  }
95
- export async function stopDaemon(options) { const timeoutMs = options.timeoutMs ?? 30_000; return await confirmDaemonStopped(() => callDaemon(options.stateDir, "daemon.stop", { cancelTree: options.cancelTree === true }, timeoutMs), locations(options.stateDir).socket, timeoutMs); }
98
+ export async function stopDaemon(options) { const timeoutMs = options.timeoutMs ?? 30_000; return await confirmDaemonStopped(() => callDaemon(options.stateDir, "daemon.stop", { cancelTree: options.cancelTree === true }, timeoutMs), locations(options.stateDir).socket, timeoutMs, { readState: () => readState(locations(options.stateDir).state) }); }
96
99
 
97
100
  export async function serveDaemon(stateDir) {
98
101
  const paths = locations(stateDir); const state = await readState(paths.state); if (!state || state.schema_id !== stateSchema) throw Object.assign(new Error("daemon state missing"), { code: "daemon_state_missing" });
99
102
  const bridge = new CodexBridge({ ...state.config, env: { ...state.config.env, CODEX_HOME: process.env.CODEX_HOME, DD_FLOW_DAEMON_ID: state.daemon_id, DD_FLOW_CODEX_STATE_DIR: paths.dir } }); await startManagedBridge({ config: state.config, state, bridge, kind: "codex-provider", publish: snapshot => writeState(paths.state, snapshot) }); const sessions = new Set(state.sessions ?? []); let active = null; let server; let persist = Promise.resolve();
100
- const closeBridge = bridge.close.bind(bridge);
101
- bridge.close = async () => { await closeBridge(); await finishDaemonProcess(state.config, state.provider_process, "stopped", "daemon_stopped"); };
102
103
  scheduleDaemonProcessHeartbeat(state.config, state.resource_process);
103
- const save = (patch = {}) => { Object.assign(state, patch, { pid: process.pid, updated_at: new Date().toISOString(), sessions: [...sessions] }); const snapshot = structuredClone(state); persist = persist.then(() => writeState(paths.state, snapshot)); return persist; };
104
- const dispatch = async (operation, params) => {
104
+ const save = (patch = {}) => { Object.assign(state, patch, { pid: process.pid, updated_at: new Date().toISOString(), sessions: [...sessions] }); const snapshot = structuredClone(state); persist = persist.catch(() => {}).then(() => writeState(paths.state, snapshot)); return persist; };
105
+ const dispatch = async (operation, params, operationId) => {
105
106
  const assertDispatch = captureDispatchGuard(bridge);
106
107
  if (operation === "hook.verify") {
107
108
  if (params.daemonId !== state.daemon_id) throw Object.assign(new Error("Hook daemon identity differs"), { code: "native_hook_unproven" });
@@ -113,6 +114,7 @@ export async function serveDaemon(stateDir) {
113
114
  // available while a request is marked active; callers must first prove the
114
115
  // provider Session itself is idle.
115
116
  if (operation === "daemon.stop") {
117
+ return await completeDaemonShutdown(paths.dir, state, save, async () => {
116
118
  if (params.cancelTree) cancelPendingDispatch(bridge);
117
119
  if (active && !params.cancelTree) throw Object.assign(new Error("productive operation is running"), { code: "tree_not_settled" });
118
120
  await waitForSettlement({
@@ -120,12 +122,34 @@ export async function serveDaemon(stateDir) {
120
122
  observe: () => observeCodexSessions(bridge, state.config, sessions, () => active),
121
123
  ...(params.cancelTree ? { cancel: async sessionId => { try { await cancelSessionWithBridge(bridge, { ...state.config, sessionId }); } catch (error) { if (error.code !== "turn_not_active") throw error; } } } : {})
122
124
  });
123
- await save({ active_tree: false });
124
- return { stopped: true, clean: true, _shutdown: true };
125
+ return { clean: true };
126
+ }, [
127
+ ["provider", () => bridge.close()],
128
+ ["provider_resource", () => finishDaemonProcess(state.config, state.provider_process)],
129
+ ["daemon_resource", () => finishDaemonProcess(state.config, state.resource_process)]
130
+ ]);
125
131
  }
132
+ if (["session.create", "session.prompt", "session.start", "session.fork", "session.resume"].includes(operation)) assertDaemonDispatch(paths.dir);
126
133
  if (active && !["session.inspect", "session.cancel"].includes(operation)) throw Object.assign(new Error(`${active} is in progress`), { code: "operation_busy", retryable: true });
127
134
  if (operation === "session.create") { active = operation; try { await save({ active_operation: active }); const result = await createSessionWithBridge(bridge, { ...state.config, ...params }); sessions.add(result.provider_session_id); return result; } finally { active = null; await save({ active_operation: null }); } }
128
- 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 }); } }
135
+ if (operation === "session.prompt") {
136
+ parseCapacityOperation(operationId);
137
+ // Admission can wait for a permit. Observe again after it returns, not
138
+ // just before the caller's backoff, so no child Turn races this dispatch.
139
+ // Reserve before the first await: distinct operation IDs are independent
140
+ // journal entries and must not both pass the idle check while inspecting.
141
+ active = operation;
142
+ try {
143
+ const tree = await inspectCodexTree(bridge, { ...state.config, ...params });
144
+ assertDispatch();
145
+ if (tree.settled !== true) throw Object.assign(new Error("native Session tree changed while waiting for admission"), { code: "tree_not_settled", details: { provider_session_id: params.sessionId, unsettled_sessions: tree.unsettled_sessions } });
146
+ if (/:capacity:[1-9]\d*:session\.prompt$|:child:[a-f0-9]+:followup:[1-9]\d*$/.test(operationId ?? ""))
147
+ await assertDaemonOwnership(paths.dir, { "operation-id": operationId, operation, "native-session-id": params.sessionId, "prompt-sha256": createHash("sha256").update(params.prompt ?? "").digest("hex") }, assertDispatch);
148
+ sessions.add(params.sessionId);
149
+ await save({ active_operation: active }); return await promptSessionWithBridge(bridge, { ...state.config, ...params, assertDispatch });
150
+ }
151
+ finally { active = null; await save({ active_operation: null }); }
152
+ }
129
153
  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 }); } }
130
154
  if (operation === "session.inspect" || operation === "session.resume") {
131
155
  const owned = new Set(sessions);
@@ -143,7 +167,28 @@ export async function serveDaemon(stateDir) {
143
167
  }
144
168
  throw Object.assign(new Error(`unknown operation: ${operation}`), { code: "unknown_operation" });
145
169
  };
146
- 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`); } })(); }); });
170
+ await removeSocket(paths.socket);
171
+ server = net.createServer(connection => {
172
+ connection.setEncoding("utf8"); let buffer = "", consumed = false;
173
+ connection.on("data", chunk => {
174
+ if (consumed) return;
175
+ buffer += chunk; const at = buffer.indexOf("\n"); if (at < 0) return; consumed = true;
176
+ void (async () => {
177
+ let request;
178
+ try {
179
+ request = JSON.parse(buffer.slice(0, at));
180
+ if (request.schema_id !== requestSchema || !request.id || !request.operation) throw Object.assign(new Error("invalid daemon request"), { code: "daemon_protocol" });
181
+ const result = await durableDaemonDispatch(paths.dir, request, () => dispatch(request.operation, request.params ?? {}, request.id), () => dispatch("session.inspect", request.params ?? {}));
182
+ if (result._shutdown) retireDaemonEndpoint(server);
183
+ connection.end(`${JSON.stringify({ schema_id: responseSchema, id: request.id, ok: true, result: { ...result, _shutdown: undefined } })}\n`);
184
+ } catch (error) {
185
+ await retireFailedDaemon(server, state, error, save);
186
+ connection.end(`${JSON.stringify({ schema_id: responseSchema, id: request?.id ?? null, ok: false, error: errorPayload(error) })}\n`);
187
+ }
188
+ })();
189
+ });
190
+ });
191
+ trackDaemonConnections(server);
147
192
  await new Promise((resolve, reject) => { server.once("error", reject); server.listen(paths.socket, resolve); }); await chmod(paths.socket, 0o600); await save({ shutdown_state: "running", active_operation: null }); return await new Promise(() => {});
148
193
  }
149
194
  import { authorizeRetainedDaemonResume } from "./driver-recovery.mjs";
@@ -2,7 +2,8 @@ import { spawn } from "node:child_process";
2
2
  import { appendFile, mkdir, readFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import readline from "node:readline";
5
- import { stopProcessGroup } from "./managed-daemon.mjs";
5
+ import { closeBridgeProcess } from "./managed-daemon.mjs";
6
+ import { errorRecord } from "./operation-errors.mjs";
6
7
  import { ObservationClock, observedTimeout } from "./observation-clock.mjs";
7
8
  import { observeModel } from "./model-observations.mjs";
8
9
 
@@ -230,7 +231,7 @@ export class CodexBridge {
230
231
  this.descendants.set(threadId, { ...previous, status: "running", active_turn_id: turn.id, terminal_turn_id: null, provider_error: null });
231
232
  } else if (value.method === "turn/completed" && (!previous.active_turn_id || previous.active_turn_id === turn.id)) {
232
233
  this.descendants.set(threadId, { ...previous, status: turn.status === "completed" ? "completed" : turn.status ?? "unknown",
233
- active_turn_id: turn.id, terminal_turn_id: turn.id, terminal_observed_at: now(), provider_error: nativeTurnError(this, threadId, turn), native_turn_items: turnItemEvidence(turn) });
234
+ active_turn_id: turn.id, terminal_turn_id: turn.id, terminal_observed_at: previous.terminal_turn_id === turn.id ? previous.terminal_observed_at ?? now() : now(), provider_error: nativeTurnError(this, threadId, turn), native_turn_items: turnItemEvidence(turn) });
234
235
  }
235
236
  }
236
237
  }
@@ -312,16 +313,16 @@ export class CodexBridge {
312
313
  }
313
314
  rejectAll(error) { for (const pending of this.pending.values()) { clearTimeout(pending.timer); pending.reject(error); } this.pending.clear(); }
314
315
  async close() {
315
- if (!this.child) return;
316
- this.child.stdin.end();
317
- await Promise.race([new Promise((resolve) => this.child.once("exit", resolve)), new Promise((resolve) => setTimeout(resolve, 2_000))]);
318
- await stopProcessGroup(this.child, 2_000);
319
- await this.modelObservations;
320
- await this.journal.flush();
316
+ return await closeBridgeProcess(this, async () => { await this.modelObservations; await this.journal.flush(); });
321
317
  }
322
318
  }
323
319
 
324
- async function withBridge(options, action) { const bridge = new CodexBridge(options); try { await bridge.start(); return await action(bridge); } finally { await bridge.close(); } }
320
+ async function withBridge(options, action) {
321
+ const bridge = new CodexBridge(options); let primary;
322
+ try { await bridge.start(); return await action(bridge); }
323
+ catch (error) { primary = error; throw error; }
324
+ finally { try { await bridge.close(); } catch (error) { if (!primary) throw error; primary.cleanup_error = errorRecord(error); primary.details = { ...primary.details, cleanup_error: primary.cleanup_error }; } }
325
+ }
325
326
  function profile(options) { return { harness: "codex-desktop", model: options.model ?? null, reasoning: options.reasoning ?? null }; }
326
327
  function threadId(result) { const id = result?.thread?.id ?? result?.threadId; return required(id, "Codex thread id"); }
327
328
  function directChildren(bridge, sessionId) { return typeof bridge?.directChildren === "function" ? bridge.directChildren(sessionId) : []; }
@@ -542,25 +543,12 @@ function nativeTurnError(bridge, sessionId, turn) {
542
543
  function turnItemEvidence(turn) {
543
544
  const items = turn?.items;
544
545
  return Array.isArray(items) && turn?.itemsView !== "notLoaded" ? { observed: true, count: items.length, types: items.map(item => item?.type ?? "unknown"),
546
+ items: items.map(item => ({ id: item?.id ?? null, type: item?.type ?? "unknown", status: item?.status ?? null })),
547
+ pending: items.some(item => !["agentMessage", "reasoning", "userMessage"].includes(item?.type) && !["completed", "failed", "declined"].includes(item?.status)),
545
548
  possible_effects: items.some(item => !["agentMessage", "reasoning", "userMessage"].includes(item?.type)) } : { observed: false };
546
549
  }
547
550
 
548
- export function terminalCodexOverload(error, expectedSessionId, expectedTurnId) {
549
- let current = error;
550
- for (let depth = 0; depth < 4 && current && typeof current === "object"; depth += 1) {
551
- if (current.code === "turn_interrupted") {
552
- const details = current.details ?? {}, native = details.provider_error;
553
- const code = native?.codexErrorInfo;
554
- return details.terminal_status === "failed" && typeof details.provider_session_id === "string" && details.provider_session_id === expectedSessionId
555
- && typeof details.turn_id === "string" && Boolean(details.turn_id) && details.native_turn_id === details.turn_id
556
- && (expectedTurnId === undefined || details.turn_id === expectedTurnId)
557
- && (code === "serverOverloaded" || code?.code === "serverOverloaded");
558
- }
559
- if (current.code !== "harness_adapter_failed") return false;
560
- current = current.details?.cause ?? current.cause;
561
- }
562
- return false;
563
- }
551
+ export { terminalCodexOverload } from "./codex-capacity-policy.mjs";
564
552
 
565
553
  function isTerminalTurnStatus(status) { return ["completed", "failed", "interrupted", "cancelled"].includes(status); }
566
554
  function isAuthoritativeTerminal(bridge, turnId, status, threadStatus, failureNotBefore) {
@@ -615,7 +603,9 @@ export async function inspectSession(options) {
615
603
 
616
604
  export async function inspectSessionWithBridge(bridge, options) {
617
605
  const sessionId = required(options.sessionId, "--session-id"); const value = await bridge.request("thread/read", { threadId: sessionId, includeTurns: false }); const thread = value?.thread ?? value;
618
- return { harness: "codex-desktop", provider_session_id: sessionId, adapter_session_id: sessionId, status: thread.status ?? null, thread: threadReceipt(thread), descendants: await observedChildren(bridge, sessionId), requested_profile: profile(options), observed_profile: await observeCodexProfile(bridge, options, sessionId, { model: thread.model ?? null, reasoning: thread.reasoningEffort ?? null, provider: thread.modelProvider ?? null }, "thread/read") };
606
+ const terminal = options.turnId ? (await readTurn(bridge, sessionId, options.turnId, true))?.turn : null;
607
+ const exact = terminal?.id === options.turnId && terminal?.status === "failed" ? { terminal_turn_id: terminal.id, terminal_status: terminal.status, provider_error: nativeTurnError(bridge, sessionId, terminal), native_turn_items: turnItemEvidence(terminal) } : {};
608
+ return { harness: "codex-desktop", provider_session_id: sessionId, adapter_session_id: sessionId, status: thread.status ?? null, thread: threadReceipt(thread), ...exact, descendants: await observedChildren(bridge, sessionId), requested_profile: profile(options), observed_profile: await observeCodexProfile(bridge, options, sessionId, { model: thread.model ?? null, reasoning: thread.reasoningEffort ?? null, provider: thread.modelProvider ?? null }, "thread/read") };
619
609
  }
620
610
 
621
611
  export async function cancelSession(options) {