@deksden-com/dd-flow-cli 0.9.0-beta.124 → 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 (113) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +10 -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/harness-runtime/bin/dd-agy.mjs +3 -2
  14. package/dist/harness-runtime/bin/dd-codex.mjs +3 -2
  15. package/dist/harness-runtime/bin/dd-droid.mjs +3 -2
  16. package/dist/harness-runtime/bin/dd-grok.mjs +16 -6
  17. package/dist/harness-runtime/bin/dd-opencode.mjs +3 -2
  18. package/dist/harness-runtime/bin/dd-zcode.mjs +20 -16
  19. package/dist/harness-runtime/lib/adapter-timeouts.d.mts +5 -0
  20. package/dist/harness-runtime/lib/adapter-timeouts.mjs +34 -0
  21. package/dist/harness-runtime/lib/daemon-observation.d.mts +2 -0
  22. package/dist/harness-runtime/lib/daemon-observation.mjs +163 -0
  23. package/dist/harness-runtime/lib/daemon-operations.mjs +60 -13
  24. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +151 -72
  25. package/dist/harness-runtime/lib/dd-agy.mjs +11 -23
  26. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +9 -14
  27. package/dist/harness-runtime/lib/dd-codex.mjs +252 -80
  28. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +14 -19
  29. package/dist/harness-runtime/lib/dd-droid.mjs +117 -31
  30. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +56 -30
  31. package/dist/harness-runtime/lib/dd-grok.mjs +15 -13
  32. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +18 -27
  33. package/dist/harness-runtime/lib/dd-opencode.mjs +44 -14
  34. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +56 -53
  35. package/dist/harness-runtime/lib/dd-zcode.mjs +200 -111
  36. package/dist/harness-runtime/lib/lease-renewal.d.mts +8 -0
  37. package/dist/harness-runtime/lib/lease-renewal.mjs +23 -0
  38. package/dist/harness-runtime/lib/lifecycle-command.d.mts +3 -3
  39. package/dist/harness-runtime/lib/lifecycle-command.mjs +74 -20
  40. package/dist/harness-runtime/lib/managed-daemon.d.mts +10 -1
  41. package/dist/harness-runtime/lib/managed-daemon.mjs +228 -40
  42. package/dist/harness-runtime/lib/native-children.d.mts +13 -0
  43. package/dist/harness-runtime/lib/native-children.mjs +78 -0
  44. package/dist/harness-runtime/lib/native-hook-command.d.mts +4 -0
  45. package/dist/harness-runtime/lib/native-hook-command.mjs +115 -11
  46. package/dist/harness-runtime/lib/native-streams.mjs +45 -0
  47. package/dist/harness-runtime/lib/observation-clock.d.mts +18 -0
  48. package/dist/harness-runtime/lib/observation-clock.mjs +73 -17
  49. package/dist/harness-runtime/lib/operation-errors.d.mts +14 -0
  50. package/dist/harness-runtime/lib/operation-errors.mjs +105 -8
  51. package/dist/harness-runtime/lib/operation-progress.d.mts +7 -0
  52. package/dist/harness-runtime/lib/operation-progress.mjs +112 -0
  53. package/dist/harness-runtime/lib/process-json.mjs +136 -38
  54. package/dist/harness-runtime/lib/tool-observations.d.mts +1 -0
  55. package/dist/harness-runtime/lib/tool-observations.mjs +5 -2
  56. package/dist/harness-runtime/lib/zcode-usage.mjs +3 -2
  57. package/dist/runtime/context.js +8 -4
  58. package/dist/schemas/run-control-receipt.schema.json +19 -0
  59. package/dist/services/code-checks.js +79 -9
  60. package/dist/services/codex-capacity.js +117 -41
  61. package/dist/services/codex-hook-delivery.js +2 -2
  62. package/dist/services/compatibility-preflight.js +4 -1
  63. package/dist/services/controller-fanout.js +9 -76
  64. package/dist/services/engines.js +70 -20
  65. package/dist/services/eval-snapshots.js +179 -40
  66. package/dist/services/external-work-launch.js +13 -8
  67. package/dist/services/harness-adapter.js +175 -11
  68. package/dist/services/hooks.js +65 -21
  69. package/dist/services/lifecycle-invocations.js +404 -32
  70. package/dist/services/managed-lease-monitor.js +128 -0
  71. package/dist/services/managed-processes.js +103 -47
  72. package/dist/services/merge-server.js +58 -21
  73. package/dist/services/recovery-observation-budget.js +67 -7
  74. package/dist/services/recovery-snapshot-database.js +7 -4
  75. package/dist/services/run-control-receipt.js +1 -1
  76. package/dist/services/run-control-worker.js +177 -118
  77. package/dist/services/run-control.js +99 -8
  78. package/dist/services/run-controller-adapter.js +169 -46
  79. package/dist/services/run-controller-capture.js +14 -2
  80. package/dist/services/run-controller-process.js +11 -4
  81. package/dist/services/run-controller-state.js +68 -7
  82. package/dist/services/run-controller.js +166 -94
  83. package/dist/services/run-fork.js +8 -3
  84. package/dist/services/run-recovery-guard.js +75 -0
  85. package/dist/services/run-recovery-runtime.js +1 -56
  86. package/dist/services/run-recovery.js +17 -13
  87. package/dist/services/runs.js +39 -6
  88. package/dist/services/runtime-budget.js +32 -15
  89. package/dist/services/runtime-scope-control.js +8 -0
  90. package/dist/services/runtime-scope-resume.js +30 -8
  91. package/dist/services/runtime-scope-worker.js +118 -73
  92. package/dist/services/runtime-service.js +6 -7
  93. package/dist/services/scope-writer-cleanup.js +3 -2
  94. package/dist/services/stage-context.js +135 -0
  95. package/dist/services/stage-lifecycle.js +3 -15
  96. package/dist/services/stage-pause.js +3 -2
  97. package/dist/services/usage.js +14 -3
  98. package/dist/services/vnext-code-review.js +10 -9
  99. package/dist/services/vnext-code.js +10 -8
  100. package/dist/services/vnext-contracts.js +10 -1
  101. package/dist/services/vnext-execution-profile.js +75 -2
  102. package/dist/services/vnext-fanout.js +5 -0
  103. package/dist/services/vnext-merge.js +10 -9
  104. package/dist/services/vnext-plan-review.js +21 -17
  105. package/dist/services/vnext-plan.js +9 -7
  106. package/dist/services/vnext-protocolize.js +15 -9
  107. package/dist/services/vnext-specify.js +6 -6
  108. package/dist/services/work-registry.js +34 -4
  109. package/dist/services/workspace-bootstrap.js +14 -3
  110. package/dist/shared/errors.js +11 -6
  111. package/dist/storage/agent-output.js +26 -0
  112. package/dist/storage/database.js +80 -21
  113. 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
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,6 +185,8 @@ 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);
213
191
  assertDaemonRestartable(previous);
214
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; }
@@ -223,7 +201,7 @@ export async function startDaemon(options) {
223
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();
224
202
  try {
225
203
  await confirmDaemonProcess(config, resourceProcess, child);
226
- 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));
227
205
  const persisted = await readState(paths.dir);
228
206
  if (persisted?.shutdown_state === "failed") {
229
207
  const failure = persisted.startup_error ?? {};
@@ -243,7 +221,7 @@ export class Runtime {
243
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; }
244
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; }
245
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 }); } }
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"])]; }
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"])]; }
247
225
  requireSessionIdentity(requestedId) {
248
226
  const observedId = this.init?.conversation_id ?? null;
249
227
  if (!requestedId || requestedId === observedId) return observedId;
@@ -251,12 +229,39 @@ export class Runtime {
251
229
  harness: "antigravity-cli", expected_session_id: requestedId, observed_session_id: observedId
252
230
  });
253
231
  }
254
- 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
+ }
255
241
  observeProviderActivity(event) {
256
- const fingerprint = createHash("sha256").update(JSON.stringify(event)).digest("hex");
257
- if (this.lastProviderEvent === fingerprint) return;
258
- this.lastProviderEvent = fingerprint;
259
- 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();
260
265
  }
261
266
  async start(sessionId = null) {
262
267
  if (this.drainingError) throw this.drainingError;
@@ -284,14 +289,15 @@ export class Runtime {
284
289
  const providerChild = this.child;
285
290
  let allowProviderFinalization;
286
291
  const providerRegistered = new Promise(resolve => { allowProviderFinalization = resolve; });
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; } }); });
288
- 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);
289
295
  this.child.on("error", error => { readyReject(error); if (this.active) { this.active.reject(error); this.active = null; } });
290
296
  // A dead leader is not yet a finalized tree: stop must also join the
291
297
  // pending drain, process-group check and final persistence/lease writes.
292
298
  this.providerFinalization = new Promise((resolve, reject) => {
293
299
  this.child.on("close", (code, signal) => {
294
- void this.draining.then(async () => {
300
+ void Promise.all([this.nativeDrain, this.diagnostics]).then(async () => {
295
301
  const interrupted = Boolean(this.active); this.exited = true; const beforeInit = !this.init;
296
302
  const error = this.startupError ?? new DaemonError(beforeInit ? "agy_init_missing" : "agy_terminal_result_missing", `Antigravity exited ${beforeInit ? "before init" : "before result"} (${code ?? signal})`);
297
303
  if (beforeInit) readyReject(error);
@@ -316,15 +322,38 @@ export class Runtime {
316
322
  catch (error) { await cleanupFailedStart(this.config, providerProcess, this.child, error); throw error; }
317
323
  finally { allowProviderFinalization(); }
318
324
  let readyTimer;
319
- 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); })]); }
320
326
  finally { clearTimeout(readyTimer); }
321
327
  }
322
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; }
323
329
  // Record an early provider rejection before journalling: a short-lived
324
330
  // child can close while the asynchronous write is still pending.
325
- 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
+ }
326
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 ?? {}); } } }
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" }); } }
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
+ }
328
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 }; }
329
358
  async captureChildTools() {
330
359
  this.capturedToolFacts ??= new Map();
@@ -346,7 +375,7 @@ export class Runtime {
346
375
  }
347
376
  }
348
377
  }
349
- async finishTurn(result) {
378
+ async finishTurn(result, retainedMetadata = null) {
350
379
  if (result.status === "RUNNING") { await this.persist({ active_tree: true }); return; }
351
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 });
352
381
  if (result.conversation_id && result.conversation_id !== this.init?.conversation_id) {
@@ -359,12 +388,18 @@ export class Runtime {
359
388
  return;
360
389
  }
361
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;
362
397
  // The ordered stream returns one result per input. Model usage counters
363
398
  // are not request IDs: native command rejections can report zero turns.
364
399
  // prompt() drains earlier events before accepting another input.
365
400
  if (!current && this.lastResult) { await this.journal("stale_terminal_observed", { result }); return; }
366
401
  await this.captureChildTools();
367
- this.lastResult = result; this.markActivity();
402
+ this.lastResult = result; this.markTerminalActivity(result);
368
403
  const rootStop = this.sessionObservations.get(this.init?.conversation_id)?.stop;
369
404
  const pendingTerminal = current && result.status === "SUCCESS";
370
405
  if (pendingTerminal) current.terminalResult = result;
@@ -377,13 +412,9 @@ export class Runtime {
377
412
  if (this.active !== current) return;
378
413
  if (result.status === "ERROR" || result.status === "CANCELLED") {
379
414
  this.active = null;
380
- const message = String(result.error ?? "Antigravity returned ERROR"); const normalized = message.toLowerCase();
381
- const code = result.status === "CANCELLED" ? "agy_turn_cancelled" : /quota|insufficient.?credit|billing|usage limit/.test(normalized) ? "agy_provider_quota_exhausted"
382
- : /rate.?limit|too many requests/.test(normalized) ? "agy_provider_rate_limited"
383
- : /\b429\b/.test(normalized) ? "agy_provider_limit_unknown"
384
- : /auth|unauthori[sz]ed|forbidden|account/.test(normalized) ? "agy_provider_account"
385
- : "agy_provider_failed";
386
- 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);
387
418
  }
388
419
  await this.completePendingTurn();
389
420
  }
@@ -401,6 +432,9 @@ export class Runtime {
401
432
  current.resolve(receipt);
402
433
  }
403
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;
404
438
  const assistantText = typeof result?.response === "string" ? result.response : typeof result?.text === "string" ? result.text : null;
405
439
  const root = this.init?.conversation_id; const observation = this.sessionObservations.get(root);
406
440
  const treeSettled = [...this.descendants.values()].every(child => child.tree_settled === true || ["completed", "failed", "cancelled", "settled_by_root"].includes(child.status));
@@ -409,13 +443,14 @@ export class Runtime {
409
443
  // non-idle Stop still wins, and every discovered child must be terminal.
410
444
  const rootSettled = observation?.stop?.fullyIdle !== false;
411
445
  const terminalPending = this.active?.terminalResult === result;
412
- 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) };
413
447
  }
414
448
  async zeroUsage() { await forwardUsage(this.config, this.init.conversation_id, { usage: {} }, this.toolSnapshot(), []); }
415
- async prompt(text, assertDispatch = captureDispatchGuard(this), operationId = null) {
449
+ async prompt(text, assertDispatch = captureDispatchGuard(this), operationId = null, livenessTimeoutMs = 1_800_000) {
450
+ validateObservationDuration(livenessTimeoutMs);
416
451
  if (!text) throw new DaemonError("prompt_required", "prompt is required");
417
452
  await this.start(); await this.draining;
418
- 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");
419
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 });
420
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");
421
456
  let resolveTurn, rejectTurn;
@@ -423,28 +458,71 @@ export class Runtime {
423
458
  // Cancellation can reject while admission persistence is still pending.
424
459
  void reply.catch(() => {});
425
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
+ };
426
468
  // Reserve before persistence so different operation IDs cannot overlap.
469
+ current.publishProgress = operationProgressReporter();
427
470
  this.active = current;
428
471
  this.hookRequests = new Map();
429
472
  const root = this.init?.conversation_id, prior = this.sessionObservations.get(root) ?? {};
430
473
  this.sessionObservations.set(root, { ...prior, step_floor: prior.last_step_index ?? null, stop: null, last_hook: null });
431
- this.lastProviderEvent = null;
474
+ this.lastProviderEvent = null; this.progressSteps = new Map(); this.progressHooks = new Set(); this.progress = undefined;
432
475
  try {
433
476
  await this.persist({ active_tree: true, last_hook_rejection: null });
434
477
  assertDispatch();
435
478
  assertDaemonDispatch(this.paths?.dir);
436
479
  if (this.active !== current) throw new DaemonError("operation_cancelled", "prompt reservation was superseded");
480
+ current.dispatched = true;
437
481
  this.child.stdin.write(`${JSON.stringify({ event: "user", message: { content: text } })}\n`, error => {
438
- 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
+ });
439
487
  });
440
- } catch (error) { if (this.active === current) this.active = null; current.reject(error); }
441
- 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
+ }
442
520
  }
443
521
  childActivity(sessionId) {
444
522
  if (sessionId === this.init?.conversation_id && !this.active) this.state.unclaimed_activity = true;
445
523
  const child = this.descendants.get(sessionId);
446
524
  if (!child) return;
447
- 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 });
448
526
  }
449
527
  async childForHook(conversationId) {
450
528
  if (conversationId === this.init?.conversation_id) return null;
@@ -466,8 +544,7 @@ export class Runtime {
466
544
  const correction = { code: error.code, message: error.message, details: error.details, event, conversation_id: payload.conversationId ?? null,
467
545
  daemon_id: this.config.daemonId, operation_id: current.operationId, turn_generation: current.generation };
468
546
  try {
469
- await this.journal("hook_correction", { correction });
470
- 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");
471
548
  current.hookCorrectionGeneration = current.generation;
472
549
  return;
473
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 } }); }
@@ -475,7 +552,7 @@ export class Runtime {
475
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 };
476
553
  this.state.last_hook_rejection = rejection;
477
554
  if (current && this.active === current) { this.active = null; current.reject(new DaemonError(rejection.code, rejection.message, false, rejection)); }
478
- 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"); }
479
556
  catch (storageError) { rejection.receipt_error = { code: storageError.code, message: storageError.message }; }
480
557
  }
481
558
  async observeHookFailures() {
@@ -489,7 +566,7 @@ export class Runtime {
489
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);
490
567
  }
491
568
  }
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;
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;
493
570
  const observed = payload.model ? observedProfile(payload) : {};
494
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" });
495
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 };
@@ -508,6 +585,7 @@ export class Runtime {
508
585
  await this.providerFinalization;
509
586
  }
510
587
  }
588
+ await this.promptObservation?.dispatchJournal;
511
589
  } catch (error) {
512
590
  if (!this.drainingError) throw error;
513
591
  this.drainingError.cleanup_error = errorRecord(error);
@@ -548,7 +626,7 @@ export async function serveDaemon(stateDir) {
548
626
  let buffer = "", consumed = false; socket.setEncoding("utf8");
549
627
  socket.on("data", chunk => {
550
628
  if (consumed) return;
551
- buffer += chunk; const at = buffer.indexOf("\n"); if (at < 0) return; consumed = true;
629
+ buffer += chunk; if (!admitNativeRequestBuffer(buffer, socket, RESPONSE_SCHEMA)) { consumed = true; return; } const at = buffer.indexOf("\n"); if (at < 0) return; consumed = true;
552
630
  void (async () => {
553
631
  let request;
554
632
  try {
@@ -580,7 +658,7 @@ async function dispatch(runtime, request) {
580
658
  if (["session.create", "session.prompt", "session.start", "session.fork", "session.resume"].includes(request.operation)) assertDaemonDispatch(runtime.paths.dir);
581
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() }; }
582
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(); }
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); }
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); }
584
662
  if (request.operation === "session.inspect") { runtime.requireSessionIdentity(params.sessionId); await runtime.captureChildTools(); return runtime.receipt(); }
585
663
  if (request.operation === "session.resume") { await runtime.start(params.sessionId ?? runtime.init?.conversation_id ?? null); runtime.requireSessionIdentity(params.sessionId); return runtime.receipt(); }
586
664
  if (request.operation === "session.cancel") { runtime.requireSessionIdentity(params.sessionId); return await runtime.cancel(); }
@@ -611,3 +689,4 @@ async function dispatch(runtime, request) {
611
689
  throw new DaemonError("unknown_operation", `unknown dd-agy daemon operation: ${request.operation}`);
612
690
  }
613
691
  import { authorizeRetainedDaemonResume } from "./driver-recovery.mjs";
692
+ import { commandText } from "./process-json.mjs";
@@ -1,11 +1,8 @@
1
- import { spawn } from "node:child_process";
2
1
  import { mkdtemp, rm } from "node:fs/promises";
3
2
  import os from "node:os";
4
3
  import path from "node:path";
5
4
 
6
5
  import { checkObservedProfile } from "./model-observations.mjs";
7
- import { stopProcessGroup } from "./managed-daemon.mjs";
8
- import { errorRecord } from "./operation-errors.mjs";
9
6
 
10
7
  const DEFAULT_MODEL = "gemini-3.1-pro-high";
11
8
  export const AGY_VERSION_TIMEOUT_MS = 30_000;
@@ -14,31 +11,21 @@ export class AgyError extends Error {
14
11
  constructor(code, message, retryable = false, details) { super(message); this.code = code; this.retryable = retryable; this.details = details; }
15
12
  }
16
13
 
14
+ export function agyTerminalFailure(result, metadata = {}) {
15
+ const message = String(result.error ?? "Antigravity returned ERROR"), normalized = message.toLowerCase();
16
+ const code = result.status === "CANCELLED" ? "agy_turn_cancelled" : /quota|insufficient.?credit|billing|usage limit/.test(normalized) ? "agy_provider_quota_exhausted"
17
+ : /rate.?limit|too many requests/.test(normalized) ? "agy_provider_rate_limited" : /\b429\b/.test(normalized) ? "agy_provider_limit_unknown"
18
+ : /auth|unauthori[sz]ed|forbidden|account|eligib/.test(normalized) ? "agy_provider_account" : "agy_provider_failed";
19
+ return new AgyError(code, message, code === "agy_provider_rate_limited", { provider_result: result, ...metadata });
20
+ }
21
+
17
22
  export function executable(bin, args = []) {
18
23
  return /\.[cm]?js$/.test(bin) ? { command: process.execPath, args: [bin, ...args] } : { command: bin, args };
19
24
  }
20
25
 
21
26
  export async function runAgy(bin, args, options = {}) {
22
- const target = executable(bin, args);
23
- return await new Promise((resolve, reject) => {
24
- const child = spawn(target.command, target.args, { cwd: options.cwd, env: options.env ?? process.env, detached: process.platform !== "win32", stdio: ["pipe", "pipe", "pipe"] });
25
- let stdout = "", stderr = "", settled = false;
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);
31
- const finish = (error, value) => { if (settled) return; settled = true; clearTimeout(timer); error ? reject(error) : resolve(value); };
32
- child.stdout.setEncoding("utf8").on("data", chunk => stdout += chunk);
33
- child.stderr.setEncoding("utf8").on("data", chunk => stderr += chunk);
34
- child.on("error", error => finish(error));
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
- });
40
- child.stdin.end(options.stdin ?? "");
41
- });
27
+ const stdout = await commandText(bin, args, { cwd: options.cwd, env: options.env, input: options.stdin ?? "", timeoutMs: options.timeoutMs, phase: "native-doctor" });
28
+ return { stdout, stderr: "", code: 0 };
42
29
  }
43
30
 
44
31
  export function usageSnapshot(result = {}, toolCalls = {}) {
@@ -68,3 +55,4 @@ export async function doctor(options = {}) {
68
55
  return { compatible: true, observed_runtime: { agy: version, dd_harness_contract: "dd-agy-harness@1" }, isolation: { gemini_dir: "verified", app_data_dir: "verified" }, auth: "available", models: modelIds };
69
56
  } finally { await rm(temporary, { recursive: true, force: true }); }
70
57
  }
58
+ import { commandText } from "./process-json.mjs";
@@ -1,9 +1,11 @@
1
+ import { callDaemonObserved } from './daemon-observation.mjs';
2
+ import { admitNativeRequestBuffer } from "./native-streams.mjs";
1
3
  import { captureDispatchGuard, cancelPendingDispatch } from "./dispatch-fence.mjs";
2
4
  import { createHash, randomUUID } from "node:crypto";
3
5
  import { chmod, lstat, mkdir, open, readFile, rename, unlink, writeFile } from "node:fs/promises";
4
6
  import net from "node:net";
5
7
  import { durableDaemonDispatch, completeDaemonShutdown, assertDaemonDispatch, assertDaemonRestartable, trackDaemonConnections, retireDaemonEndpoint, retireFailedDaemon } from "./daemon-operations.mjs";
6
- import { observedTimeout } from "./observation-clock.mjs";
8
+ import { ObservationClock } from "./observation-clock.mjs";
7
9
  import { parseCapacityOperation } from "./codex-capacity-policy.mjs";
8
10
  import path from "node:path";
9
11
  import { spawn } from "node:child_process";
@@ -70,15 +72,8 @@ export async function cancelCodexTree(bridge, options, { remember = () => {}, ac
70
72
  }
71
73
 
72
74
  export async function callDaemon(stateDir, operation, params = {}, timeoutMs = 30_000) {
73
- const target = locations(stateDir); const request = { schema_id: requestSchema, id: process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation, params };
74
- return await new Promise((resolve, reject) => {
75
- const client = net.createConnection(target.socket); let buffer = ""; let settled = false; const timer = operation === "session.prompt" ? null : observedTimeout(() => finish(Object.assign(new Error(`${operation} timed out`), { code: "daemon_timeout", retryable: true })), timeoutMs);
76
- const finish = (error, value) => { if (settled) return; settled = true; clearTimeout(timer); client.destroy(); error ? reject(error) : resolve(value); };
77
- client.setEncoding("utf8"); client.on("connect", () => client.write(`${JSON.stringify(request)}\n`)); client.on("error", (error) => finish(Object.assign(error, { code: "daemon_not_running", retryable: true })));
78
- client.on("end", () => finish(Object.assign(new Error("daemon closed the request without a response"), { code: "daemon_connection_closed", retryable: true })));
79
- client.on("close", () => finish(Object.assign(new Error("daemon connection closed without a response"), { code: "daemon_connection_closed", retryable: true })));
80
- client.on("data", (chunk) => { buffer += chunk; const at = buffer.indexOf("\n"); if (at < 0) return; try { const response = JSON.parse(buffer.slice(0, at)); if (response.schema_id !== responseSchema || response.id !== request.id) throw new Error("invalid daemon response"); response.ok ? finish(null, response.result) : finish(Object.assign(new Error(response.error?.message ?? "daemon error"), response.error)); } catch (error) { finish(Object.assign(error, { code: "daemon_protocol" })); } });
81
- });
75
+ const request = { schema_id: requestSchema, id: process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation, params };
76
+ return await callDaemonObserved({ stateDir, socket: locations(stateDir).socket, request, responseSchema: responseSchema, harness: 'codex', timeoutMs });
82
77
  }
83
78
 
84
79
  export async function startDaemon(options) {
@@ -92,7 +87,7 @@ export async function startDaemon(options) {
92
87
  if (previous?.shutdown_state === "clean") await authorizeRetainedDaemonResume(paths.dir, previous, options.sessionId, config);
93
88
  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);
94
89
  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();
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" }); }
90
+ try { await confirmDaemonProcess(config, resourceProcess, child); const startupClock = new ObservationClock({ timeoutMs: 60_000 }); while (!startupClock.sample()) { 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" }); }
96
91
  catch (error) { await cleanupFailedDaemonStart(config, resourceProcess, child, error, () => readState(paths.state), state.daemon_id); throw error; }
97
92
  }
98
93
  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) }); }
@@ -146,11 +141,11 @@ export async function serveDaemon(stateDir) {
146
141
  if (/:capacity:[1-9]\d*:session\.prompt$|:child:[a-f0-9]+:followup:[1-9]\d*$/.test(operationId ?? ""))
147
142
  await assertDaemonOwnership(paths.dir, { "operation-id": operationId, operation, "native-session-id": params.sessionId, "prompt-sha256": createHash("sha256").update(params.prompt ?? "").digest("hex") }, assertDispatch);
148
143
  sessions.add(params.sessionId);
149
- await save({ active_operation: active }); return await promptSessionWithBridge(bridge, { ...state.config, ...params, assertDispatch });
144
+ await save({ active_operation: active }); return await promptSessionWithBridge(bridge, { ...state.config, ...params, operationId, assertDispatch });
150
145
  }
151
146
  finally { active = null; await save({ active_operation: null }); }
152
147
  }
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 }); } }
148
+ if (operation === "session.start") { sessions.add(params.sessionId); active = operation; try { await save({ active_operation: active }); return await startSessionWithBridge(bridge, { ...state.config, ...params, assertDispatch, operationId }); } finally { active = null; await save({ active_operation: null }); } }
154
149
  if (operation === "session.inspect" || operation === "session.resume") {
155
150
  const owned = new Set(sessions);
156
151
  for (const id of owned) for (const child of bridge.directChildren?.(id) ?? []) owned.add(child.provider_session_id);
@@ -172,7 +167,7 @@ export async function serveDaemon(stateDir) {
172
167
  connection.setEncoding("utf8"); let buffer = "", consumed = false;
173
168
  connection.on("data", chunk => {
174
169
  if (consumed) return;
175
- buffer += chunk; const at = buffer.indexOf("\n"); if (at < 0) return; consumed = true;
170
+ buffer += chunk; if (!admitNativeRequestBuffer(buffer, connection, responseSchema)) { consumed = true; return; } const at = buffer.indexOf("\n"); if (at < 0) return; consumed = true;
176
171
  void (async () => {
177
172
  let request;
178
173
  try {