@deksden-com/dd-flow-cli 0.9.0-beta.123 → 0.9.0-beta.127

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 (122) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +33 -1
  3. package/dist/build-info.json +7 -7
  4. package/dist/cli/argument-values.js +38 -0
  5. package/dist/cli/command-inputs.js +4 -3
  6. package/dist/cli/help.js +1 -0
  7. package/dist/cli/hook-ingress.js +1 -1
  8. package/dist/cli/run-cli.js +126 -193
  9. package/dist/cli/runtime-maintenance-diagnostics.js +46 -0
  10. package/dist/cli/runtime-maintenance-ingress.js +51 -0
  11. package/dist/cli/runtime-process.js +128 -0
  12. package/dist/cli.js +8 -5
  13. package/dist/domain/stage-catalog.js +1 -1
  14. package/dist/harness-runtime/bin/dd-agy.mjs +3 -2
  15. package/dist/harness-runtime/bin/dd-codex.mjs +3 -2
  16. package/dist/harness-runtime/bin/dd-droid.mjs +3 -2
  17. package/dist/harness-runtime/bin/dd-grok.mjs +16 -6
  18. package/dist/harness-runtime/bin/dd-opencode.mjs +3 -2
  19. package/dist/harness-runtime/bin/dd-zcode.mjs +20 -16
  20. package/dist/harness-runtime/lib/adapter-timeouts.d.mts +5 -0
  21. package/dist/harness-runtime/lib/adapter-timeouts.mjs +34 -0
  22. package/dist/harness-runtime/lib/codex-capacity-policy.d.mts +10 -0
  23. package/dist/harness-runtime/lib/codex-capacity-policy.mjs +89 -0
  24. package/dist/harness-runtime/lib/daemon-observation.d.mts +2 -0
  25. package/dist/harness-runtime/lib/daemon-observation.mjs +163 -0
  26. package/dist/harness-runtime/lib/daemon-operations.mjs +159 -15
  27. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +215 -86
  28. package/dist/harness-runtime/lib/dd-agy.mjs +11 -13
  29. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +47 -23
  30. package/dist/harness-runtime/lib/dd-codex.mjs +257 -99
  31. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +32 -30
  32. package/dist/harness-runtime/lib/dd-droid.mjs +117 -31
  33. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +96 -38
  34. package/dist/harness-runtime/lib/dd-grok.mjs +15 -13
  35. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +54 -20
  36. package/dist/harness-runtime/lib/dd-opencode.mjs +44 -14
  37. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +81 -76
  38. package/dist/harness-runtime/lib/dd-zcode.mjs +204 -116
  39. package/dist/harness-runtime/lib/driver-recovery.mjs +12 -2
  40. package/dist/harness-runtime/lib/lease-renewal.d.mts +8 -0
  41. package/dist/harness-runtime/lib/lease-renewal.mjs +23 -0
  42. package/dist/harness-runtime/lib/lifecycle-command.d.mts +3 -3
  43. package/dist/harness-runtime/lib/lifecycle-command.mjs +74 -20
  44. package/dist/harness-runtime/lib/managed-daemon.d.mts +11 -0
  45. package/dist/harness-runtime/lib/managed-daemon.mjs +321 -65
  46. package/dist/harness-runtime/lib/native-children.d.mts +13 -0
  47. package/dist/harness-runtime/lib/native-children.mjs +78 -0
  48. package/dist/harness-runtime/lib/native-hook-command.d.mts +4 -0
  49. package/dist/harness-runtime/lib/native-hook-command.mjs +115 -11
  50. package/dist/harness-runtime/lib/native-streams.mjs +45 -0
  51. package/dist/harness-runtime/lib/observation-clock.d.mts +18 -0
  52. package/dist/harness-runtime/lib/observation-clock.mjs +73 -17
  53. package/dist/harness-runtime/lib/operation-errors.d.mts +14 -0
  54. package/dist/harness-runtime/lib/operation-errors.mjs +105 -8
  55. package/dist/harness-runtime/lib/operation-progress.d.mts +7 -0
  56. package/dist/harness-runtime/lib/operation-progress.mjs +112 -0
  57. package/dist/harness-runtime/lib/process-json.mjs +136 -38
  58. package/dist/harness-runtime/lib/tool-observations.d.mts +1 -0
  59. package/dist/harness-runtime/lib/tool-observations.mjs +5 -2
  60. package/dist/harness-runtime/lib/zcode-usage.mjs +3 -2
  61. package/dist/runtime/context.js +8 -4
  62. package/dist/schemas/flow-run.schema.json +1 -0
  63. package/dist/schemas/plan-review-decision.schema.json +3 -3
  64. package/dist/schemas/run-control-receipt.schema.json +19 -0
  65. package/dist/services/code-checks.js +93 -27
  66. package/dist/services/codex-capacity.js +251 -74
  67. package/dist/services/codex-hook-delivery.js +2 -2
  68. package/dist/services/compatibility-preflight.js +4 -1
  69. package/dist/services/controller-fanout.js +9 -75
  70. package/dist/services/engines.js +70 -20
  71. package/dist/services/eval-snapshots.js +179 -40
  72. package/dist/services/external-work-launch.js +21 -9
  73. package/dist/services/harness-adapter.js +195 -25
  74. package/dist/services/hooks.js +65 -21
  75. package/dist/services/lifecycle-invocations.js +404 -32
  76. package/dist/services/managed-lease-monitor.js +128 -0
  77. package/dist/services/managed-processes.js +117 -53
  78. package/dist/services/merge-server.js +66 -23
  79. package/dist/services/recovery-observation-budget.js +67 -7
  80. package/dist/services/recovery-snapshot-database.js +7 -4
  81. package/dist/services/review-deferral.js +23 -0
  82. package/dist/services/run-control-receipt.js +1 -1
  83. package/dist/services/run-control-worker.js +177 -118
  84. package/dist/services/run-control.js +99 -8
  85. package/dist/services/run-controller-adapter.js +177 -51
  86. package/dist/services/run-controller-capture.js +14 -2
  87. package/dist/services/run-controller-process.js +11 -4
  88. package/dist/services/run-controller-recovery.js +17 -0
  89. package/dist/services/run-controller-state.js +68 -7
  90. package/dist/services/run-controller.js +207 -107
  91. package/dist/services/run-fork.js +8 -3
  92. package/dist/services/run-recovery-guard.js +75 -0
  93. package/dist/services/run-recovery-runtime.js +21 -30
  94. package/dist/services/run-recovery.js +17 -13
  95. package/dist/services/runs.js +40 -7
  96. package/dist/services/runtime-budget.js +32 -15
  97. package/dist/services/runtime-scope-control.js +8 -0
  98. package/dist/services/runtime-scope-resume.js +30 -8
  99. package/dist/services/runtime-scope-worker.js +118 -73
  100. package/dist/services/runtime-service.js +6 -7
  101. package/dist/services/scope-writer-cleanup.js +3 -2
  102. package/dist/services/stage-context.js +135 -0
  103. package/dist/services/stage-lifecycle.js +3 -15
  104. package/dist/services/stage-pause.js +3 -2
  105. package/dist/services/usage.js +14 -3
  106. package/dist/services/verification-matrix.js +30 -1
  107. package/dist/services/vnext-code-review.js +26 -13
  108. package/dist/services/vnext-code.js +10 -8
  109. package/dist/services/vnext-contracts.js +10 -1
  110. package/dist/services/vnext-execution-profile.js +75 -2
  111. package/dist/services/vnext-fanout.js +5 -0
  112. package/dist/services/vnext-merge.js +30 -11
  113. package/dist/services/vnext-plan-review.js +102 -35
  114. package/dist/services/vnext-plan.js +9 -7
  115. package/dist/services/vnext-protocolize.js +15 -9
  116. package/dist/services/vnext-specify.js +6 -6
  117. package/dist/services/work-registry.js +43 -6
  118. package/dist/services/workspace-bootstrap.js +14 -3
  119. package/dist/shared/errors.js +11 -6
  120. package/dist/storage/agent-output.js +26 -0
  121. package/dist/storage/database.js +80 -21
  122. package/package.json +7 -4
@@ -1,4 +1,7 @@
1
+ import { callDaemonObserved } from './daemon-observation.mjs';
2
+ import { operationProgressReporter } from './operation-progress.mjs';
1
3
  import { captureDispatchGuard, cancelPendingDispatch } from "./dispatch-fence.mjs";
4
+ import { errorRecord } from "./operation-errors.mjs";
2
5
  import { ToolObservations, agyToolObservation, agyTranscriptObservations } from "./tool-observations.mjs";
3
6
  import { constants } from "node:fs";
4
7
  import { fileURLToPath } from "node:url";
@@ -7,14 +10,15 @@ import { appendFile, chmod, lstat, mkdir, open, readFile, realpath, rename, unli
7
10
  import { spawn } from "node:child_process";
8
11
  import net from "node:net";
9
12
  import { isDeepStrictEqual } from "node:util";
10
- import { ObservationClock } from "./observation-clock.mjs";
13
+ import { ObservationClock, observedTimeout, validateObservationDuration } from "./observation-clock.mjs";
11
14
  import { hookBudget } from "./native-hook-command.mjs";
12
15
  import { agyWorkspaceHooks } from "./agy-hooks.mjs";
13
- import { durableDaemonDispatch } from "./daemon-operations.mjs";
16
+ import { durableDaemonDispatch, completeDaemonShutdown, assertDaemonDispatch, assertDaemonRestartable, trackDaemonConnections, retireDaemonEndpoint, retireFailedDaemon } from "./daemon-operations.mjs";
14
17
  import path from "node:path";
15
18
  import { observeModel } from "./model-observations.mjs";
16
- import { AGY_VERSION_TIMEOUT_MS, AgyError, assertProfile, executable, observedProfile, runAgy, usageSnapshot } from "./dd-agy.mjs";
19
+ import { AGY_VERSION_TIMEOUT_MS, AgyError, agyTerminalFailure, assertProfile, executable, observedProfile, runAgy, usageSnapshot } from "./dd-agy.mjs";
17
20
  import { prepareRuntimeOwner, cleanupFailedStart, cleanupFailedDaemonStart, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, scheduleDaemonProcessHeartbeat, registerDaemonProcess, stopProcessGroup } from "./managed-daemon.mjs";
21
+ import { readNativeLines, streamNativeDiagnostics, nativeObservationError, admitNativeRequestBuffer } from "./native-streams.mjs";
18
22
 
19
23
  const REQUEST_SCHEMA = "dd-agy/daemon-request@1", RESPONSE_SCHEMA = "dd-agy/daemon-response@1", STATE_SCHEMA = "dd-agy/daemon-state@1";
20
24
  class DaemonError extends AgyError {}
@@ -53,15 +57,15 @@ async function writeJson(file, value) { const temporary = `${file}.${process.pid
53
57
  async function readState(dir) { try { return JSON.parse(await readFile(locations(dir).state, "utf8")); } catch (error) { if (error.code === "ENOENT") return null; throw error; } }
54
58
  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; } }
55
59
  const shell = value => `'${String(value).replaceAll("'", `'"'"'`)}'`;
56
- export function sanitizedEnv(config) { const env = { ...process.env, TMPDIR: config.temporary, DD_AGY_HOOK_NODE: process.execPath, DD_AGY_HOOK_ENTRY: config.entryPath, DD_AGY_STATE_DIR: config.stateDir, DD_AGY_DAEMON_ID: config.daemonId }; for (const key of ["GEMINI_API_KEY","GOOGLE_API_KEY","ANTHROPIC_API_KEY","OPENAI_API_KEY","DD_FLOW_DAEMON_ID"]) delete env[key]; if (config.ddFlowHome && config.daemonId) env.DD_FLOW_DAEMON_ID = config.daemonId; return env; }
57
- function errorPayload(error) { return { code: error.code ?? "operation_failed", message: error.message ?? String(error), retryable: error.retryable === true, ...(error.details === undefined ? {} : { details: error.details }) }; }
60
+ export function sanitizedEnv(config) { const env = { ...process.env, TMPDIR: config.temporary, DD_AGY_HOOK_NODE: process.execPath, DD_AGY_HOOK_ENTRY: config.entryPath, DD_AGY_STATE_DIR: config.stateDir, DD_AGY_DAEMON_ID: config.daemonId }; for (const key of ["GEMINI_API_KEY","GOOGLE_API_KEY","ANTHROPIC_API_KEY","OPENAI_API_KEY","DD_FLOW_DAEMON_ID"]) env[key] = undefined; if (config.ddFlowHome && config.daemonId) env.DD_FLOW_DAEMON_ID = config.daemonId; return env; }
61
+ const errorPayload = errorRecord;
58
62
  async function forwardUsage(config, sessionId, result, tools, descendants) {
59
63
  if (!config.ddFlowBin || !config.projectRoot) return;
60
64
  const snapshot = usageSnapshot(result, tools), hasChildren = descendants.length > 0;
61
65
  if (snapshot.total_tokens === null) return { status: "unavailable", reason: "provider_did_not_report_total_tokens" };
62
66
  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 };
63
67
  const target = executable(config.ddFlowBin, ["agy", "usage", "ingest", "--project-root", config.projectRoot, "--json"]);
64
- 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`); });
68
+ await commandText(target.command, target.args, { input: `${JSON.stringify(payload)}\n`, env: { DD_FLOW_HOME: config.ddFlowHome }, phase: "usage-ingest" });
65
69
  return { status: "ingested" };
66
70
  }
67
71
 
@@ -159,39 +163,11 @@ export async function qualifyAgyHooks(projectRoot, { stateDir = null, install =
159
163
  }
160
164
 
161
165
  export async function callDaemon(stateDir, operation, params = {}, timeoutMs = 30_000, requestId = null) {
162
- const paths = locations(stateDir), request = { schema_id: REQUEST_SCHEMA, id: requestId ?? process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation, params };
163
- return await new Promise((resolve, reject) => {
164
- const client = net.createConnection(paths.socket); let buffer = "", settled = false, timer, checking = false, lastActivity = Date.now();
165
- const clock = new ObservationClock({ timeoutMs, onGap: gap => { void appendFile(paths.journal, `${JSON.stringify({ kind: "observation_gap", payload: { ...gap, operation_id: request.id } })}\n`, { mode: 0o600 }).catch(() => {}); } });
166
- const finish = (error, result) => { if (settled) return; settled = true; if (timer) clearInterval(timer); client.destroy(); error ? reject(error) : resolve(result); };
167
- const poll = Math.min(5_000, Math.max(25, Math.floor(timeoutMs / 20)));
168
- timer = setInterval(async () => {
169
- if (settled || checking) return;
170
- checking = true;
171
- try {
172
- if (operation !== "session.prompt") {
173
- if (clock.sample()) finish(new DaemonError("daemon_timeout", `${operation} timed out`, true));
174
- return;
175
- }
176
- let status = null;
177
- try { status = await callDaemon(stateDir, "daemon.status", {}, Math.min(timeoutMs, 5_000)); }
178
- catch { /* The original RPC retains the authoritative failure. */ }
179
- const observed = Date.parse(status?.last_activity_at ?? "");
180
- const progressed = status?.active_tree === true && Number.isFinite(observed) && observed > lastActivity;
181
- if (progressed) lastActivity = observed;
182
- if (!clock.sample(progressed ? lastActivity : undefined, progressed ? lastActivity : undefined)) return;
183
- finish(new DaemonError(status?.active_tree ? "subject_liveness_timeout" : "operation_observation_lost", `${operation} has no recent productive provider activity`, true, {
184
- operation_id: request.id,
185
- last_activity_at: status?.last_activity_at ?? null,
186
- active_tree: status?.active_tree === true
187
- }));
188
- } finally { checking = false; }
189
- }, poll);
190
- 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)));
191
- });
166
+ const request = { schema_id: REQUEST_SCHEMA, id: requestId ?? process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation, params };
167
+ return await callDaemonObserved({ stateDir, socket: locations(stateDir).socket, request, responseSchema: RESPONSE_SCHEMA, harness: 'agy', timeoutMs });
192
168
  }
193
169
 
194
- function sameConfig(state, config) { return ["cwd","journal","bin","provider","model","reasoning","mode","projectRoot","ddFlowBin","ddFlowHome","resourceHome","noFlow"].every(key => (state.config?.[key] ?? null) === (config[key] ?? null)); }
170
+ function sameConfig(state, config) { return ["cwd","journal","bin","provider","model","reasoning","mode","projectRoot","ddFlowBin","ddFlowHome","resourceHome","noFlow","livenessTimeoutMs"].every(key => (state.config?.[key] ?? null) === (config[key] ?? null)); }
195
171
  // Called only after authorizeRetainedDaemonResume verifies the old daemon,
196
172
  // provider processes and operation ledger. Native outcomes remain historical;
197
173
  // their old liveness flags cannot describe the replacement process tree.
@@ -209,8 +185,12 @@ export function retainedAgyState(previous, sessionId) {
209
185
  export async function startDaemon(options) {
210
186
  const paths = locations(options.stateDir); await mkdir(paths.dir, { recursive: true, mode: 0o700 }); await chmod(paths.dir, 0o700);
211
187
  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 };
188
+ config.livenessTimeoutMs = options.livenessTimeoutMs ?? 1_800_000;
189
+ validateObservationDuration(config.livenessTimeoutMs);
212
190
  await prepareRuntimeOwner(config, paths.dir);
191
+ assertDaemonRestartable(previous);
213
192
  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; }
193
+ assertDaemonRestartable(await readState(paths.dir));
214
194
  if (previous?.shutdown_state === "clean") await authorizeRetainedDaemonResume(paths.dir, previous, options.sessionId, config);
215
195
  if (previous?.shutdown_state === "running" && previous.active_tree) throw new DaemonError("invalid_harness_crash", "previous Antigravity daemon died with an active or unproven tree");
216
196
  await removeSocket(paths); await prepare(paths, config);
@@ -221,7 +201,7 @@ export async function startDaemon(options) {
221
201
  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();
222
202
  try {
223
203
  await confirmDaemonProcess(config, resourceProcess, child);
224
- const deadline = Date.now() + 30_000; while (Date.now() < deadline) { await new Promise(resolve => setTimeout(resolve, 100));
204
+ const startupClock = new ObservationClock({ timeoutMs: 30_000 }); while (!startupClock.sample()) { await new Promise(resolve => setTimeout(resolve, 100));
225
205
  const persisted = await readState(paths.dir);
226
206
  if (persisted?.shutdown_state === "failed") {
227
207
  const failure = persisted.startup_error ?? {};
@@ -235,13 +215,13 @@ export async function startDaemon(options) {
235
215
  throw error;
236
216
  }
237
217
  }
238
- 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); };
218
+ 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) }); };
239
219
 
240
220
  export class Runtime {
241
221
  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; }
242
- 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.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; }
222
+ 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; }
243
223
  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 }); } }
244
- 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"])]; }
224
+ 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", "0", `--gemini_dir=${this.paths.gemini}`, "--app_data_dir=runtime", ...(sessionId ? ["--conversation", sessionId] : ["--new-project"])]; }
245
225
  requireSessionIdentity(requestedId) {
246
226
  const observedId = this.init?.conversation_id ?? null;
247
227
  if (!requestedId || requestedId === observedId) return observedId;
@@ -249,20 +229,51 @@ export class Runtime {
249
229
  harness: "antigravity-cli", expected_session_id: requestedId, observed_session_id: observedId
250
230
  });
251
231
  }
252
- markActivity() { this.lastActivityAt = new Date().toISOString(); }
232
+ markActivity() { const current = this.active; this.lastActivityAt = new Date().toISOString(); this.progress = { cursor: (this.progress?.cursor ?? 0) + 1, observed_at: Date.now() }; void current?.publishProgress?.('native_progress', { session_id: this.init?.conversation_id, observed_at: this.lastActivityAt }).catch(error => { if(this.active === current) current.reject(error); }); }
233
+ markTerminalActivity(result) {
234
+ const current = this.active;
235
+ if (!current) return;
236
+ const key = createHash("sha256").update(JSON.stringify({ conversation_id: result.conversation_id ?? this.init?.conversation_id, status: result.status, num_turns: result.num_turns, error: result.error, response: result.response ?? result.text })).digest("hex");
237
+ current.terminalProgress ??= new Set();
238
+ if (current.terminalProgress.has(key) || current.terminalProgress.size >= 1_024) return;
239
+ current.terminalProgress.add(key); this.markActivity();
240
+ }
253
241
  observeProviderActivity(event) {
254
- const fingerprint = createHash("sha256").update(JSON.stringify(event)).digest("hex");
255
- if (this.lastProviderEvent === fingerprint) return;
256
- this.lastProviderEvent = fingerprint;
257
- if (["init", "step_update", "result"].includes(event.event)) this.markActivity();
242
+ if (!this.active) return;
243
+ const root = this.init?.conversation_id, step = event.step_update;
244
+ const id = step?.conversation_id ?? event.result?.conversation_id ?? root;
245
+ let parent = id; const seen = new Set();
246
+ while (parent && parent !== root && !seen.has(parent)) { seen.add(parent); const child = this.descendants.get(parent); if (child?.activity_generation !== this.active.generation) return; parent = child.parent_provider_session_id; }
247
+ if (!root || parent !== root) return;
248
+ if (event.event === "result") { if (["SUCCESS", "ERROR", "CANCELLED"].includes(event.result?.status)) this.markTerminalActivity(event.result); return; }
249
+ if (event.event !== "step_update") return;
250
+ const index = step?.step_index ?? (/^\d+$/.test(step?.step_id ?? "") ? Number(step.step_id) : null);
251
+ if (!Number.isInteger(index)) return;
252
+ const floor = this.sessionObservations.get(id)?.step_floor;
253
+ if (floor != null && index <= floor) return;
254
+ const tool = step.tool_call ?? step.tool_info ?? (step.step_type === "tool" ? step.tool_name : null);
255
+ if (!step.content && !step.text && !step.thought && !tool && !step.error && !(step.subagent_info?.subagents?.length)) return;
256
+ const fingerprint = createHash("sha256").update(JSON.stringify({ content: step.content, text: step.text, thought: step.thought, tool_call: step.tool_call, tool_info: step.tool_info, tool_name: step.tool_name, ...(tool ? { state: step.state, status: step.status } : {}), error: step.error, subagent_info: step.subagent_info })).digest("hex");
257
+ this.progressSteps ??= new Map(); const prior = this.progressSteps.get(id);
258
+ if (prior && index < prior.index) return;
259
+ const snapshots = prior?.index === index ? prior.seen : new Set();
260
+ if (snapshots.has(fingerprint)) return;
261
+ // Native step snapshots can alternate/replay; scope retained dedup to the
262
+ // current step. Too many unique snapshots becomes uncertain, never a grant.
263
+ if (snapshots.size >= 1_024) return;
264
+ snapshots.add(fingerprint); this.progressSteps.set(id, { index, seen: snapshots }); this.markActivity();
258
265
  }
259
266
  async start(sessionId = null) {
267
+ if (this.drainingError) throw this.drainingError;
268
+ assertDaemonDispatch(this.paths?.dir);
260
269
  if (this.child && !this.exited) {
261
270
  await this.readyPromise;
262
271
  this.requireSessionIdentity(sessionId);
263
272
  return this.init;
264
273
  }
265
274
  await this.providerFinalization;
275
+ if (this.drainingError) throw this.drainingError;
276
+ assertDaemonDispatch(this.paths?.dir);
266
277
  const target = executable(this.config.bin, this.args(sessionId)); this.exited = false; this.buffer = ""; this.init = null;
267
278
  let readyResolve, readyReject; this.readyPromise = new Promise((resolve, reject) => { readyResolve = resolve; readyReject = reject; });
268
279
  // The provider can reject before durable registration finishes. Keep a
@@ -273,19 +284,20 @@ export class Runtime {
273
284
  const providerProcess = this.providerProcess;
274
285
  try { await this.persist({ provider_process: providerProcess }); }
275
286
  catch (error) { await cleanupFailedStart(this.config, providerProcess, null, error); throw error; }
276
- try { this.child = spawn(target.command, target.args, { cwd: this.config.cwd, env: sanitizedEnv(this.config), detached: true, stdio: ["pipe", "pipe", "pipe"] }); }
287
+ 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"] }); }
277
288
  catch (error) { await cleanupFailedStart(this.config, providerProcess, null, error); throw error; }
278
289
  const providerChild = this.child;
279
290
  let allowProviderFinalization;
280
291
  const providerRegistered = new Promise(resolve => { allowProviderFinalization = resolve; });
281
- 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; } }); });
282
- this.child.stderr.setEncoding("utf8").on("data", chunk => void this.journal("stderr", { text: String(chunk).slice(0, 4000) }));
292
+ const observationFailure = error => { this.drainingError ??= error; readyReject(error); this.active?.reject(error); };
293
+ this.nativeDrain = readNativeLines(this.child.stdout, line => { this.buffer = `${line}\n`; return this.draining = this.drain(readyResolve); }, observationFailure);
294
+ this.diagnostics = streamNativeDiagnostics(this.child.stderr, chunk => this.journal("stderr", { text: chunk }), observationFailure);
283
295
  this.child.on("error", error => { readyReject(error); if (this.active) { this.active.reject(error); this.active = null; } });
284
296
  // A dead leader is not yet a finalized tree: stop must also join the
285
297
  // pending drain, process-group check and final persistence/lease writes.
286
298
  this.providerFinalization = new Promise((resolve, reject) => {
287
299
  this.child.on("close", (code, signal) => {
288
- void this.draining.then(async () => {
300
+ void Promise.all([this.nativeDrain, this.diagnostics]).then(async () => {
289
301
  const interrupted = Boolean(this.active); this.exited = true; const beforeInit = !this.init;
290
302
  const error = this.startupError ?? new DaemonError(beforeInit ? "agy_init_missing" : "agy_terminal_result_missing", `Antigravity exited ${beforeInit ? "before init" : "before result"} (${code ?? signal})`);
291
303
  if (beforeInit) readyReject(error);
@@ -310,15 +322,38 @@ export class Runtime {
310
322
  catch (error) { await cleanupFailedStart(this.config, providerProcess, this.child, error); throw error; }
311
323
  finally { allowProviderFinalization(); }
312
324
  let readyTimer;
313
- try { return await Promise.race([this.readyPromise, new Promise((_, reject) => { readyTimer = setTimeout(() => reject(new DaemonError("agy_init_timeout", "Antigravity init timed out", true)), 30_000); })]); }
325
+ try { return await Promise.race([this.readyPromise, new Promise((_, reject) => { readyTimer = observedTimeout(() => reject(new DaemonError("agy_init_timeout", "Antigravity init timed out", true)), 30_000); })]); }
314
326
  finally { clearTimeout(readyTimer); }
315
327
  }
316
328
  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; }
317
329
  // Record an early provider rejection before journalling: a short-lived
318
330
  // child can close while the asynchronous write is still pending.
319
- 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 });
331
+ if (event.event === "result" && ["ERROR", "CANCELLED"].includes(event.result?.status)) {
332
+ this.lastTerminalFailure ??= this.state.last_terminal_failure;
333
+ const repeated = Boolean(this.lastTerminalFailure && this.lastTerminalFailure.details?.provider_result?.error === event.result.error
334
+ && this.lastTerminalFailure.details.provider_session_id === (event.result.conversation_id || this.init?.conversation_id || null)
335
+ && (this.active?.previousError === event.result.error || this.lastResult?.status === "ERROR" && this.lastResult.error === event.result.error));
336
+ event.terminal_metadata = repeated ? this.lastTerminalFailure.details : { observed_at: new Date().toISOString(), provider_session_id: event.result.conversation_id || this.init?.conversation_id || null,
337
+ operation_id: this.active?.operationId ?? null, turn_generation: this.active?.generation ?? null, journal_locator: this.paths.journal, native_source: "agy.result" };
338
+ if (!this.init) this.startupError ??= agyTerminalFailure(event.result, event.terminal_metadata);
339
+ this.receivedTerminalMetadata = event.terminal_metadata;
340
+ }
320
341
  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 ?? {}); } } }
321
- 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" }); } }
342
+ observeStep(step) {
343
+ const sessionId = step.conversation_id ?? this.init?.conversation_id, observation = this.sessionObservations.get(sessionId) ?? {};
344
+ const fresh = Number.isInteger(step.step_index) && step.step_index > (observation.last_step_index ?? -1) && step.step_index > (observation.step_floor ?? -1);
345
+ const bound = sessionId === this.init?.conversation_id || this.descendants.get(sessionId)?.activity_generation === this.active?.generation;
346
+ // Tree liveness is broader than current-Turn model progress: fresh work
347
+ // from a retained child invalidates settlement without adopting this Turn.
348
+ if (fresh && (sessionId === this.init?.conversation_id || this.descendants.has(sessionId))) this.childActivity(sessionId);
349
+ if (Number.isInteger(step.step_index)) this.sessionObservations.set(sessionId, { ...observation, last_step_index: Math.max(observation.last_step_index ?? -1, step.step_index) });
350
+ this.toolObservations.add(agyToolObservation(step, this.init?.conversation_id));
351
+ for (const child of step.subagent_info?.subagents ?? []) if (child.conversation_id) {
352
+ const prior = this.descendants.get(child.conversation_id);
353
+ 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",
354
+ ...(fresh && bound && this.active ? { activity_generation: this.active.generation } : {}) });
355
+ }
356
+ }
322
357
  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 }; }
323
358
  async captureChildTools() {
324
359
  this.capturedToolFacts ??= new Map();
@@ -340,7 +375,7 @@ export class Runtime {
340
375
  }
341
376
  }
342
377
  }
343
- async finishTurn(result) {
378
+ async finishTurn(result, retainedMetadata = null) {
344
379
  if (result.status === "RUNNING") { await this.persist({ active_tree: true }); return; }
345
380
  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 });
346
381
  if (result.conversation_id && result.conversation_id !== this.init?.conversation_id) {
@@ -353,12 +388,18 @@ export class Runtime {
353
388
  return;
354
389
  }
355
390
  const current = this.active;
391
+ const terminalMetadata = retainedMetadata ?? this.receivedTerminalMetadata ?? (this.lastTerminalFailure && this.lastTerminalFailure.details?.provider_result?.error === result.error
392
+ && this.lastTerminalFailure.details.provider_session_id === (result.conversation_id || this.init?.conversation_id || null)
393
+ && (current?.previousError === result.error || this.lastResult?.status === "ERROR" && this.lastResult.error === result.error) ? this.lastTerminalFailure.details
394
+ : { observed_at: new Date().toISOString(), provider_session_id: result.conversation_id || this.init?.conversation_id || null, operation_id: current?.operationId ?? null,
395
+ turn_generation: current?.generation ?? null, journal_locator: this.paths.journal, native_source: "agy.result" });
396
+ this.receivedTerminalMetadata = null;
356
397
  // The ordered stream returns one result per input. Model usage counters
357
398
  // are not request IDs: native command rejections can report zero turns.
358
399
  // prompt() drains earlier events before accepting another input.
359
400
  if (!current && this.lastResult) { await this.journal("stale_terminal_observed", { result }); return; }
360
401
  await this.captureChildTools();
361
- this.lastResult = result; this.markActivity();
402
+ this.lastResult = result; this.markTerminalActivity(result);
362
403
  const rootStop = this.sessionObservations.get(this.init?.conversation_id)?.stop;
363
404
  const pendingTerminal = current && result.status === "SUCCESS";
364
405
  if (pendingTerminal) current.terminalResult = result;
@@ -371,13 +412,9 @@ export class Runtime {
371
412
  if (this.active !== current) return;
372
413
  if (result.status === "ERROR" || result.status === "CANCELLED") {
373
414
  this.active = null;
374
- const message = String(result.error ?? "Antigravity returned ERROR"); const normalized = message.toLowerCase();
375
- const code = result.status === "CANCELLED" ? "agy_turn_cancelled" : /quota|insufficient.?credit|billing|usage limit/.test(normalized) ? "agy_provider_quota_exhausted"
376
- : /rate.?limit|too many requests/.test(normalized) ? "agy_provider_rate_limited"
377
- : /\b429\b/.test(normalized) ? "agy_provider_limit_unknown"
378
- : /auth|unauthori[sz]ed|forbidden|account/.test(normalized) ? "agy_provider_account"
379
- : "agy_provider_failed";
380
- return current.reject(new DaemonError(code, message, code === "agy_provider_rate_limited", { provider_result: result, provider_error_scope: current.previousError && current.previousError === result.error ? "repeated_conversation_error" : "terminal_result", usage_ingest: this.usageIngest }));
415
+ this.lastTerminalFailure = agyTerminalFailure(result, { ...terminalMetadata, provider_error_scope: current.previousError && current.previousError === result.error ? "repeated_conversation_error" : "terminal_result", usage_ingest: this.usageIngest });
416
+ await this.persist({ last_terminal_failure: errorRecord(this.lastTerminalFailure) });
417
+ return current.reject(this.lastTerminalFailure);
381
418
  }
382
419
  await this.completePendingTurn();
383
420
  }
@@ -395,6 +432,9 @@ export class Runtime {
395
432
  current.resolve(receipt);
396
433
  }
397
434
  receipt(result = this.lastResult) {
435
+ if (this.state.last_terminal_failure && !this.lastTerminalFailure) this.lastTerminalFailure = this.state.last_terminal_failure;
436
+ const terminalMetadata = this.lastTerminalFailure && this.lastTerminalFailure.details?.provider_result?.error === result?.error
437
+ && this.lastTerminalFailure.details.provider_session_id === (result?.conversation_id || this.init?.conversation_id || null) ? this.lastTerminalFailure.details : null;
398
438
  const assistantText = typeof result?.response === "string" ? result.response : typeof result?.text === "string" ? result.text : null;
399
439
  const root = this.init?.conversation_id; const observation = this.sessionObservations.get(root);
400
440
  const treeSettled = [...this.descendants.values()].every(child => child.tree_settled === true || ["completed", "failed", "cancelled", "settled_by_root"].includes(child.status));
@@ -403,13 +443,14 @@ export class Runtime {
403
443
  // non-idle Stop still wins, and every discovered child must be terminal.
404
444
  const rootSettled = observation?.stop?.fullyIdle !== false;
405
445
  const terminalPending = this.active?.terminalResult === result;
406
- 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.state.unclaimed_activity && (!this.active || terminalPending) && result && ["SUCCESS", "ERROR", "CANCELLED"].includes(result.status) && treeSettled && rootSettled) };
446
+ 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, ...(terminalMetadata ? { terminal_metadata: terminalMetadata } : {}), ...(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.state.unclaimed_activity && (!this.active || terminalPending) && result && ["SUCCESS", "ERROR", "CANCELLED"].includes(result.status) && treeSettled && rootSettled) };
407
447
  }
408
448
  async zeroUsage() { await forwardUsage(this.config, this.init.conversation_id, { usage: {} }, this.toolSnapshot(), []); }
409
- async prompt(text, assertDispatch = captureDispatchGuard(this), operationId = null) {
449
+ async prompt(text, assertDispatch = captureDispatchGuard(this), operationId = null, livenessTimeoutMs = 1_800_000) {
450
+ validateObservationDuration(livenessTimeoutMs);
410
451
  if (!text) throw new DaemonError("prompt_required", "prompt is required");
411
452
  await this.start(); await this.draining;
412
- if (this.active) throw new DaemonError("operation_in_progress", "an Antigravity turn is already running");
453
+ if (this.active || this.promptObservation) throw new DaemonError("operation_in_progress", "an Antigravity turn or its mandatory dispatch observation is already running");
413
454
  if (this.state.last_hook_rejection && !this.receipt().settled) throw new DaemonError("tree_not_settled", "Rejected hook does not prove that the prior Antigravity tree stopped", false, { primary_error: this.state.last_hook_rejection });
414
455
  if (this.state.unclaimed_activity || (this.lastResult && !this.receipt().settled)) throw new DaemonError("tree_not_settled", "the prior Antigravity agent tree is not fully settled");
415
456
  let resolveTurn, rejectTurn;
@@ -417,27 +458,71 @@ export class Runtime {
417
458
  // Cancellation can reject while admission persistence is still pending.
418
459
  void reply.catch(() => {});
419
460
  const current = { resolve: resolveTurn, reject: rejectTurn, generation: ++this.turnGeneration, operationId, previousError: this.lastResult?.error ?? null };
461
+ let resolveDispatchJournal;
462
+ const dispatchJournal = current.dispatchJournal = new Promise(resolve => { resolveDispatchJournal = resolve; });
463
+ this.promptObservation = current;
464
+ const finishDispatchJournal = () => {
465
+ current.dispatchJournalSettled = true; resolveDispatchJournal();
466
+ if (current.observerReturned && this.promptObservation === current) this.promptObservation = null;
467
+ };
420
468
  // Reserve before persistence so different operation IDs cannot overlap.
469
+ current.publishProgress = operationProgressReporter();
421
470
  this.active = current;
422
471
  this.hookRequests = new Map();
423
472
  const root = this.init?.conversation_id, prior = this.sessionObservations.get(root) ?? {};
424
473
  this.sessionObservations.set(root, { ...prior, step_floor: prior.last_step_index ?? null, stop: null, last_hook: null });
425
- this.lastProviderEvent = null;
474
+ this.lastProviderEvent = null; this.progressSteps = new Map(); this.progressHooks = new Set(); this.progress = undefined;
426
475
  try {
427
476
  await this.persist({ active_tree: true, last_hook_rejection: null });
428
477
  assertDispatch();
478
+ assertDaemonDispatch(this.paths?.dir);
429
479
  if (this.active !== current) throw new DaemonError("operation_cancelled", "prompt reservation was superseded");
480
+ current.dispatched = true;
430
481
  this.child.stdin.write(`${JSON.stringify({ event: "user", message: { content: text } })}\n`, error => {
431
- if (error) { if (this.active === current) this.active = null; current.reject(error); }
482
+ if (error) { this.drainingError ??= nativeObservationError(error); current.dispatchObservationError = this.drainingError; current.reject(this.drainingError); finishDispatchJournal(); }
483
+ else void Promise.resolve().then(() => this.journal("native_dispatch", { operation_id: operationId, provider_session_id: this.init?.conversation_id, native_request_id: current.generation })).then(finishDispatchJournal, cause => {
484
+ this.drainingError ??= nativeObservationError(cause); current.dispatchObservationError = this.drainingError;
485
+ current.reject(this.drainingError); finishDispatchJournal();
486
+ });
432
487
  });
433
- } catch (error) { if (this.active === current) this.active = null; current.reject(error); }
434
- return await reply;
488
+ } catch (error) {
489
+ if (current.dispatched) { this.drainingError ??= nativeObservationError(error); error = this.drainingError; }
490
+ else if (this.active === current) this.active = null;
491
+ current.reject(error); finishDispatchJournal();
492
+ }
493
+ const clock = new ObservationClock({ timeoutMs: livenessTimeoutMs, onGap: gap => { void this.journal("observation_gap", { gap, operation_id: operationId }).catch(() => {}); } });
494
+ let checking = false;
495
+ const timer = setInterval(() => {
496
+ if (this.active !== current || checking || !clock.sample(this.progress)) return;
497
+ checking = true; const cursor = this.progress?.cursor;
498
+ void this.completePendingTurn().then(() => {
499
+ if (this.active !== current || this.progress?.cursor !== cursor || !clock.sample(this.progress)) return;
500
+ current.reject(new DaemonError("operation_observation_lost", "Antigravity native outcome is unknown after the productive silence window", true, { operation_id: operationId, turn_generation: current.generation, native_outcome: "unknown", last_activity_at: this.lastActivityAt ?? null }));
501
+ }).catch(error => {
502
+ if (this.active === current && this.progress?.cursor === cursor) current.reject(new DaemonError("operation_observation_lost", "Antigravity quiet reconciliation could not confirm the native outcome", true, { operation_id: operationId, turn_generation: current.generation, native_outcome: "unknown", observation_error: errorRecord(error) }));
503
+ }).finally(() => { checking = false; });
504
+ }, Math.max(10, Math.min(1_000, livenessTimeoutMs / 4)));
505
+ try {
506
+ const result = await reply;
507
+ await dispatchJournal;
508
+ if (current.dispatchObservationError) {
509
+ const error = current.dispatchObservationError;
510
+ if (error.code !== "operation_observation_lost") throw error;
511
+ throw Object.assign(new Error("Antigravity completed natively but its dispatch journal could not be saved", { cause: error.cause ?? error }), {
512
+ code: "native_outcome_observation_failed", details: { native_outcome: result, settled: false, observation_error: errorRecord(error) }
513
+ });
514
+ }
515
+ return result;
516
+ } finally {
517
+ clearInterval(timer); current.observerReturned = true;
518
+ if (current.dispatchJournalSettled && this.promptObservation === current) this.promptObservation = null;
519
+ }
435
520
  }
436
521
  childActivity(sessionId) {
437
522
  if (sessionId === this.init?.conversation_id && !this.active) this.state.unclaimed_activity = true;
438
523
  const child = this.descendants.get(sessionId);
439
524
  if (!child) return;
440
- this.descendants.set(sessionId, { ...child, previous_terminal_status: child.status === "unknown" ? child.previous_terminal_status : child.status, status: "unknown", tree_settled: false, settlement_evidence: null, settlement_daemon_id: null, activity_generation: this.turnGeneration });
525
+ this.descendants.set(sessionId, { ...child, previous_terminal_status: child.status === "unknown" ? child.previous_terminal_status : child.status, status: "unknown", tree_settled: false, settlement_evidence: null, settlement_daemon_id: null });
441
526
  }
442
527
  async childForHook(conversationId) {
443
528
  if (conversationId === this.init?.conversation_id) return null;
@@ -459,8 +544,7 @@ export class Runtime {
459
544
  const correction = { code: error.code, message: error.message, details: error.details, event, conversation_id: payload.conversationId ?? null,
460
545
  daemon_id: this.config.daemonId, operation_id: current.operationId, turn_generation: current.generation };
461
546
  try {
462
- await this.journal("hook_correction", { correction });
463
- await this.persist({ last_hook_correction: correction });
547
+ await hookBudget(1_000, null).run(async () => { await this.journal("hook_correction", { correction }); await this.persist({ last_hook_correction: correction }); }, "correction_persistence");
464
548
  current.hookCorrectionGeneration = current.generation;
465
549
  return;
466
550
  } catch (storage) { error = Object.assign(new Error("Could not persist native hook correction"), { code: "native_hook_receipt_invalid", details: { effect: "unknown", primary_error: correction, receipt_error: storage.message } }); }
@@ -468,7 +552,7 @@ export class Runtime {
468
552
  const rejection = this.state.last_hook_rejection && this.state.last_hook_rejection.turn_generation === current?.generation ? this.state.last_hook_rejection : { code: error.code ?? "agy_hook_rejected", message: error.message ?? String(error), details: error.details, event, conversation_id: payload.conversationId ?? null, tool: payload.toolCall?.name ?? null, daemon_id: this.config.daemonId, operation_id: current?.operationId ?? null, turn_generation: current?.generation ?? null };
469
553
  this.state.last_hook_rejection = rejection;
470
554
  if (current && this.active === current) { this.active = null; current.reject(new DaemonError(rejection.code, rejection.message, false, rejection)); }
471
- try { await this.journal("hook_rejected", { rejection }); await this.persist({ last_hook_rejection: rejection }); }
555
+ try { await hookBudget(1_000, null).run(async () => { await this.journal("hook_rejected", { rejection }); await this.persist({ last_hook_rejection: rejection }); }, "failure_persistence"); }
472
556
  catch (storageError) { rejection.receipt_error = { code: storageError.code, message: storageError.message }; }
473
557
  }
474
558
  async observeHookFailures() {
@@ -482,26 +566,49 @@ export class Runtime {
482
566
  await this.rejectHook(Object.assign(new Error(failure.message ?? "Native hook failed"), { code: failure.code, details: failure.details }), binding.event, binding.payload, binding.current);
483
567
  }
484
568
  }
485
- 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;
569
+ 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"); this.progressHooks ??= new Set(); const scoped = this.active && (!child || child.activity_generation === this.active.generation); const progressKey = JSON.stringify([conversationId, step, execution, event, payload.toolCall ?? null, payload.fullyIdle ?? null]); if (scoped && step !== null && (["PreToolUse", "PostToolUse"].includes(event) || event === "Stop" && payload.fullyIdle === true) && !this.progressHooks.has(progressKey) && this.progressHooks.size < 1_024) { this.progressHooks.add(progressKey); 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;
486
570
  const observed = payload.model ? observedProfile(payload) : {};
487
571
  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" });
488
572
  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 };
489
573
  }
490
574
  async finalizeProviderForControl() {
491
575
  const pending = this.providerFinalization;
492
- try { await pending; }
493
- catch (error) {
494
- // A leaderless group may have exited naturally since the last check.
495
- // Only control may recheck it; startup never retries failed cleanup.
496
- if (this.providerFinalization !== pending) return await this.providerFinalization;
497
- if (error.code !== "process_group_ownership_unknown" || !this.retryProviderFinalization) throw error;
498
- this.providerFinalization = this.retryProviderFinalization(5_000);
499
- await this.providerFinalization;
576
+ try {
577
+ try { await pending; }
578
+ catch (error) {
579
+ // Control retries only retained physical/resource cleanup, never a
580
+ // missing mandatory stream record or a productive provider request.
581
+ if (this.providerFinalization !== pending) await this.providerFinalization;
582
+ else {
583
+ if (!this.retryProviderFinalization) throw error;
584
+ this.providerFinalization = this.retryProviderFinalization(5_000);
585
+ await this.providerFinalization;
586
+ }
587
+ }
588
+ await this.promptObservation?.dispatchJournal;
589
+ } catch (error) {
590
+ if (!this.drainingError) throw error;
591
+ this.drainingError.cleanup_error = errorRecord(error);
592
+ this.drainingError.details = { ...this.drainingError.details, cleanup_error: this.drainingError.cleanup_error };
593
+ throw this.drainingError;
500
594
  }
595
+ if (this.drainingError) throw this.drainingError;
596
+ }
597
+ async prepareStop(cancelTree) {
598
+ const current = this.active, priorResult = this.lastResult;
599
+ const emptyTree = !current && !this.state.active_tree && !this.state.unclaimed_activity && !priorResult && !this.descendants.size;
600
+ if (cancelTree) await this.cancel();
601
+ 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");
602
+ // A physical kill is settlement, not a native clean terminal receipt.
603
+ // In particular, the previous Turn's SUCCESS cannot certify a cancelled
604
+ // in-flight Turn that exited without its own result.
605
+ const currentResult = !current || current.terminalResult === this.lastResult || this.lastResult !== priorResult;
606
+ const clean = emptyTree || (currentResult && this.receipt().settled);
607
+ return { clean, ...(clean ? {} : { native_settlement: { confirmed: false, reason: "native_terminal_unconfirmed" } }) };
501
608
  }
502
609
  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) };
503
610
  }
504
- 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; }
611
+ 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(); }
505
612
  async abortProvider(reason) {
506
613
  const child = this.child, providerProcess = this.providerProcess;
507
614
  await stopProcessGroup(child);
@@ -514,7 +621,27 @@ export class Runtime {
514
621
  export async function serveDaemon(stateDir) {
515
622
  const paths = locations(stateDir), state = await readState(paths.dir); if (!state?.config) throw new DaemonError("daemon_state_missing", "Antigravity daemon state is missing");
516
623
  scheduleDaemonProcessHeartbeat(state.config, state.resource_process);
517
- 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;
624
+ const runtime = new Runtime(paths, state); await removeSocket(paths);
625
+ const server = net.createServer(socket => {
626
+ let buffer = "", consumed = false; socket.setEncoding("utf8");
627
+ socket.on("data", chunk => {
628
+ if (consumed) return;
629
+ buffer += chunk; if (!admitNativeRequestBuffer(buffer, socket, RESPONSE_SCHEMA)) { consumed = true; return; } const at = buffer.indexOf("\n"); if (at < 0) return; consumed = true;
630
+ void (async () => {
631
+ let request;
632
+ try {
633
+ request = JSON.parse(buffer.slice(0, at));
634
+ const result = await durableDaemonDispatch(paths.dir, request, () => dispatch(runtime, request), () => dispatch(runtime, { operation: "session.inspect", params: request.params ?? {} }));
635
+ if (result._shutdown) retireDaemonEndpoint(server);
636
+ socket.end(`${JSON.stringify({ schema_id: RESPONSE_SCHEMA, id: request.id, ok: true, result: { ...result, _shutdown: undefined } })}\n`);
637
+ } catch (error) {
638
+ await retireFailedDaemon(server, state, error, patch => runtime.persist(patch));
639
+ socket.end(`${JSON.stringify({ schema_id: RESPONSE_SCHEMA, id: request?.id ?? null, ok: false, error: errorPayload(error) })}\n`);
640
+ }
641
+ })();
642
+ });
643
+ });
644
+ trackDaemonConnections(server); runtime.server = server;
518
645
  await new Promise((resolve, reject) => { server.once("error", reject); server.listen(paths.socket, resolve); }); await chmod(paths.socket, 0o600);
519
646
  try { await runtime.start(state.config.resumeSessionId); runtime.requireSessionIdentity(state.config.resumeSessionId); }
520
647
  catch (error) {
@@ -528,9 +655,10 @@ export async function serveDaemon(stateDir) {
528
655
 
529
656
  async function dispatch(runtime, request) {
530
657
  if (request.schema_id !== REQUEST_SCHEMA) throw new DaemonError("daemon_protocol_mismatch", "unsupported dd-agy request schema"); const params = request.params ?? {};
658
+ if (["session.create", "session.prompt", "session.start", "session.fork", "session.resume"].includes(request.operation)) assertDaemonDispatch(runtime.paths.dir);
531
659
  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() }; }
532
660
  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(); }
533
- 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); }
661
+ 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, params.livenessTimeoutMs ?? runtime.config.livenessTimeoutMs ?? 1_800_000); }
534
662
  if (request.operation === "session.inspect") { runtime.requireSessionIdentity(params.sessionId); await runtime.captureChildTools(); return runtime.receipt(); }
535
663
  if (request.operation === "session.resume") { await runtime.start(params.sessionId ?? runtime.init?.conversation_id ?? null); runtime.requireSessionIdentity(params.sessionId); return runtime.receipt(); }
536
664
  if (request.operation === "session.cancel") { runtime.requireSessionIdentity(params.sessionId); return await runtime.cancel(); }
@@ -557,7 +685,8 @@ async function dispatch(runtime, request) {
557
685
  }
558
686
  catch (error) { await runtime.rejectHook(error, params.event, params.payload ?? {}, current); throw error; }
559
687
  }
560
- 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 }; }
688
+ 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()]]);
561
689
  throw new DaemonError("unknown_operation", `unknown dd-agy daemon operation: ${request.operation}`);
562
690
  }
563
691
  import { authorizeRetainedDaemonResume } from "./driver-recovery.mjs";
692
+ import { commandText } from "./process-json.mjs";