@deksden-com/dd-flow-cli 0.9.0-beta.145 → 0.9.0-beta.149

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 (101) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/dist/build-info.json +7 -7
  3. package/dist/cli/command-inputs.js +6 -2
  4. package/dist/cli/command-metadata.js +18 -2
  5. package/dist/cli/diagnostic-invocation.js +46 -0
  6. package/dist/cli/diagnostic.js +120 -0
  7. package/dist/cli/help.js +35 -0
  8. package/dist/cli/memory-flow-admission.js +4 -2
  9. package/dist/cli/memory-init.js +5 -2
  10. package/dist/cli/run-cli.js +16 -15
  11. package/dist/cli.js +6 -0
  12. package/dist/domain/diagnostic-aspects.js +103 -0
  13. package/dist/domain/diagnostic-flows.js +8 -0
  14. package/dist/harness-runtime/lib/agy-native-inspection.mjs +216 -0
  15. package/dist/harness-runtime/lib/agy-native_pb.js +117 -0
  16. package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
  17. package/dist/harness-runtime/lib/daemon-operations.mjs +84 -10
  18. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +306 -34
  19. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +27 -13
  20. package/dist/harness-runtime/lib/dd-codex.mjs +4 -0
  21. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +4 -3
  22. package/dist/harness-runtime/lib/dd-droid.mjs +70 -28
  23. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +3 -3
  24. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +66 -20
  25. package/dist/harness-runtime/lib/dd-opencode.mjs +30 -3
  26. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +7 -6
  27. package/dist/harness-runtime/lib/driver-recovery.mjs +9 -8
  28. package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -1
  29. package/dist/harness-runtime/lib/managed-daemon.mjs +11 -3
  30. package/dist/harness-runtime/lib/native-children.d.mts +8 -1
  31. package/dist/harness-runtime/lib/native-children.mjs +114 -8
  32. package/dist/harness-runtime/lib/native-request-proof.mjs +1 -1
  33. package/dist/harness-runtime/lib/observation-clock.d.mts +2 -2
  34. package/dist/harness-runtime/lib/observation-clock.mjs +7 -4
  35. package/dist/harness-runtime/lib/provider-overload-policy.d.mts +1 -1
  36. package/dist/harness-runtime/lib/provider-overload-policy.mjs +1 -1
  37. package/dist/harness-runtime/lib/tool-observations.mjs +1 -1
  38. package/dist/schemas/diagnostic-stage-result.schema.json +354 -0
  39. package/dist/schemas/diagnostic-work-result.schema.json +296 -0
  40. package/dist/schemas/execution-bundle.schema.json +7 -1
  41. package/dist/schemas/project-flow-pack-manifest.schema.json +1 -1
  42. package/dist/services/check-environment.js +21 -0
  43. package/dist/services/check-runner-evidence.js +267 -15
  44. package/dist/services/code-checks.js +23 -15
  45. package/dist/services/controller-execution.js +10 -4
  46. package/dist/services/controller-fanout.js +13 -6
  47. package/dist/services/controller-memory-worker.js +3 -2
  48. package/dist/services/dashboard.js +2 -2
  49. package/dist/services/descriptor-run-settlement.js +5 -3
  50. package/dist/services/descriptor-run-start.js +22 -3
  51. package/dist/services/descriptor-stage-coordinator.js +37 -4
  52. package/dist/services/descriptor-stage-settlement.js +2 -1
  53. package/dist/services/diagnostic-evidence.js +73 -0
  54. package/dist/services/diagnostic-report-publication.js +72 -0
  55. package/dist/services/diagnostic-report.js +67 -0
  56. package/dist/services/diagnostic-run-bundle.js +74 -0
  57. package/dist/services/diagnostic-snapshot.js +159 -0
  58. package/dist/services/diagnostic-stage-handlers.js +108 -0
  59. package/dist/services/diagnostic-stage-results.js +132 -0
  60. package/dist/services/diagnostic-start.js +84 -0
  61. package/dist/services/diagnostic-work-batch.js +34 -0
  62. package/dist/services/diagnostic-work-collection.js +97 -0
  63. package/dist/services/diagnostic-work-registration.js +101 -0
  64. package/dist/services/diagnostic-work-results.js +93 -0
  65. package/dist/services/eval-snapshots.js +85 -29
  66. package/dist/services/execution-bundles.js +19 -7
  67. package/dist/services/flow-catalog.js +6 -1
  68. package/dist/services/frozen-engine.js +37 -2
  69. package/dist/services/hybrid-contracts.js +2 -0
  70. package/dist/services/installed-flow-source.js +25 -3
  71. package/dist/services/lifecycle-invocations.js +147 -34
  72. package/dist/services/memory-init-contexts.js +16 -0
  73. package/dist/services/memory-init-controller-start.js +12 -4
  74. package/dist/services/memory-init-lint-capability.js +5 -5
  75. package/dist/services/memory-init-native-capacity.js +4 -4
  76. package/dist/services/memory-init-preflight.js +10 -16
  77. package/dist/services/memory-init-worktrunk-capability.js +3 -7
  78. package/dist/services/memory-layer.js +4 -3
  79. package/dist/services/memory-tool-probe.js +25 -0
  80. package/dist/services/memory-verification-checks.js +15 -4
  81. package/dist/services/native-daemon-history.js +2 -1
  82. package/dist/services/provider-capacity.js +43 -38
  83. package/dist/services/release-cache.js +8 -0
  84. package/dist/services/run-control.js +2 -1
  85. package/dist/services/run-controller-adapter.js +2 -1
  86. package/dist/services/run-controller-process.js +2 -1
  87. package/dist/services/run-controller-recovery.js +2 -1
  88. package/dist/services/run-controller.js +85 -15
  89. package/dist/services/run-fork.js +1 -1
  90. package/dist/services/runs.js +6 -1
  91. package/dist/services/runtime-scope-resume.js +12 -6
  92. package/dist/services/runtime-scope-stop.js +3 -2
  93. package/dist/services/runtime-service.js +2 -1
  94. package/dist/services/stage-handlers.js +16 -3
  95. package/dist/services/stage-pause.js +4 -1
  96. package/dist/services/version-status.js +1 -1
  97. package/dist/services/vnext-code.js +2 -2
  98. package/dist/services/vnext-fanout.js +31 -1
  99. package/dist/services/work-registry.js +37 -6
  100. package/dist/services/workspace-bootstrap.js +2 -1
  101. package/package.json +6 -3
@@ -3,11 +3,14 @@ import { SETUP_STARTUP_MS, startupObserver, observationDetails } from './setup-p
3
3
  import { callDaemonObserved } from './daemon-observation.mjs';
4
4
  import { operationProgressReporter } from './operation-progress.mjs';
5
5
  import { prepareDaemonRequest } from "./native-request-proof.mjs";
6
+ import { inspectAgyNative, stopAgyNativeTree, ownedAgyEndpoint, readAgyNativeQualification, AGY_NATIVE_INSPECTION_CONTRACT } from './agy-native-inspection.mjs';
7
+ import { processSnapshot } from './process-snapshot.mjs';
8
+ import { streamRequestKey } from './provider-overload-policy.mjs';
6
9
  import { captureDispatchGuard, cancelPendingDispatch } from "./dispatch-fence.mjs";
7
10
  import { errorRecord, isProvenShellCorrection } from "./operation-errors.mjs";
8
11
  import { ToolObservations, agyToolObservation, agyTranscriptObservations } from "./tool-observations.mjs";
9
12
  import { constants } from "node:fs";
10
- import { fileURLToPath } from "node:url";
13
+ import { fileURLToPath, pathToFileURL } from "node:url";
11
14
  import { createHash, randomUUID } from "node:crypto";
12
15
  import { appendFile, chmod, lstat, mkdir, open, readFile, realpath, rename, unlink, writeFile } from "node:fs/promises";
13
16
  import { spawn } from "node:child_process";
@@ -21,12 +24,45 @@ import path from "node:path";
21
24
  import { observeModel } from "./model-observations.mjs";
22
25
  import { AGY_VERSION_TIMEOUT_MS, AgyError, agyTerminalFailure, agyModelArgs, assertProfile, executable, observedProfile, runAgy, usageSnapshot } from "./dd-agy.mjs";
23
26
  import { recoveredAgyErrorBoundary } from "./agy-error-boundary.mjs";
24
- import { settledNativeTree } from "./native-children.mjs";
27
+ import { settledNativeTree, nativeExecutionReceipt } from "./native-children.mjs";
25
28
  import { retainStartupFailure, prepareRuntimeOwner, cleanupFailedStart, cleanupFailedDaemonStart, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, scheduleDaemonProcessHeartbeat, registerDaemonProcess, stopProcessGroup } from "./managed-daemon.mjs";
26
29
  import { readNativeLines, streamNativeDiagnostics, nativeObservationError, admitNativeRequestBuffer } from "./native-streams.mjs";
27
30
 
28
31
  const REQUEST_SCHEMA = "dd-agy/daemon-request@1", RESPONSE_SCHEMA = "dd-agy/daemon-response@1", STATE_SCHEMA = "dd-agy/daemon-state@1";
29
32
  class DaemonError extends AgyError {}
33
+ // Only unambiguously correlated native tool results can introduce children or
34
+ // classify services. This transcript format has no tool-call IDs: parallel
35
+ // calls therefore retain unknown resources, never FIFO-derived authority.
36
+ export function agyTranscriptFacts(text, source, sessionId) {
37
+ const children = [], resources = [], calls = [];
38
+ if (!source?.locator || !source?.sha256 || !sessionId) return { children, resources };
39
+ for (const line of text.trim().split('\n')) {
40
+ let event;
41
+ try { event = JSON.parse(line); } catch { return { children: [], resources: [] }; }
42
+ if (event.source !== 'MODEL') continue;
43
+ if (event.type === 'PLANNER_RESPONSE') { calls.length = 0; if (Array.isArray(event.tool_calls) && event.tool_calls.length === 1) calls.push(event.tool_calls[0]); continue; }
44
+ if (event.type !== 'GENERIC') continue;
45
+ const call = calls.shift(), content = typeof event.content === 'string' ? event.content : '';
46
+ const native_source = { ...source, step_index: event.step_index };
47
+ if (call?.name === 'invoke_subagent' && event.status === 'DONE') {
48
+ const match = content.match(/^Created At: [^\n]+\nCompleted At: [^\n]+\nCreated the following subagent:\n(\{[\s\S]*?\})\nThe subagent will send you a message/);
49
+ if (!match) continue;
50
+ try {
51
+ const child = JSON.parse(match[1]);
52
+ if (/^[\w-]+$/.test(child.conversationId) && typeof child.logAbsoluteUri === 'string') children.push({ provider_session_id: child.conversationId, parent_provider_session_id: sessionId, log_uri: child.logAbsoluteUri, workspace_uris: child.workspaceUris ?? [], status: 'unknown', native_source });
53
+ } catch { /* No partial/guessed native edge. */ }
54
+ }
55
+ if (event.status !== 'RUNNING') continue;
56
+ const task = content.match(/^Created At: [^\n]+\nTool is running as a background task with task id: ([\w-]+\/task-\d+)\n/);
57
+ if (!task || !task[1].startsWith(`${sessionId}/`)) continue;
58
+ let daemon = call?.name === 'run_command' ? call.args?.IsDaemon : null;
59
+ if (typeof daemon === 'string') { try { daemon = JSON.parse(daemon); } catch { daemon = null; } }
60
+ resources.push({ resource_id: task[1], provider_session_id: sessionId, classification: daemon === true ? 'support_service' : daemon === false ? 'required_work' : 'unknown', state: 'unknown', native_source,
61
+ ...(typeof daemon === 'boolean' ? { native_classification: { tool: 'run_command', IsDaemon: daemon } } : {}) });
62
+ }
63
+ return { children, resources };
64
+ }
65
+ export const agyTranscriptResources = (text, source, sessionId) => agyTranscriptFacts(text, source, sessionId).resources;
30
66
  export async function captureAgyTranscript(runtimeRoot, child, rootSessionId, ownedSessionIds = [rootSessionId], inspect = null) {
31
67
  const session = { harness_id: "antigravity-cli", session_id: child.provider_session_id };
32
68
  let handle;
@@ -173,7 +209,7 @@ export async function callDaemon(stateDir, operation, params = {}, timeoutMs = 3
173
209
  return await callDaemonObserved({ stateDir, socket: locations(stateDir).socket, request, responseSchema: RESPONSE_SCHEMA, harness: 'agy', timeoutMs });
174
210
  }
175
211
 
176
- 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)); }
212
+ function sameConfig(state, config) { return ["cwd","journal","bin","provider","model","reasoning","mode","projectRoot","ddFlowBin","ddFlowHome","resourceHome","noFlow","livenessTimeoutMs","nativeQualificationPath"].every(key => (state.config?.[key] ?? null) === (config[key] ?? null)); }
177
213
  // Called only after authorizeRetainedDaemonResume verifies the old daemon,
178
214
  // provider processes and operation ledger. Native outcomes remain historical;
179
215
  // their old liveness flags cannot describe the replacement process tree.
@@ -183,6 +219,8 @@ export function retainedAgyState(previous, sessionId) {
183
219
  return {
184
220
  ...previous,
185
221
  unclaimed_activity: false,
222
+ native_resources_entries: [],
223
+ native_topology_error: null,
186
224
  retained_tree_settlement: { daemon_id: previous.daemon_id, session_id: sessionId, turn_generation: previous.turn_generation ?? 0, evidence: "verified_clean_daemon_shutdown" },
187
225
  session_observations: (previous.session_observations ?? []).map(([id, observation]) => [id, id === sessionId ? { ...observation, retained_stop: observation.stop ?? null, stop: null } : observation]),
188
226
  descendants: (previous.descendants ?? []).map(child => child.tree_settled === true ? child : { ...child, tree_settled: true, settlement_evidence: "prior_clean_daemon_shutdown", settlement_daemon_id: previous.daemon_id })
@@ -192,6 +230,7 @@ export async function startDaemon(options) {
192
230
  const paths = locations(options.stateDir); await mkdir(paths.dir, { recursive: true, mode: 0o700 }); await chmod(paths.dir, 0o700);
193
231
  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 };
194
232
  config.livenessTimeoutMs = options.livenessTimeoutMs ?? 1_800_000;
233
+ config.nativeQualificationPath = process.env.DD_AGY_NATIVE_QUALIFICATION ? absolute(process.env.DD_AGY_NATIVE_QUALIFICATION, 'DD_AGY_NATIVE_QUALIFICATION') : null;
195
234
  validateObservationDuration(config.livenessTimeoutMs);
196
235
  await prepareRuntimeOwner(config, paths.dir);
197
236
  assertDaemonRestartable(previous);
@@ -203,7 +242,7 @@ export async function startDaemon(options) {
203
242
  const version = (await runAgy(config.bin, ["--version"], { cwd: config.cwd, env: sanitizedEnv(config), timeoutMs: AGY_VERSION_TIMEOUT_MS })).stdout.trim();
204
243
  const resourceProcess = await registerDaemonProcess(config, { kind: "agy-daemon", owner: `agy:${path.basename(paths.dir)}`, operation: `agy-daemon:${path.basename(paths.dir)}`, stdout: paths.log, stderr: paths.log });
205
244
  const retained = retainedAgyState(previous, config.resumeSessionId);
206
- await writeJson(paths.state, { schema_id: STATE_SCHEMA, daemon_id: config.daemonId, shutdown_state: "starting", active_tree: false, unclaimed_activity: retained.unclaimed_activity ?? false, retained_tree_settlement: retained.retained_tree_settlement ?? null, versions: { agy: version }, config, sessions: [], session_observations: retained.session_observations ?? [], descendants: retained.descendants ?? [], ...(Array.isArray(retained.tool_observations) ? { tool_observations: retained.tool_observations } : {}), tool_observation_gaps: retained.tool_observation_gaps ?? [], tool_calls: retained.tool_calls ?? [], completed_tool_steps: retained.completed_tool_steps ?? [], tool_identity_complete: retained.tool_identity_complete !== false, turn_generation: retained.turn_generation ?? 0, last_result: retained.last_result ?? null, resource_process: resourceProcess });
245
+ await writeJson(paths.state, { schema_id: STATE_SCHEMA, daemon_id: config.daemonId, shutdown_state: "starting", active_tree: false, unclaimed_activity: retained.unclaimed_activity ?? false, retained_tree_settlement: retained.retained_tree_settlement ?? null, versions: { agy: version }, config, sessions: [], session_observations: retained.session_observations ?? [], descendants: retained.descendants ?? [], native_resources_entries: retained.native_resources_entries ?? [], native_topology_error: retained.native_topology_error ?? null, ...(Array.isArray(retained.tool_observations) ? { tool_observations: retained.tool_observations } : {}), tool_observation_gaps: retained.tool_observation_gaps ?? [], tool_calls: retained.tool_calls ?? [], completed_tool_steps: retained.completed_tool_steps ?? [], tool_identity_complete: retained.tool_identity_complete !== false, turn_generation: retained.turn_generation ?? 0, last_result: retained.last_result ?? null, resource_process: resourceProcess });
207
246
  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();
208
247
  try {
209
248
  await confirmDaemonProcess(config, resourceProcess, child);
@@ -224,10 +263,17 @@ export async function startDaemon(options) {
224
263
  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) }); };
225
264
 
226
265
  export class Runtime {
227
- 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; }
228
- 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; }
266
+ 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;
267
+ const saved = state.native_observation_episode;
268
+ if (saved?.clock) {
269
+ this.observationEpisode = { operationId: saved.operation_id, generation: saved.turn_generation, connectionId: saved.connection_id, terminalMetadata: state.last_terminal_metadata,
270
+ clock: new ObservationClock({ timeoutMs: saved.clock.timeout_ms, saved: saved.clock }) };
271
+ this.connectionId = saved.connection_id;
272
+ }
273
+ }
274
+ persist(patch = {}) { Object.assign(this.state, patch, { native_observation_episode: this.episodeSnapshot(), 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; }
229
275
  async journal(kind, payload = {}, reservedPosition = null) { const position = reservedPosition ?? (this.journalPosition = (this.journalPosition ?? 0) + 1); const line = `${JSON.stringify({ schema_id: "dd-agy/daemon-event@1", event_id: randomUUID(), position, 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 }); } return position; }
230
- args(sessionId) { return ["--input-format", "stream-json", "--output-format", "stream-json", ...agyModelArgs(this.config.model, 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"])]; }
276
+ args(sessionId) { return ["--input-format", "stream-json", "--output-format", "stream-json", ...agyModelArgs(this.config.model, this.config.reasoning), "--mode", this.config.mode, "--dangerously-skip-permissions", "--print-timeout", "0", ...(this.nativeLogFile ? ['--log-file', this.nativeLogFile] : []), ...(this.nativeCsrfToken ? [`--csrf_token=${this.nativeCsrfToken}`] : []), `--gemini_dir=${this.paths.gemini}`, "--app_data_dir=runtime", ...(sessionId ? ["--conversation", sessionId] : ["--new-project"])]; }
231
277
  requireSessionIdentity(requestedId) {
232
278
  const observedId = this.init?.conversation_id ?? null;
233
279
  if (!requestedId || requestedId === observedId) return observedId;
@@ -235,7 +281,7 @@ export class Runtime {
235
281
  harness: "antigravity-cli", expected_session_id: requestedId, observed_session_id: observedId
236
282
  });
237
283
  }
238
- 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); }); }
284
+ markActivity() { const current = this.active ?? this.observationEpisode; this.nativeRevision = (this.nativeRevision ?? 0) + 1; this.state.native_execution_revision = this.nativeRevision; this.nativeProof = null; this.nativeAbort?.abort(); this.lastActivityAt = new Date().toISOString(); this.progress = { cursor: (this.progress?.cursor ?? 0) + 1, observed_at: Date.now() }; current?.clock?.sample(this.progress); void current?.publishProgress?.('native_progress', { session_id: this.init?.conversation_id, observed_at: this.lastActivityAt }).catch(error => { if(this.active === current) current.reject(error); }); }
239
285
  markTerminalActivity(result) {
240
286
  const current = this.active;
241
287
  if (!current) return;
@@ -245,11 +291,12 @@ export class Runtime {
245
291
  current.terminalProgress.add(key); this.markActivity();
246
292
  }
247
293
  observeProviderActivity(event) {
248
- if (!this.active) return;
294
+ const owner = this.active ?? this.observationEpisode;
295
+ if (!owner || owner.connectionId !== this.connectionId || owner.generation !== this.turnGeneration) return;
249
296
  const root = this.init?.conversation_id, step = event.step_update;
250
297
  const id = step?.conversation_id ?? event.result?.conversation_id ?? root;
251
298
  let parent = id; const seen = new Set();
252
- 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; }
299
+ while (parent && parent !== root && !seen.has(parent)) { seen.add(parent); const child = this.descendants.get(parent); if (child?.activity_generation !== owner.generation) return; parent = child.parent_provider_session_id; }
253
300
  if (!root || parent !== root) return;
254
301
  if (event.event === "result") { if (["SUCCESS", "ERROR", "CANCELLED"].includes(event.result?.status)) this.markTerminalActivity(event.result); return; }
255
302
  if (event.event !== "step_update") return;
@@ -267,7 +314,7 @@ export class Runtime {
267
314
  // Native step snapshots can alternate/replay; scope retained dedup to the
268
315
  // current step. Too many unique snapshots becomes uncertain, never a grant.
269
316
  if (snapshots.size >= 1_024) return;
270
- snapshots.add(fingerprint); this.progressSteps.set(id, { index, seen: snapshots }); this.markActivity();
317
+ snapshots.add(fingerprint); this.progressSteps.set(id, { index, seen: snapshots }); this.invalidateSettlement(id); this.markActivity();
271
318
  }
272
319
  async start(sessionId = null) {
273
320
  if (this.drainingError) throw this.drainingError;
@@ -280,6 +327,11 @@ export class Runtime {
280
327
  await this.providerFinalization;
281
328
  if (this.drainingError) throw this.drainingError;
282
329
  assertDaemonDispatch(this.paths?.dir);
330
+ this.nativeAbort?.abort(); this.nativeProof = null;
331
+ this.nativeLogFile = path.join(this.paths.dir, `native-${randomUUID()}.log`);
332
+ this.nativeCsrfToken = randomUUID();
333
+ this.admissionClock = null;
334
+ this.nativeQualification = await readAgyNativeQualification(this.paths.dir, this.config.bin, this.config.nativeQualificationPath);
283
335
  const target = executable(this.config.bin, this.args(sessionId)); this.exited = false; this.buffer = ""; this.init = null;
284
336
  let readyResolve, readyReject; this.readyPromise = new Promise((resolve, reject) => { readyResolve = resolve; readyReject = reject; });
285
337
  // The provider can reject before durable registration finishes. Keep a
@@ -293,6 +345,7 @@ export class Runtime {
293
345
  try { assertDaemonDispatch(this.paths?.dir); this.connectionId = randomUUID(); this.child = spawn(target.command, target.args, { cwd: this.config.cwd, env: sanitizedEnv(this.config), detached: true, stdio: ["pipe", "pipe", "pipe"] }); }
294
346
  catch (error) { await cleanupFailedStart(this.config, providerProcess, null, error); throw error; }
295
347
  const providerChild = this.child;
348
+ this.nativeLaunch = { logFile: this.nativeLogFile, providerPid: providerChild.pid, providerBirth: (await processSnapshot()).find(item => item.pid === providerChild.pid)?.started, connectionId: this.connectionId };
296
349
  let allowProviderFinalization;
297
350
  const providerRegistered = new Promise(resolve => { allowProviderFinalization = resolve; });
298
351
  const observationFailure = error => { this.drainingError ??= error; readyReject(error); this.active?.reject(error); };
@@ -356,25 +409,53 @@ export class Runtime {
356
409
  // Tree liveness is broader than current-Turn model progress: fresh work
357
410
  // from a retained child invalidates settlement without adopting this Turn.
358
411
  if (fresh && (sessionId === this.init?.conversation_id || this.descendants.has(sessionId))) this.childActivity(sessionId);
359
- if (Number.isInteger(step.step_index)) this.sessionObservations.set(sessionId, { ...observation, last_step_index: Math.max(observation.last_step_index ?? -1, step.step_index) });
412
+ if (Number.isInteger(step.step_index)) this.sessionObservations.set(sessionId, { ...this.sessionObservations.get(sessionId), ...(step.step_index > (observation.last_step_index ?? -1) ? { last_execution_num: null } : {}), last_step_index: Math.max(observation.last_step_index ?? -1, step.step_index) });
360
413
  this.toolObservations.add(agyToolObservation(step, this.init?.conversation_id));
361
414
  for (const child of step.subagent_info?.subagents ?? []) if (child.conversation_id) {
362
415
  const prior = this.descendants.get(child.conversation_id);
416
+ if (child.conversation_id === this.init?.conversation_id || prior && prior.parent_provider_session_id !== sessionId) {
417
+ this.state.native_topology_error = 'agy_native_ancestry_contradiction'; this.invalidateSettlement(sessionId); continue;
418
+ }
363
419
  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",
420
+ ...(bound && Number.isInteger(step.step_index) ? { native_source: { kind: 'agy.step_update.subagent_info', parent_provider_session_id: sessionId, step_index: step.step_index } } : {}),
364
421
  ...(fresh && bound && this.active ? { activity_generation: this.active.generation } : {}) });
365
422
  }
366
423
  }
367
424
  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 }; }
368
425
  async captureChildTools() {
369
426
  this.capturedToolFacts ??= new Map();
427
+ this.nativeResources ??= new Map(this.state.native_resources_entries?.map(item => [item.resource_id, item]) ?? []);
370
428
  const root = this.init?.conversation_id ?? this.state.sessions?.[0]?.provider_session_id ?? this.lastResult?.conversation_id;
371
- for (const child of this.descendants.values()) {
429
+ const queue = root && this.paths.runtime ? [{ provider_session_id: root, parent_provider_session_id: root, log_uri: pathToFileURL(path.join(this.paths.runtime, 'brain', root, '.system_generated', 'logs', 'transcript.jsonl')).href }, ...this.descendants.values()] : [...this.descendants.values()];
430
+ const visited = new Set();
431
+ for (let index = 0; index < queue.length && index < 256; index++) {
432
+ const child = queue[index];
433
+ if (visited.has(child.provider_session_id)) continue;
434
+ visited.add(child.provider_session_id);
372
435
  const ancestors = new Set(); let parent = child.parent_provider_session_id;
373
436
  while (parent && parent !== root && this.descendants.has(parent) && !ancestors.has(parent)) {
374
437
  ancestors.add(parent); parent = this.descendants.get(parent).parent_provider_session_id;
375
438
  }
376
439
  const events = parent === root && root
377
- ? await captureAgyTranscript(this.paths.runtime, child, root, [root, ...ancestors])
440
+ ? await captureAgyTranscript(this.paths.runtime, child, root, [root, ...ancestors], (text, source) => {
441
+ const facts = agyTranscriptFacts(text, source, child.provider_session_id);
442
+ for (const resource of facts.resources) this.nativeResources.set(resource.resource_id, resource);
443
+ for (const discovered of facts.children) {
444
+ const expected = path.join(this.paths.runtime, 'brain', discovered.provider_session_id, '.system_generated', 'logs', 'transcript.jsonl');
445
+ const sourceBrain = path.resolve(path.dirname(fileURLToPath(source.locator)), '../../..');
446
+ const sibling = path.join(sourceBrain, discovered.provider_session_id, '.system_generated', 'logs', 'transcript.jsonl');
447
+ let locator;
448
+ try { locator = fileURLToPath(discovered.log_uri); } catch { locator = null; }
449
+ if (locator !== expected && locator !== sibling) { this.state.native_topology_error = 'agy_native_locator_unbound'; this.invalidateSettlement(root); continue; }
450
+ const prior = this.descendants.get(discovered.provider_session_id);
451
+ if (discovered.provider_session_id === root || prior && (prior.parent_provider_session_id !== discovered.parent_provider_session_id || prior.log_uri && prior.log_uri !== discovered.log_uri)) {
452
+ this.state.native_topology_error = 'agy_native_ancestry_contradiction'; this.invalidateSettlement(root); continue;
453
+ }
454
+ const value = { ...discovered, ...prior, native_source: discovered.native_source };
455
+ this.descendants.set(value.provider_session_id, value);
456
+ queue.push(value);
457
+ }
458
+ })
378
459
  : [{ session: { harness_id: "antigravity-cli", session_id: child.provider_session_id }, reason: "agy_transcript_ancestry_unproven" }];
379
460
  for (const event of events) {
380
461
  this.toolObservations.add(event);
@@ -384,6 +465,8 @@ export class Runtime {
384
465
  this.capturedToolFacts.set(key, encoded);
385
466
  }
386
467
  }
468
+ if (queue.length > 256 && visited.size < new Set(queue.map(item => item.provider_session_id)).size) { this.state.native_topology_error = 'agy_native_inventory_limit'; this.invalidateSettlement(root); }
469
+ this.state.native_resources_entries = [...this.nativeResources.values()];
387
470
  }
388
471
  async finishTurn(result, retainedMetadata = null, providerEvent = null) {
389
472
  if (result.status === "RUNNING") { await this.persist({ active_tree: true }); return; }
@@ -420,7 +503,7 @@ export class Runtime {
420
503
  const rootStop = this.sessionObservations.get(this.init?.conversation_id)?.stop;
421
504
  const pendingTerminal = current && result.status === "SUCCESS";
422
505
  if (current) { current.terminalResult = result; current.terminalMetadata = terminalMetadata; current.terminalPersisted = false; }
423
- if (pendingTerminal && rootStop?.fullyIdle === true) 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" });
506
+ if (pendingTerminal && rootStop?.source_bound === true && rootStop.fullyIdle === true) 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" });
424
507
  this.state.terminal_observation_pending = true;
425
508
  try {
426
509
  await this.persist({ active_tree: true, last_result: result, last_terminal_metadata: terminalMetadata });
@@ -438,6 +521,7 @@ export class Runtime {
438
521
  try { await this.persist({ terminal_observation_pending: false }); }
439
522
  catch (error) { this.state.terminal_observation_pending = true; return this.failTerminalObservation(current, result, terminalMetadata, error); }
440
523
  this.state.terminal_observation_pending = false;
524
+ this.queueNativeInspection();
441
525
  if (!current) return;
442
526
  if (this.active !== current) return;
443
527
  if (result.status === "ERROR" || result.status === "CANCELLED") {
@@ -466,6 +550,7 @@ export class Runtime {
466
550
  const current = this.active;
467
551
  if (current?.observationError) throw current.observationError;
468
552
  if (current?.terminalResult?.status !== "SUCCESS" || current.terminalResult !== this.lastResult) return;
553
+ this.queueNativeInspection();
469
554
  await this.persist({ active_tree: !this.receipt().settled });
470
555
  // Hooks can invalidate the tree while persistence is pending. Never
471
556
  // publish a proof captured before that asynchronous boundary.
@@ -486,26 +571,127 @@ export class Runtime {
486
571
  if (current) { current.observationError = error; current.reject(error); }
487
572
  throw error;
488
573
  }
574
+ queueNativeInspection() {
575
+ const owner = this.active ?? this.observationEpisode, proof = owner?.terminalMetadata?.request_proof;
576
+ if (!owner?.clock || !proof || this.state.terminal_observation_pending || this.nativeFlight || this.nativeProof || this.exited) return;
577
+ if (proof.connection_id !== this.connectionId || proof.daemon_instance !== this.config.daemonId || proof.turn_generation !== this.turnGeneration || proof.operation_id !== owner.operationId || proof.session_id !== this.init?.conversation_id) { this.nativeInspection = { state: 'unproven', reason: 'native_owner_binding_changed' }; return; }
578
+ if (owner.clock.sample(this.progress)) { this.nativeInspection = { state: 'unproven', reason: 'observation_budget_exhausted' }; return; }
579
+ // The observer's raw wire result is never authority before P3 qualification.
580
+ if (!this.nativeObserver && !this.nativeQualification?.qualified) { this.nativeInspection = { state: 'unproven', reason: 'native_inspection_not_qualified' }; return; }
581
+ const revision = this.nativeRevision ?? 0, connection = this.connectionId;
582
+ const controller = this.nativeAbort = new AbortController();
583
+ let recording = false;
584
+ this.nativeInspection = { state: 'inspection_pending', reason: 'native_idle_barrier_pending' };
585
+ this.nativeFlight = Promise.resolve().then(async () => {
586
+ await owner.dispatchJournal;
587
+ const clock = owner.clock.state(), remainingMs = Math.max(0, clock.timeout_ms - clock.elapsed_ms);
588
+ if (!remainingMs || owner.clock.sample(this.progress)) throw new Error('observation_budget_exhausted');
589
+ const endpoint = this.nativeObserver ? null : await ownedAgyEndpoint(this.nativeLaunch);
590
+ const observed = await (this.nativeObserver ?? inspectAgyNative)({ endpoint, sessionId: proof.session_id, cwd: this.config.cwd, remainingMs, signal: controller.signal, rootRelation: this.nativeQualification?.rootRelation, csrfToken: this.nativeCsrfToken });
591
+ if (!this.nativeObserver && JSON.stringify(await ownedAgyEndpoint(this.nativeLaunch)) !== JSON.stringify(endpoint)) throw new Error('native_endpoint_changed');
592
+ const current = () => !controller.signal.aborted && this.connectionId === connection && (this.nativeRevision ?? 0) === revision && this.turnGeneration === owner.generation && this.observationEpisode === owner && !this.exited && !owner.clock.sample(this.progress);
593
+ if (!current()) return;
594
+ if (!observed.idle) { this.nativeInspection = { state: 'inspection_pending', reason: observed.reason ?? 'native_wait_pending' }; return; }
595
+ const candidate = { daemon_instance: this.config.daemonId, connection_id: connection, operation_id: owner.operationId, turn_generation: owner.generation, request_proof: proof, revision, method: observed.method, source: 'native_idle_barrier' };
596
+ recording = true;
597
+ await this.journal('native_tree_settlement', { proof: candidate });
598
+ if (!current()) return;
599
+ // Persist conservatively before publishing. A crash may require another
600
+ // read-only observation; no concurrent reader gets an uncommitted grant.
601
+ await this.persist({ active_tree: true });
602
+ if (!current()) return;
603
+ this.nativeProof = candidate;
604
+ this.state.native_execution_revision = revision;
605
+ this.state.unclaimed_activity = false;
606
+ this.projectNativeIdleChildren();
607
+ this.nativeInspection = { state: 'ready', reason: 'native_wait_fully_idle' };
608
+ this.state.active_tree = !this.receipt().settled;
609
+ await this.completePendingTurn();
610
+ }).catch(error => {
611
+ if (!controller.signal.aborted) {
612
+ this.nativeProof = null;
613
+ const transient = !recording && ['grpc_4', 'grpc_14'].includes(error.details?.reason) && !owner.clock.sample(this.progress);
614
+ this.nativeInspection = { state: transient ? 'inspection_pending' : 'unproven', reason: error.details?.reason ?? error.message ?? 'native_inspection_failed' };
615
+ if (recording && this.active === owner && owner.terminalResult) {
616
+ try { this.failTerminalObservation(owner, owner.terminalResult, owner.terminalMetadata, error); } catch { /* original prompt owns the typed failure */ }
617
+ }
618
+ if (recording) this.state.terminal_observation_pending = true;
619
+ }
620
+ })
621
+ .finally(() => { this.nativeFlight = null; });
622
+ }
623
+ projectNativeIdleChildren() {
624
+ for (const [id, child] of this.descendants) this.descendants.set(id, { ...child, ...(!['completed', 'failed', 'cancelled'].includes(child.status) ? { previous_native_status: child.status, status: 'settled_by_root' } : {}), ...(child.native_turn_items ? { native_turn_items: { ...child.native_turn_items, observed: true, pending: false } } : {}), tree_settled: true, settlement_evidence: 'native_idle_barrier' });
625
+ }
626
+ async reconcileRetainedAdmission() {
627
+ const retained = this.state.retained_tree_settlement;
628
+ const fresh = !this.lastResult;
629
+ if (this.active || this.receipt().settled || !fresh && (retained?.evidence !== 'verified_clean_daemon_shutdown' || retained.session_id !== this.init?.conversation_id)) return;
630
+ if (!this.nativeObserver && !this.nativeQualification?.qualified) return;
631
+ if (this.admissionFlight) return await this.admissionFlight;
632
+ const clock = this.admissionClock ??= new ObservationClock({ timeoutMs: SETUP_STARTUP_MS });
633
+ if (!clock || clock.sample()) throw new DaemonError('agy_native_inspection_unavailable', 'Retained AGY native admission budget exhausted');
634
+ const connection = this.connectionId, revision = this.nativeRevision ?? 0, generation = this.turnGeneration;
635
+ const controller = this.nativeAbort = new AbortController();
636
+ this.admissionFlight = (async () => {
637
+ const endpoint = this.nativeObserver ? null : await ownedAgyEndpoint(this.nativeLaunch);
638
+ const observed = await (this.nativeObserver ?? inspectAgyNative)({ endpoint, sessionId: this.init.conversation_id, cwd: this.config.cwd, remainingMs: Math.max(1, clock.timeoutMs - clock.elapsed), signal: controller.signal, rootRelation: this.nativeQualification?.rootRelation, csrfToken: this.nativeCsrfToken });
639
+ if (!this.nativeObserver && JSON.stringify(await ownedAgyEndpoint(this.nativeLaunch)) !== JSON.stringify(endpoint)) throw new DaemonError('agy_native_inspection_unavailable', 'AGY admission endpoint changed');
640
+ const current = () => !controller.signal.aborted && !this.exited && !this.active && !clock.sample() && this.connectionId === connection && this.turnGeneration === generation && (this.nativeRevision ?? 0) === revision;
641
+ if (!observed.idle || !current()) throw new DaemonError('agy_native_inspection_unavailable', 'AGY fresh native admission is unproven');
642
+ // Admission is not the old stream request: no invented operation/Turn or
643
+ // request proof. The old failed result remains historical diagnostics.
644
+ const candidate = { daemon_instance: this.config.daemonId, connection_id: connection, turn_generation: generation, revision, method: observed.method, source: 'native_idle_admission' };
645
+ await this.journal('native_idle_admission', { proof: candidate });
646
+ if (!current()) throw new DaemonError('agy_native_inspection_unavailable', 'AGY native admission changed during observation persistence');
647
+ await this.persist({ active_tree: true });
648
+ if (!current()) throw new DaemonError('agy_native_inspection_unavailable', 'AGY native admission changed during observation persistence');
649
+ this.nativeProof = candidate;
650
+ this.state.native_execution_revision = revision;
651
+ this.state.unclaimed_activity = false; this.projectNativeIdleChildren();
652
+ this.state.active_tree = !this.receipt().settled;
653
+ })().catch(error => { this.nativeProof = null; this.nativeInspection = { state: 'unproven', reason: error.details?.reason ?? error.code ?? 'native_admission_unproven' }; throw error; }).finally(() => { this.admissionFlight = null; });
654
+ return await this.admissionFlight;
655
+ }
656
+ episodeSnapshot() {
657
+ const owner = this.observationEpisode;
658
+ return owner?.clock ? { operation_id: owner.operationId, turn_generation: owner.generation, connection_id: owner.connectionId, clock: owner.clock.state() } : null;
659
+ }
489
660
  receipt(result = this.lastResult) {
490
661
  if (this.state.last_terminal_failure && !this.lastTerminalFailure) this.lastTerminalFailure = this.state.last_terminal_failure;
491
- const terminalMetadata = this.active?.terminalResult === result ? this.active.terminalMetadata : this.state.last_result === result ? this.state.last_terminal_metadata : this.lastTerminalFailure && this.lastTerminalFailure.details?.provider_result?.error === result?.error
662
+ const metadata = this.active?.terminalResult === result ? this.active.terminalMetadata : this.state.last_result === result ? this.state.last_terminal_metadata : this.lastTerminalFailure && this.lastTerminalFailure.details?.provider_result?.error === result?.error
492
663
  && this.lastTerminalFailure.details.provider_session_id === (result?.conversation_id || this.init?.conversation_id || null) ? this.lastTerminalFailure.details : null;
664
+ const terminalMetadata = metadata?.request_proof && (metadata.request_proof.connection_id !== this.connectionId || metadata.request_proof.daemon_instance !== this.config.daemonId)
665
+ ? { ...metadata, request_proof: undefined } : metadata;
493
666
  const assistantText = typeof result?.response === "string" ? result.response : typeof result?.text === "string" ? result.text : null;
494
667
  const root = this.init?.conversation_id; const observation = this.sessionObservations.get(root);
495
668
  const treeSettled = [...this.descendants.values()].every(child => child.tree_settled === true || ["completed", "failed", "cancelled", "settled_by_root"].includes(child.status));
496
- // A terminal root result is the provider's authoritative end-of-turn
497
- // observation when AGY does not emit a root Stop hook. A received
498
- // non-idle Stop still wins, and every discovered child must be terminal.
499
- const rootSettled = observation?.stop?.fullyIdle !== false;
669
+ // SUCCESS can coexist with background tasks. Only a current native idle
670
+ // proof (or verified prior clean retirement before new activity) closes it.
671
+ const retained = this.state.retained_tree_settlement;
672
+ const rootSettled = this.nativeProof?.turn_generation === this.turnGeneration && this.nativeProof.connection_id === this.connectionId && this.nativeProof.revision === (this.nativeRevision ?? 0)
673
+ || observation?.stop?.source_bound === true && observation.stop?.fullyIdle === true && observation.stop.generation === this.turnGeneration
674
+ || retained?.evidence === "verified_clean_daemon_shutdown" && retained.session_id === root && retained.turn_generation === this.turnGeneration && !this.child;
500
675
  const terminalPending = this.active?.terminalResult === result;
501
- const settled = Boolean(!this.state.terminal_observation_pending && !this.active?.observationError && !this.state.unclaimed_activity && (!this.active || terminalPending) && result && ["SUCCESS", "ERROR", "CANCELLED"].includes(result.status) && treeSettled && rootSettled);
502
- 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, ...(terminalMetadata.turn_id ? { terminal_turn_id: terminalMetadata.turn_id } : {}) } : {}), ...(assistantText === null ? {} : { assistant_text: assistantText }), usage: usageSnapshot(result ?? {}, this.toolSnapshot()), usage_ingest: this.usageIngest, descendants: [...this.descendants.values()], ...settledNativeTree(root, [...this.descendants.values()], settled, !this.state.unclaimed_activity && (!this.active || terminalPending) && rootSettled && ["SUCCESS", "ERROR", "CANCELLED"].includes(result?.status)), transcript_path: observation?.transcript_path ?? null, settled };
676
+ // A positively inspected owned scope before its first Turn has no model
677
+ // result to manufacture. Native admission alone proves this empty execution.
678
+ const emptyExecution = !result && this.turnGeneration === 0 && !this.active && !this.descendants.size && this.nativeProof?.source === 'native_idle_admission';
679
+ const executionTerminal = emptyExecution || result && ["SUCCESS", "ERROR", "CANCELLED"].includes(result.status);
680
+ const settled = Boolean(!this.state.native_topology_error && !this.state.terminal_observation_pending && !this.active?.observationError && !this.state.unclaimed_activity && (!this.active || terminalPending) && executionTerminal && treeSettled && rootSettled);
681
+ const clock = this.observationEpisode?.clock;
682
+ if (clock) clock.sample(this.progress);
683
+ const exhausted = clock && (clock.observationLost || clock.elapsed >= clock.timeoutMs);
684
+ const nativeSettlement = { confirmed: Boolean(settled), state: settled ? 'ready' : exhausted ? 'unproven' : this.nativeInspection?.state ?? 'unproven', reason: settled ? 'current_root_idle_proven' : exhausted ? 'observation_budget_exhausted' : this.nativeInspection?.reason ?? 'current_root_idle_proof_missing', capability: AGY_NATIVE_INSPECTION_CONTRACT, operation_id: terminalMetadata?.request_proof?.operation_id, request_key: streamRequestKey(terminalMetadata?.request_proof), provider_session_id: root, daemon_instance: this.config.daemonId, connection_epoch: this.connectionId, generation: this.turnGeneration, ...(this.nativeProof ? { source: this.nativeProof.source, method: this.nativeProof.method, proof_sha256: createHash('sha256').update(JSON.stringify(this.nativeProof)).digest('hex') } : {}), ...(clock ? { observation_clock: clock.state() } : {}) };
685
+ return nativeExecutionReceipt({ 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, ...(terminalMetadata.turn_id ? { terminal_turn_id: terminalMetadata.turn_id } : {}) } : {}), ...(assistantText === null ? {} : { assistant_text: assistantText }), usage: usageSnapshot(result ?? {}, this.toolSnapshot()), usage_ingest: this.usageIngest, descendants: [...this.descendants.values()], ...settledNativeTree(root, [...this.descendants.values()], settled, !this.state.unclaimed_activity && (!this.active || terminalPending) && rootSettled && ["SUCCESS", "ERROR", "CANCELLED"].includes(result?.status)), native_turn_items: { observed: Boolean(terminalMetadata?.request_proof), possible_effects: true, pending: !settled, settled, ...(terminalMetadata?.request_proof ? { request_proof: terminalMetadata.request_proof } : {}) }, transcript_path: observation?.transcript_path ?? null, settled, native_settled: settled, physical_stopped: this.exited === true, native_settlement: nativeSettlement, }, { daemonId: this.config.daemonId, generation: this.turnGeneration, revision: this.nativeRevision ?? 0, resources: { coverage: "partial", entries: [...(this.nativeResources?.values() ?? this.state.native_resources_entries ?? [])] } });
503
686
  }
504
687
  async zeroUsage() { await forwardUsage(this.config, this.init.conversation_id, { usage: {} }, this.toolSnapshot(), []); }
505
688
  async prompt(text, assertDispatch = captureDispatchGuard(this), operationId = null, livenessTimeoutMs = 1_800_000) {
506
689
  validateObservationDuration(livenessTimeoutMs);
507
690
  if (!text) throw new DaemonError("prompt_required", "prompt is required");
508
691
  await this.start(); await this.draining;
692
+ if (!this.nativeObserver && !this.nativeQualification?.qualified && this.config.bin && this.paths.dir) this.nativeQualification = await readAgyNativeQualification(this.paths.dir, this.config.bin, this.config.nativeQualificationPath);
693
+ if (!this.nativeObserver && !this.nativeQualification?.qualified) throw new DaemonError('agy_native_inspection_unqualified', 'AGY whole-tree inspection requires a matching native qualification receipt before productive dispatch', false, { capability: AGY_NATIVE_INSPECTION_CONTRACT });
694
+ await this.reconcileRetainedAdmission();
509
695
  if (this.active || this.promptObservation) throw new DaemonError("operation_in_progress", "an Antigravity turn or its mandatory dispatch observation is already running");
510
696
  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 });
511
697
  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");
@@ -515,6 +701,9 @@ export class Runtime {
515
701
  void reply.catch(() => {});
516
702
  this.connectionId ??= randomUUID();
517
703
  const current = { resolve: resolveTurn, reject: rejectTurn, generation: ++this.turnGeneration, operationId, connectionId: this.connectionId, dispatched: false };
704
+ this.nativeAbort?.abort(); this.nativeProof = null; this.nativeInspection = null;
705
+ current.clock = new ObservationClock({ timeoutMs: livenessTimeoutMs, onGap: gap => { void this.journal('observation_gap', { gap, operation_id: operationId }).catch(() => {}); } });
706
+ this.observationEpisode = current;
518
707
  let resolveDispatchJournal;
519
708
  const dispatchJournal = current.dispatchJournal = new Promise(resolve => { resolveDispatchJournal = resolve; });
520
709
  this.promptObservation = current;
@@ -549,9 +738,10 @@ export class Runtime {
549
738
  else if (this.active === current) this.active = null;
550
739
  current.reject(error); finishDispatchJournal();
551
740
  }
552
- const clock = new ObservationClock({ timeoutMs: livenessTimeoutMs, onGap: gap => { void this.journal("observation_gap", { gap, operation_id: operationId }).catch(() => {}); } });
741
+ const clock = current.clock;
553
742
  let checking = false;
554
743
  const timer = setInterval(() => {
744
+ if (this.active === current && current.terminalResult && this.nativeInspection?.state === 'inspection_pending' && !this.nativeFlight) this.queueNativeInspection();
555
745
  if (this.active !== current || checking || !clock.sample(this.progress)) return;
556
746
  checking = true; const cursor = this.progress?.cursor;
557
747
  void this.completePendingTurn().then(() => {
@@ -580,7 +770,18 @@ export class Runtime {
580
770
  } catch (error) {
581
771
  await dispatchJournal;
582
772
  if (error.code === "native_outcome_observation_failed") throw error;
583
- if (current.dispatchObservationError) throw current.dispatchObservationError;
773
+ if (current.dispatchObservationError) {
774
+ if (!current.terminalResult) throw current.dispatchObservationError;
775
+ const cause = current.dispatchObservationError;
776
+ const failed = current.terminalResult.status === 'SUCCESS'
777
+ ? new DaemonError('native_outcome_observation_failed', 'Antigravity completed natively but its dispatch journal could not be saved', false, { native_outcome: this.receipt(current.terminalResult), settled: false, observation_error: errorRecord(cause) })
778
+ : agyTerminalFailure(current.terminalResult, { ...current.terminalMetadata, native_outcome: current.terminalResult, settled: false, native_turn_items: { observed: false, possible_effects: true, pending: true }, observation_error: errorRecord(cause) });
779
+ failed.cause = cause.cause ?? cause;
780
+ current.observationError = failed; current.dispatchObservationError = failed;
781
+ this.drainingError = failed; this.state.terminal_observation_pending = true;
782
+ if (current.terminalResult.status !== 'SUCCESS') this.lastTerminalFailure = failed;
783
+ throw failed;
784
+ }
584
785
  const metadata = current.terminalMetadata;
585
786
  if (error === this.lastTerminalFailure && metadata && !metadata.request_proof && current.outboundPosition && metadata.stream_terminal_position && this.config.daemonId && current.operationId) {
586
787
  metadata.request_proof = { kind: "stream_request", daemon_instance: this.config.daemonId, connection_id: current.connectionId, operation_id: current.operationId,
@@ -597,18 +798,36 @@ export class Runtime {
597
798
  }
598
799
  }
599
800
  childActivity(sessionId) {
801
+ this.invalidateSettlement(sessionId);
600
802
  if (sessionId === this.init?.conversation_id && !this.active) this.state.unclaimed_activity = true;
601
803
  const child = this.descendants.get(sessionId);
602
804
  if (!child) return;
603
805
  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 });
604
806
  }
807
+ invalidateSettlement(sessionId) {
808
+ this.nativeProof = null; this.nativeRevision = (this.nativeRevision ?? 0) + 1; this.nativeAbort?.abort();
809
+ this.state.native_execution_revision = this.nativeRevision;
810
+ if (this.init) this.state.active_tree = true;
811
+ this.state.retained_tree_settlement = null;
812
+ for (const id of new Set([this.init?.conversation_id, sessionId])) {
813
+ const observation = this.sessionObservations.get(id);
814
+ if (observation?.stop) this.sessionObservations.set(id, { ...observation, stop: null });
815
+ }
816
+ }
605
817
  async childForHook(conversationId) {
606
818
  if (conversationId === this.init?.conversation_id) return null;
819
+ await this.captureChildTools();
607
820
  const deadline = Date.now() + 1_000;
608
- while (!this.descendants.has(conversationId) && Date.now() < deadline) await new Promise(resolve => setTimeout(resolve, 10));
821
+ while (!this.descendants.has(conversationId) && Date.now() < deadline) { await new Promise(resolve => setTimeout(resolve, 25)); await this.captureChildTools(); }
609
822
  const child = this.descendants.get(conversationId);
610
823
  if (!child) throw new DaemonError("agy_child_identity_unconfirmed", "Antigravity hook has no confirmed native parent", true, { conversation_id: conversationId });
611
- if (child.parent_provider_session_id !== this.init?.conversation_id) throw new DaemonError("agy_child_parent_unqualified", "Nested Antigravity worker is not a direct child of the controlled Session", false, { conversation_id: conversationId, parent_conversation_id: child.parent_provider_session_id });
824
+ const seen = new Set([conversationId]); let current = child;
825
+ while (current.parent_provider_session_id !== this.init?.conversation_id) {
826
+ const parent = this.descendants.get(current.parent_provider_session_id);
827
+ if (this.state.native_topology_error || !current.native_source || !parent || seen.has(parent.provider_session_id)) throw new DaemonError("agy_child_parent_unqualified", "Antigravity worker has no source-bound ancestry to the controlled Session", false, { conversation_id: conversationId, parent_conversation_id: current.parent_provider_session_id });
828
+ seen.add(parent.provider_session_id); current = parent;
829
+ }
830
+ if (this.state.native_topology_error) throw new DaemonError("agy_child_parent_unqualified", "Antigravity native ancestry is contradictory");
612
831
  return child;
613
832
  }
614
833
  async rejectHook(error, event, payload, current = this.active) {
@@ -642,12 +861,44 @@ export class Runtime {
642
861
  await this.rejectHook(Object.assign(new Error(failure.message ?? "Native hook failed"), { code: failure.code, details: failure.details }), binding.event, binding.payload, binding.current);
643
862
  }
644
863
  }
645
- 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;
864
+ async observeHook(event, payload, budget = null) {
865
+ const conversationId = payload.conversationId ?? this.init?.conversation_id;
866
+ const child = await this.childForHook(conversationId); budget?.remaining("identity");
867
+ const prior = this.sessionObservations.get(conversationId) ?? {};
868
+ const execution = Number.isInteger(payload.executionNum) ? payload.executionNum : null;
869
+ const step = Number.isInteger(payload.stepIdx) ? payload.stepIdx : null;
870
+ const stale = step !== null && (Number.isInteger(prior.step_floor) && step <= prior.step_floor
871
+ || Number.isInteger(prior.last_step_index) && step < prior.last_step_index
872
+ || step === prior.last_step_index && execution !== null && Number.isInteger(prior.last_execution_num) && execution < prior.last_execution_num);
873
+ if (stale) { await this.journal("stale_hook", { hook_event: event, conversation_id: conversationId, step_index: step }); return { stale: true }; }
874
+ const fingerprint = createHash("sha256").update(JSON.stringify({ event, payload })).digest("hex");
875
+ if (event === "Stop" && prior.last_stop_hook === fingerprint && prior.last_stop_generation === this.turnGeneration) return { stale: true };
876
+ this.progressHooks ??= new Set();
877
+ const scoped = this.active && (!child || child.activity_generation === this.active.generation);
878
+ const sourceBound = Boolean(scoped && step !== null && step > (this.active.stepFloor ?? -1)
879
+ && this.progressSteps?.get(conversationId)?.index === step);
880
+ const progressKey = JSON.stringify([conversationId, step, execution, event, payload.toolCall ?? null, payload.fullyIdle ?? null]);
881
+ 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(); }
882
+ if (conversationId) this.sessionObservations.set(conversationId, { ...prior,
883
+ ...(step !== null ? { last_step_index: step, last_execution_num: execution ?? (step === prior.last_step_index ? prior.last_execution_num : null) } : {}),
884
+ last_hook: fingerprint, ...(event === "PreToolUse" ? { last_pre_hook: fingerprint } : {}),
885
+ ...(payload.transcriptPath ? { transcript_path: payload.transcriptPath } : {}),
886
+ ...(event === "Stop" ? { last_stop_hook: fingerprint, last_stop_generation: this.turnGeneration, stop: { fullyIdle: payload.fullyIdle === true, source_bound: sourceBound, terminationReason: payload.terminationReason ?? null, executionNum: execution } } : {}) });
887
+ if (event === "PreToolUse" && prior.last_pre_hook !== fingerprint) this.childActivity(conversationId);
888
+ if (child && event === "Stop" && sourceBound && payload.fullyIdle === true) this.descendants.set(conversationId, { ...this.descendants.get(conversationId), tree_settled: true, settlement_evidence: "child_fully_idle_stop" });
889
+ const terminalPending = this.active?.terminalResult === this.lastResult;
890
+ if (!child && event === "Stop" && sourceBound && 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" });
891
+ const parent = child?.parent_provider_session_id ?? null;
892
+ if (event === "Stop" && !stale) {
893
+ const observation = this.sessionObservations.get(conversationId);
894
+ this.sessionObservations.set(conversationId, { ...observation, stop: { ...observation.stop, generation: this.turnGeneration } });
895
+ }
646
896
  const observed = payload.model ? observedProfile(payload) : {};
647
897
  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" });
648
898
  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");
649
899
  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 });
650
900
  if (!stale && !child && event === "PreToolUse" && Number.isInteger(step) && this.active) { this.active.nativeHookEvents ??= new Map(); this.active.nativeHookEvents.set(step, `${eventId}:before`); }
901
+ this.queueNativeInspection();
651
902
  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 };
652
903
  }
653
904
  async finalizeProviderForControl() {
@@ -675,19 +926,40 @@ export class Runtime {
675
926
  }
676
927
  async prepareStop(cancelTree) {
677
928
  const current = this.active, priorResult = this.lastResult;
678
- const emptyTree = !current && !this.state.active_tree && !this.state.unclaimed_activity && !priorResult && !this.descendants.size;
679
- if (cancelTree) await this.cancel();
929
+ if (!cancelTree && !current && !priorResult) await this.reconcileRetainedAdmission();
930
+ if (cancelTree && (this.active || !this.receipt().settled)) await this.cancel();
680
931
  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");
681
932
  // A physical kill is settlement, not a native clean terminal receipt.
682
933
  // In particular, the previous Turn's SUCCESS cannot certify a cancelled
683
934
  // in-flight Turn that exited without its own result.
684
935
  const currentResult = !current || current.terminalResult === this.lastResult || this.lastResult !== priorResult;
685
- const clean = emptyTree || (currentResult && this.receipt().settled);
686
- return { clean, ...(clean ? {} : { native_settlement: { confirmed: false, reason: "native_terminal_unconfirmed" } }) };
936
+ if (currentResult && this.receipt().settled) await this.retireNativeServices();
937
+ const clean = currentResult && this.receipt().settled;
938
+ return { ...this.receipt(), clean, settled: Boolean(clean), native_settled: Boolean(clean), ...(clean ? {} : { native_settlement: { confirmed: false, reason: "native_terminal_unconfirmed" } }) };
939
+ }
940
+ async retireNativeServices() {
941
+ const services = [...(this.nativeResources?.values() ?? [])].filter(item => item.classification === 'support_service' && item.state !== 'stopped');
942
+ // Unknown classification/unsupported binaries keep the existing cleanup
943
+ // blocker. A fixture PID printed to stdout is never a production mapping.
944
+ if (!services.length || !this.nativeQualification?.qualified) return;
945
+ const revision = this.nativeRevision ?? 0;
946
+ const endpoint = this.nativeStopper ? null : await ownedAgyEndpoint(this.nativeLaunch);
947
+ const input = { endpoint, sessionId: this.init.conversation_id, cwd: this.config.cwd,
948
+ csrfToken: this.nativeCsrfToken, remainingMs: 10_000, rootRelation: this.nativeQualification.rootRelation };
949
+ const stopped = await (this.nativeStopper ?? stopAgyNativeTree)(input, [...this.descendants.keys()]);
950
+ const observed = await (this.nativeObserver ?? inspectAgyNative)(input);
951
+ if (!this.nativeStopper && !isDeepStrictEqual(await ownedAgyEndpoint(this.nativeLaunch), endpoint)
952
+ || !observed.idle || revision !== (this.nativeRevision ?? 0) || !this.receipt().settled)
953
+ throw new DaemonError('native_resources_unproven', 'AGY native service retirement lost its owner/execution fence');
954
+ // This is native handle retirement, not physical cleanup. Shared shutdown
955
+ // still re-observes the retained local PID/birth inventory after close.
956
+ for (const service of services) this.nativeResources.set(service.resource_id, { ...service, state: 'stopped',
957
+ native_retirement: { method: stopped.method, stopped_session_ids: stopped.stopped_session_ids, revision } });
958
+ await this.persist({ native_resources_entries: [...this.nativeResources.values()] });
687
959
  }
688
- 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) };
960
+ async cancel() { if (!this.active && this.receipt().settled) return { ...this.receipt(), cancelled: true }; this.nativeAbort?.abort(); this.nativeProof = null; this.nativeRevision = (this.nativeRevision ?? 0) + 1; this.state.native_execution_revision = this.nativeRevision; cancelPendingDispatch(this); if (!this.child) return this.receipt(); 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 };
689
961
  }
690
- 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(); }
962
+ 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) { if (!this.active && this.receipt().settled) await stopProcessGroup(this.child, 5_000); else await this.cancel(); } await this.finalizeProviderForControl(); }
691
963
  async abortProvider(reason) {
692
964
  const child = this.child, providerProcess = this.providerProcess;
693
965
  await stopProcessGroup(child);
@@ -742,7 +1014,7 @@ async function dispatch(runtime, request) {
742
1014
  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() }; }
743
1015
  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(); }
744
1016
  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); }
745
- if (request.operation === "session.inspect") { runtime.requireSessionIdentity(params.sessionId); await runtime.captureChildTools(); return runtime.receipt(); }
1017
+ if (request.operation === "session.inspect") { runtime.requireSessionIdentity(params.sessionId); await runtime.captureChildTools(); void runtime.reconcileRetainedAdmission().catch(error => { runtime.nativeInspection = { state: 'unproven', reason: error.details?.reason ?? error.code ?? 'native_admission_unproven' }; }); runtime.queueNativeInspection(); await runtime.persist({ native_observation_episode: runtime.episodeSnapshot() }); return runtime.receipt(); }
746
1018
  if (request.operation === "session.resume") { await runtime.start(params.sessionId ?? runtime.init?.conversation_id ?? null); runtime.requireSessionIdentity(params.sessionId); return runtime.receipt(); }
747
1019
  if (request.operation === "session.cancel") { runtime.requireSessionIdentity(params.sessionId); return await runtime.cancel(); }
748
1020
  if (request.operation === "hook.observe") {