@deksden-com/dd-flow-cli 0.9.0-beta.151 → 0.9.0-beta.155

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 (227) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/build-info.json +8 -8
  3. package/dist/cli/command-inputs.js +33 -3
  4. package/dist/cli/command-metadata.js +73 -2
  5. package/dist/cli/delivery.js +108 -0
  6. package/dist/cli/diagnostic-invocation.js +23 -7
  7. package/dist/cli/diagnostic.js +14 -3
  8. package/dist/cli/help.js +117 -1
  9. package/dist/cli/hook-ingress.js +36 -49
  10. package/dist/cli/interactive-invocation.js +33 -0
  11. package/dist/cli/interactive.js +114 -0
  12. package/dist/cli/memory-fix-invocation.js +52 -0
  13. package/dist/cli/memory-fix.js +100 -0
  14. package/dist/cli/memory-flow-admission.js +7 -3
  15. package/dist/cli/memory-init.js +5 -4
  16. package/dist/cli/native-hook-ingress.js +5 -1
  17. package/dist/cli/native-hook-preparation.js +31 -4
  18. package/dist/cli/run-cli.js +273 -19
  19. package/dist/cli.js +17 -3
  20. package/dist/domain/delivery-flows.js +8 -0
  21. package/dist/domain/distill-flow.js +20 -0
  22. package/dist/domain/interactive-flow.js +1 -0
  23. package/dist/domain/memory-fix-flow.js +2 -0
  24. package/dist/domain/review-fix-flow.js +2 -0
  25. package/dist/domain/source-snapshot-flows.js +4 -0
  26. package/dist/harness-runtime/bin/dd-agy.mjs +18 -3
  27. package/dist/harness-runtime/bin/dd-grok.mjs +15 -5
  28. package/dist/harness-runtime/bin/dd-zcode.mjs +15 -7
  29. package/dist/harness-runtime/lib/agy-error-boundary.mjs +18 -10
  30. package/dist/harness-runtime/lib/agy-native-inspection.mjs +12 -3
  31. package/dist/harness-runtime/lib/agy-native-inventory.mjs +19 -0
  32. package/dist/harness-runtime/lib/agy-terminal-envelope.mjs +13 -6
  33. package/dist/harness-runtime/lib/agy-transcript-stream.mjs +54 -0
  34. package/dist/harness-runtime/lib/daemon-observation.d.mts +1 -0
  35. package/dist/harness-runtime/lib/daemon-observation.mjs +12 -4
  36. package/dist/harness-runtime/lib/daemon-operations.mjs +20 -5
  37. package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +1 -0
  38. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +371 -114
  39. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +43 -3
  40. package/dist/harness-runtime/lib/dd-codex.mjs +36 -4
  41. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +25 -11
  42. package/dist/harness-runtime/lib/dd-droid.mjs +124 -11
  43. package/dist/harness-runtime/lib/dd-grok-daemon.d.mts +1 -0
  44. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +137 -25
  45. package/dist/harness-runtime/lib/dd-grok.mjs +4 -2
  46. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +170 -25
  47. package/dist/harness-runtime/lib/dd-opencode.mjs +40 -19
  48. package/dist/harness-runtime/lib/dd-zcode-daemon.d.mts +1 -0
  49. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +115 -22
  50. package/dist/harness-runtime/lib/dd-zcode.mjs +75 -36
  51. package/dist/harness-runtime/lib/delegation-instructions.d.mts +1 -0
  52. package/dist/harness-runtime/lib/delegation-instructions.mjs +10 -3
  53. package/dist/harness-runtime/lib/delivery-command.mjs +84 -0
  54. package/dist/harness-runtime/lib/driver-recovery.mjs +0 -19
  55. package/dist/harness-runtime/lib/droid-observation.mjs +9 -5
  56. package/dist/harness-runtime/lib/lifecycle-contract.d.mts +1 -0
  57. package/dist/harness-runtime/lib/lifecycle-contract.mjs +2 -1
  58. package/dist/harness-runtime/lib/native-hook-command.d.mts +3 -2
  59. package/dist/harness-runtime/lib/native-hook-command.mjs +63 -8
  60. package/dist/harness-runtime/lib/native-streams.mjs +3 -10
  61. package/dist/harness-runtime/lib/opencode-plugin.mjs +36 -9
  62. package/dist/harness-runtime/lib/operation-errors.mjs +13 -0
  63. package/dist/harness-runtime/lib/operation-progress.mjs +6 -2
  64. package/dist/harness-runtime/lib/process-json.mjs +4 -5
  65. package/dist/harness-runtime/lib/tool-observations.mjs +20 -10
  66. package/dist/harness-runtime/lib/zcode-native-hook-ownership.mjs +109 -0
  67. package/dist/schemas/code-work-result.schema.json +2 -1
  68. package/dist/schemas/delivery-stage-result.schema.json +286 -0
  69. package/dist/schemas/distill-stage-result.schema.json +667 -0
  70. package/dist/schemas/distill-work-result.schema.json +428 -0
  71. package/dist/schemas/execution-bundle.schema.json +6 -2
  72. package/dist/schemas/interactive-stage-result.schema.json +47 -0
  73. package/dist/schemas/interactive-work-result.schema.json +63 -0
  74. package/dist/schemas/mb-fix-stage-result.schema.json +93 -0
  75. package/dist/schemas/project-flow-pack-manifest.schema.json +2 -2
  76. package/dist/schemas/review-fix-stage-result.schema.json +210 -0
  77. package/dist/schemas/run-control-receipt.schema.json +1 -0
  78. package/dist/services/assigned-lifecycle.js +382 -0
  79. package/dist/services/check-runner-evidence.js +77 -10
  80. package/dist/services/check-runner-proof.js +69 -1
  81. package/dist/services/cli-operation-classifier.js +6 -1
  82. package/dist/services/code-checks.js +56 -23
  83. package/dist/services/codex-hook-delivery.js +11 -2
  84. package/dist/services/controller-execution.js +4 -1
  85. package/dist/services/controller-fanout.js +9 -4
  86. package/dist/services/controller-memory-worker.js +6 -4
  87. package/dist/services/controller-result-authority.js +122 -0
  88. package/dist/services/dashboard.js +4 -2
  89. package/dist/services/delivery-access.js +123 -0
  90. package/dist/services/delivery-approval.js +68 -0
  91. package/dist/services/delivery-artifact.js +86 -0
  92. package/dist/services/delivery-checks.js +178 -0
  93. package/dist/services/delivery-execution.js +181 -0
  94. package/dist/services/delivery-intents.js +212 -0
  95. package/dist/services/delivery-operation.js +81 -0
  96. package/dist/services/delivery-policy.js +91 -0
  97. package/dist/services/delivery-procedures.js +43 -0
  98. package/dist/services/delivery-request.js +99 -0
  99. package/dist/services/delivery-result.js +144 -0
  100. package/dist/services/delivery-stage-commands.js +26 -0
  101. package/dist/services/delivery-stage-entry.js +56 -0
  102. package/dist/services/delivery-stage-handlers.js +135 -0
  103. package/dist/services/delivery-stage-results.js +94 -0
  104. package/dist/services/descriptor-coordinator-session.js +3 -1
  105. package/dist/services/descriptor-run-start.js +24 -4
  106. package/dist/services/descriptor-stage-coordinator.js +78 -10
  107. package/dist/services/descriptor-stage-settlement.js +59 -14
  108. package/dist/services/diagnostic-repair-intake.js +123 -0
  109. package/dist/services/diagnostic-repair-source-intake.js +102 -0
  110. package/dist/services/diagnostic-report.js +30 -0
  111. package/dist/services/diagnostic-run-bundle.js +48 -9
  112. package/dist/services/diagnostic-snapshot.js +2 -2
  113. package/dist/services/diagnostic-stage-handlers.js +3 -2
  114. package/dist/services/diagnostic-stage-results.js +26 -0
  115. package/dist/services/diagnostic-start.js +56 -6
  116. package/dist/services/diagnostic-work-collection.js +3 -3
  117. package/dist/services/distill-document-publication.js +280 -0
  118. package/dist/services/distill-promotion.js +197 -0
  119. package/dist/services/distill-publication-custody.js +162 -0
  120. package/dist/services/distill-stage-handlers.js +249 -0
  121. package/dist/services/distill-stage-results.js +105 -0
  122. package/dist/services/distill-user-inputs.js +4 -0
  123. package/dist/services/distill-work-batch.js +42 -0
  124. package/dist/services/distill-work-collection.js +74 -0
  125. package/dist/services/distill-work-registration.js +93 -0
  126. package/dist/services/distill-work-results.js +98 -0
  127. package/dist/services/eval-snapshots.js +17 -7
  128. package/dist/services/external-work-launch.js +8 -2
  129. package/dist/services/flow-catalog.js +24 -0
  130. package/dist/services/frozen-engine.js +19 -2
  131. package/dist/services/generic-execution.js +4 -3
  132. package/dist/services/harness-adapter.js +2 -14
  133. package/dist/services/hook-memory-routing.js +1 -1
  134. package/dist/services/hooks.js +31 -6
  135. package/dist/services/hybrid-contracts.js +6 -1
  136. package/dist/services/installed-flow-source.js +37 -2
  137. package/dist/services/interactive-attached-session.js +215 -0
  138. package/dist/services/interactive-baseline-custody.js +54 -0
  139. package/dist/services/interactive-before-versions.js +148 -0
  140. package/dist/services/interactive-bundle-data.js +85 -0
  141. package/dist/services/interactive-checks.js +179 -0
  142. package/dist/services/interactive-execution.js +33 -0
  143. package/dist/services/interactive-finish-integration.js +236 -0
  144. package/dist/services/interactive-git-baseline.js +230 -0
  145. package/dist/services/interactive-git-dispatch.js +278 -0
  146. package/dist/services/interactive-input-delivery.js +82 -0
  147. package/dist/services/interactive-mutation-scope.js +231 -0
  148. package/dist/services/interactive-request.js +287 -0
  149. package/dist/services/interactive-run-bundle.js +109 -0
  150. package/dist/services/interactive-stage-entry.js +41 -0
  151. package/dist/services/interactive-stage-handlers.js +289 -0
  152. package/dist/services/interactive-stage-observation.js +136 -0
  153. package/dist/services/interactive-stage-results.js +111 -0
  154. package/dist/services/interactive-user-inputs.js +142 -0
  155. package/dist/services/interactive-work-batch.js +43 -0
  156. package/dist/services/interactive-work-collection.js +119 -0
  157. package/dist/services/interactive-work-completion.js +180 -0
  158. package/dist/services/interactive-work-context.js +249 -0
  159. package/dist/services/interactive-work-history.js +112 -0
  160. package/dist/services/interactive-work-registration.js +124 -0
  161. package/dist/services/interactive-work-results.js +115 -0
  162. package/dist/services/interactive-work-start.js +155 -0
  163. package/dist/services/lifecycle-invocations.js +214 -24
  164. package/dist/services/managed-daemon-binding.js +52 -1
  165. package/dist/services/memory-compose-handler.js +2 -22
  166. package/dist/services/memory-finish-integration.js +58 -17
  167. package/dist/services/memory-fix-finish-integration.js +129 -0
  168. package/dist/services/memory-fix-git-dispatch.js +139 -0
  169. package/dist/services/memory-fix-intake-custody.js +11 -0
  170. package/dist/services/memory-fix-intake-settlement.js +127 -0
  171. package/dist/services/memory-fix-publication.js +196 -0
  172. package/dist/services/memory-fix-repair-admission.js +116 -0
  173. package/dist/services/memory-fix-request.js +141 -0
  174. package/dist/services/memory-fix-run-bundle.js +83 -0
  175. package/dist/services/memory-fix-scope.js +78 -0
  176. package/dist/services/memory-fix-sources.js +85 -0
  177. package/dist/services/memory-fix-stage-entry.js +24 -0
  178. package/dist/services/memory-fix-stage-handlers.js +282 -0
  179. package/dist/services/memory-fix-stage-results.js +106 -0
  180. package/dist/services/memory-fix-workspace.js +127 -0
  181. package/dist/services/memory-flow-user-inputs.js +88 -0
  182. package/dist/services/memory-init-contexts.js +55 -5
  183. package/dist/services/memory-init-controller-start.js +20 -4
  184. package/dist/services/memory-layer.js +7 -3
  185. package/dist/services/memory-native-review-copy.js +27 -3
  186. package/dist/services/memory-repair-document-delta.js +1 -0
  187. package/dist/services/project-flow-source.js +195 -0
  188. package/dist/services/project-git-integration.js +92 -0
  189. package/dist/services/repair-intents.js +2 -4
  190. package/dist/services/retained-native-hook.js +245 -0
  191. package/dist/services/review-fix-handoff.js +82 -0
  192. package/dist/services/review-fix-intake-custody.js +17 -0
  193. package/dist/services/review-fix-stage-entry.js +43 -0
  194. package/dist/services/review-fix-stage-handlers.js +176 -0
  195. package/dist/services/review-fix-stage-results.js +86 -0
  196. package/dist/services/run-control-worker.js +42 -1
  197. package/dist/services/run-control.js +40 -5
  198. package/dist/services/run-controller-adapter.js +37 -3
  199. package/dist/services/run-controller-process.js +12 -4
  200. package/dist/services/run-controller-recovery.js +46 -6
  201. package/dist/services/run-controller-state.js +3 -2
  202. package/dist/services/run-controller.js +209 -43
  203. package/dist/services/run-recovery.js +31 -2
  204. package/dist/services/run-timeline.js +59 -0
  205. package/dist/services/runs.js +18 -31
  206. package/dist/services/sessions.js +2 -1
  207. package/dist/services/stage-handlers.js +55 -3
  208. package/dist/services/stage-lifecycle.js +4 -3
  209. package/dist/services/stage-pause.js +26 -9
  210. package/dist/services/stage-report-renderer.js +2 -6
  211. package/dist/services/technical-notes.js +31 -0
  212. package/dist/services/verification-matrix.js +1 -1
  213. package/dist/services/version-status.js +1 -1
  214. package/dist/services/vnext-code-review.js +74 -5
  215. package/dist/services/vnext-code.js +9 -32
  216. package/dist/services/vnext-fanout.js +53 -8
  217. package/dist/services/vnext-merge.js +10 -5
  218. package/dist/services/vnext-plan-review.js +1 -1
  219. package/dist/services/vnext-plan.js +1 -1
  220. package/dist/services/vnext-protocolize.js +1 -1
  221. package/dist/services/vnext-specify.js +10 -5
  222. package/dist/services/work-registry.js +252 -66
  223. package/dist/services/worktrees.js +7 -4
  224. package/dist/storage/database.js +2 -1
  225. package/dist/verification-components.json +82 -0
  226. package/package.json +4 -3
  227. package/tools/verification-jobs.mjs +342 -0
@@ -3,27 +3,29 @@ 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';
6
+ import { inspectAgyNative, inspectAgyNativeState, stopAgyNativeTree, ownedAgyEndpoint, readAgyNativeQualification, AGY_NATIVE_INSPECTION_CONTRACT } from './agy-native-inspection.mjs';
7
+ import { agyNativeInventory } from './agy-native-inventory.mjs';
7
8
  import { processSnapshot } from './process-snapshot.mjs';
8
9
  import { streamRequestKey } from './provider-overload-policy.mjs';
9
10
  import { captureDispatchGuard, cancelPendingDispatch } from "./dispatch-fence.mjs";
10
- import { errorRecord, isProvenShellCorrection } from "./operation-errors.mjs";
11
- import { ToolObservations, agyToolObservation, agyTranscriptObservations } from "./tool-observations.mjs";
11
+ import { errorRecord } from "./operation-errors.mjs";
12
+ import { ToolObservations, agyToolObservation, createAgyTranscriptObserver } from "./tool-observations.mjs";
13
+ import { readAgyTranscriptSnapshot } from "./agy-transcript-stream.mjs";
12
14
  import { constants } from "node:fs";
13
15
  import { fileURLToPath, pathToFileURL } from "node:url";
14
16
  import { createHash, randomUUID } from "node:crypto";
15
- import { appendFile, chmod, lstat, mkdir, open, readFile, realpath, rename, unlink, writeFile } from "node:fs/promises";
17
+ import { appendFile, chmod, lstat, mkdir, open, readFile, readdir, realpath, rename, unlink, writeFile } from "node:fs/promises";
16
18
  import { spawn } from "node:child_process";
17
19
  import net from "node:net";
18
20
  import { isDeepStrictEqual } from "node:util";
19
21
  import { ObservationClock, observedTimeout, validateObservationDuration } from "./observation-clock.mjs";
20
- import { hookBudget } from "./native-hook-command.mjs";
22
+ import { hookBudget, retainNativeWorkStart, isStandaloneWorkStart } from "./native-hook-command.mjs";
23
+ import { parseLifecycleCommand } from "./lifecycle-command.mjs";
21
24
  import { agyWorkspaceHooks } from "./agy-hooks.mjs";
22
25
  import { durableDaemonDispatch, completeDaemonShutdown, assertDaemonDispatch, assertDaemonRestartable, trackDaemonConnections, retireDaemonEndpoint, retireFailedDaemon } from "./daemon-operations.mjs";
23
26
  import path from "node:path";
24
27
  import { observeModel } from "./model-observations.mjs";
25
28
  import { AGY_VERSION_TIMEOUT_MS, AgyError, agyTerminalFailure, agyModelArgs, assertProfile, executable, observedProfile, runAgy, usageSnapshot, managedAgyEnv } from "./dd-agy.mjs";
26
- import { recoveredAgyErrorBoundary } from "./agy-error-boundary.mjs";
27
29
  import { agyTerminalEnvelope } from "./agy-terminal-envelope.mjs";
28
30
  import { settledNativeTree, nativeExecutionReceipt } from "./native-children.mjs";
29
31
  import { retainStartupFailure, prepareRuntimeOwner, cleanupFailedStart, cleanupFailedDaemonStart, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, scheduleDaemonProcessHeartbeat, registerDaemonProcess, stopProcessGroup } from "./managed-daemon.mjs";
@@ -35,20 +37,22 @@ class DaemonError extends AgyError {}
35
37
  // classify services. This transcript format has no tool-call IDs: parallel
36
38
  // calls therefore retain unknown resources, never FIFO-derived authority.
37
39
  export function agyTranscriptFacts(text, source, sessionId) {
38
- const children = [], resources = new Map(), calls = [];
39
- const result = () => ({ children, resources: [...resources.values()] });
40
- if (!source?.locator || !source?.sha256 || !sessionId) return result();
41
- const lines = text.split('\n');
40
+ const reducer = createAgyTranscriptFacts(source, sessionId), lines = text.split('\n');
42
41
  for (let index = 0; index < lines.length; index++) {
43
42
  const line = lines[index];
44
43
  if (!line && index === lines.length - 1) continue;
45
44
  let event;
46
- try { event = JSON.parse(line); } catch {
47
- // A writer may have appended only part of the last record. Retain the
48
- // valid prefix; a malformed newline-terminated record is a real gap.
49
- return index === lines.length - 1 ? result() : { ...result(), gap: 'agy_transcript_invalid_record' };
50
- }
51
- if (!event || typeof event !== 'object' || Array.isArray(event)) return { ...result(), gap: 'agy_transcript_invalid_record' };
45
+ try { event = JSON.parse(line); } catch { if (index < lines.length - 1) reducer.gap(); break; }
46
+ reducer.add(event);
47
+ }
48
+ return reducer.finish();
49
+ }
50
+ export function createAgyTranscriptFacts(source, sessionId) {
51
+ const children = [], resources = new Map(), calls = [];
52
+ let invalid = false;
53
+ function add(event) {
54
+ if (invalid || !source?.locator || !source?.sha256 || !sessionId) return;
55
+ if (!event || typeof event !== 'object' || Array.isArray(event) || event.conversation_id && event.conversation_id !== sessionId) { invalid = true; return; }
52
56
  const content = typeof event.content === 'string' ? event.content : '';
53
57
  const native_source = { ...source, step_index: event.step_index };
54
58
  if (event.source === 'SYSTEM' && event.type === 'SYSTEM_MESSAGE' && event.status === 'DONE') {
@@ -57,11 +61,11 @@ export function agyTranscriptFacts(text, source, sessionId) {
57
61
  const prior = resources.get(terminal.task_id);
58
62
  if (prior) resources.set(prior.resource_id, mergeAgyResource(prior, { ...prior, state: 'stopped', ...(terminal.command_outcome ? { native_outcome: terminal.command_outcome } : {}), terminal_source: native_source }));
59
63
  }
60
- continue;
64
+ return;
61
65
  }
62
- if (event.source !== 'MODEL') continue;
63
- 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; }
64
- if (event.type !== 'GENERIC') continue;
66
+ if (event.source !== 'MODEL') return;
67
+ 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]); return; }
68
+ if (event.type !== 'GENERIC') return;
65
69
  const call = calls.shift();
66
70
  if (call?.name === 'manage_task' && event.status === 'DONE') {
67
71
  const terminal = agyTerminalEnvelope(event, call), prior = resources.get(terminal?.task_id);
@@ -69,22 +73,22 @@ export function agyTranscriptFacts(text, source, sessionId) {
69
73
  }
70
74
  if (call?.name === 'invoke_subagent' && event.status === 'DONE') {
71
75
  const match = content.match(/^Created At: [^\n]+\nCompleted At: [^\n]+\nCreated the following subagent:\n(\{[\s\S]*?\})\nThe subagent will send you a message/);
72
- if (!match) continue;
76
+ if (!match) return;
73
77
  try {
74
78
  const child = JSON.parse(match[1]);
75
79
  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 });
76
80
  } catch { /* No partial/guessed native edge. */ }
77
81
  }
78
- if (event.status !== 'RUNNING') continue;
82
+ if (event.status !== 'RUNNING') return;
79
83
  const task = content.match(/^Created At: [^\n]+\nTool is running as a background task with task id: ([\w-]+\/task-\d+)\n/);
80
- if (!task || !task[1].startsWith(`${sessionId}/`)) continue;
84
+ if (!task || !task[1].startsWith(`${sessionId}/`)) return;
81
85
  let daemon = call?.name === 'run_command' ? call.args?.IsDaemon : null;
82
86
  if (typeof daemon === 'string') { try { daemon = JSON.parse(daemon); } catch { daemon = null; } }
83
- const resource = { resource_id: task[1], provider_session_id: sessionId, classification: daemon === true ? 'support_service' : daemon === false ? 'required_work' : 'unknown', state: 'unknown', native_source,
87
+ const resource = { resource_id: task[1], provider_session_id: sessionId, ...(call?.name === 'schedule' ? { kind: 'schedule' } : {}), classification: daemon === true ? 'support_service' : daemon === false ? 'required_work' : 'unknown', state: 'unknown', native_source,
84
88
  ...(typeof daemon === 'boolean' ? { native_classification: { tool: 'run_command', IsDaemon: daemon } } : {}) };
85
89
  resources.set(resource.resource_id, mergeAgyResource(resources.get(resource.resource_id), resource));
86
90
  }
87
- return result();
91
+ return { add, gap: () => { invalid = true; }, finish: () => ({ children, resources: [...resources.values()], ...(invalid ? { gap: 'agy_transcript_invalid_record' } : {}) }) };
88
92
  }
89
93
  export function mergeAgyResource(prior, next) {
90
94
  if (!prior) return next;
@@ -104,7 +108,7 @@ export function mergeAgyResource(prior, next) {
104
108
  return { ...prior, ...next };
105
109
  }
106
110
  export const agyTranscriptResources = (text, source, sessionId) => agyTranscriptFacts(text, source, sessionId).resources;
107
- export async function captureAgyTranscript(runtimeRoot, child, rootSessionId, ownedSessionIds = [rootSessionId], inspect = null) {
111
+ export async function captureAgyTranscript(runtimeRoot, child, rootSessionId, ownedSessionIds = [rootSessionId], inspect = null, onProgress = null) {
108
112
  const session = { harness_id: "antigravity-cli", session_id: child.provider_session_id };
109
113
  let handle;
110
114
  try {
@@ -117,15 +121,20 @@ export async function captureAgyTranscript(runtimeRoot, child, rootSessionId, ow
117
121
  handle = await open(expected, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
118
122
  const stat = await handle.stat();
119
123
  if (!before.isFile() || !stat.isFile() || stat.dev !== before.dev || stat.ino !== before.ino || await realpath(expected) !== canonical) throw new Error("native locator changed during open");
120
- const maximum = 8 * 1024 * 1024, buffer = Buffer.alloc(Math.min(stat.size, maximum));
121
- const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
122
- const bytes = buffer.subarray(0, bytesRead), prefix = bytes.subarray(0, bytes.lastIndexOf(10) + 1);
123
- const source = { locator: child.log_uri, byte_start: 0, byte_end: prefix.length, sha256: createHash("sha256").update(prefix).digest("hex") };
124
- if (prefix.length === bytesRead && stat.size <= maximum) inspect?.(prefix.toString("utf8"), source);
125
- const events = agyTranscriptObservations(prefix.toString("utf8"), child.provider_session_id, child.parent_provider_session_id ?? rootSessionId, source);
126
- if (prefix.length !== bytesRead || stat.size > maximum) events.push({ session, reason: stat.size > maximum ? "agy_transcript_size_limit" : "agy_transcript_partial_tail" });
124
+ let observer, inspector;
125
+ const snapshot = await readAgyTranscriptSnapshot(handle, { locator: child.log_uri, onProgress,
126
+ assertIdentity: async current => { const pathStat = await lstat(expected); if (!pathStat.isFile() || current.dev !== pathStat.dev || current.ino !== pathStat.ino || await realpath(expected) !== canonical) throw new Error('native locator changed during read'); },
127
+ onRecord: (record, { line, ...source }) => {
128
+ observer ??= createAgyTranscriptObserver(child.provider_session_id, child.parent_provider_session_id ?? rootSessionId, source);
129
+ inspector ??= inspect?.(source);
130
+ observer.add(record, line); inspector?.add(record);
131
+ } });
132
+ observer ??= createAgyTranscriptObserver(child.provider_session_id, child.parent_provider_session_id ?? rootSessionId, snapshot.source);
133
+ if (!snapshot.partial_tail) inspector?.finish();
134
+ const events = observer.finish();
135
+ if (snapshot.partial_tail) events.push({ session, reason: "agy_transcript_partial_tail" });
127
136
  return events;
128
- } catch (error) { return [{ session, reason: error.code === "ENOENT" ? "agy_transcript_missing" : "agy_transcript_locator_rejected" }]; }
137
+ } catch (error) { return [{ session, reason: error.code === "ENOENT" ? "agy_transcript_missing" : error.message === 'agy_transcript_invalid_record' ? 'agy_transcript_invalid_record' : "agy_transcript_locator_rejected" }]; }
129
138
  finally { await handle?.close(); }
130
139
  }
131
140
  const absolute = (value, label) => { if (!value || !path.isAbsolute(value)) throw new DaemonError("invalid_path", `${label} must be an absolute path`); return path.resolve(value); };
@@ -141,6 +150,7 @@ async function readState(dir) { try { return JSON.parse(await readFile(locations
141
150
  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; } }
142
151
  const shell = value => `'${String(value).replaceAll("'", `'"'"'`)}'`;
143
152
  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"]) env[key] = undefined; return managedAgyEnv({ ...env, ...managedFlowEnv(config) }); }
153
+
144
154
  const errorPayload = errorRecord;
145
155
  async function forwardUsage(config, sessionId, result, tools, descendants) {
146
156
  if (!config.ddFlowBin || !config.projectRoot) return;
@@ -328,7 +338,7 @@ export class Runtime {
328
338
  if (!current) return;
329
339
  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");
330
340
  current.terminalProgress ??= new Set();
331
- if (current.terminalProgress.has(key) || current.terminalProgress.size >= 1_024) return;
341
+ if (current.terminalProgress.has(key)) return;
332
342
  current.terminalProgress.add(key); this.markActivity();
333
343
  }
334
344
  observeProviderActivity(event) {
@@ -353,8 +363,7 @@ export class Runtime {
353
363
  const snapshots = prior?.index === index ? prior.seen : new Set();
354
364
  if (snapshots.has(fingerprint)) return;
355
365
  // Native step snapshots can alternate/replay; scope retained dedup to the
356
- // current step. Too many unique snapshots becomes uncertain, never a grant.
357
- if (snapshots.size >= 1_024) return;
366
+ // current step. Unique later updates remain useful activity at any size.
358
367
  snapshots.add(fingerprint); this.progressSteps.set(id, { index, seen: snapshots }); this.invalidateSettlement(id); this.markActivity();
359
368
  }
360
369
  async start(sessionId = null) {
@@ -463,7 +472,7 @@ export class Runtime {
463
472
  }
464
473
  }
465
474
  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 }; }
466
- async captureChildTools() {
475
+ async captureChildTools(onProgress = null) {
467
476
  this.capturedToolFacts ??= new Map();
468
477
  this.nativeResources ??= new Map(this.state.native_resources_entries?.map(item => [item.resource_id, item]) ?? []);
469
478
  const root = this.init?.conversation_id ?? this.state.sessions?.[0]?.provider_session_id ?? this.lastResult?.conversation_id;
@@ -478,8 +487,10 @@ export class Runtime {
478
487
  ancestors.add(parent); parent = this.descendants.get(parent).parent_provider_session_id;
479
488
  }
480
489
  const events = parent === root && root
481
- ? await captureAgyTranscript(this.paths.runtime, child, root, [root, ...ancestors], (text, source) => {
482
- const facts = agyTranscriptFacts(text, source, child.provider_session_id);
490
+ ? await captureAgyTranscript(this.paths.runtime, child, root, [root, ...ancestors], source => {
491
+ const reducer = createAgyTranscriptFacts(source, child.provider_session_id);
492
+ return { add: reducer.add, finish: () => {
493
+ const facts = reducer.finish();
483
494
  for (const resource of facts.resources) this.nativeResources.set(resource.resource_id, mergeAgyResource(this.nativeResources.get(resource.resource_id), resource));
484
495
  if (facts.gap) { this.state.native_topology_error = facts.gap; this.invalidateSettlement(root); }
485
496
  for (const discovered of facts.children) {
@@ -497,7 +508,8 @@ export class Runtime {
497
508
  this.descendants.set(value.provider_session_id, value);
498
509
  queue.push(value);
499
510
  }
500
- })
511
+ } };
512
+ }, onProgress && (cursor => onProgress(`${child.provider_session_id}:${cursor}`)))
501
513
  : [{ session: { harness_id: "antigravity-cli", session_id: child.provider_session_id }, reason: "agy_transcript_ancestry_unproven" }];
502
514
  for (const event of events) {
503
515
  this.toolObservations.add(event);
@@ -542,10 +554,7 @@ export class Runtime {
542
554
  return;
543
555
  }
544
556
  this.lastResult = result; this.markTerminalActivity(result);
545
- const rootStop = this.sessionObservations.get(this.init?.conversation_id)?.stop;
546
- const pendingTerminal = current && result.status === "SUCCESS";
547
557
  if (current) { current.terminalResult = result; current.terminalMetadata = terminalMetadata; current.terminalPersisted = false; }
548
- 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" });
549
558
  this.state.terminal_observation_pending = true;
550
559
  try {
551
560
  await this.persist({ active_tree: true, last_result: result, last_terminal_metadata: terminalMetadata });
@@ -571,14 +580,6 @@ export class Runtime {
571
580
  if (this.active !== current) return;
572
581
  this.active = null;
573
582
  const settled = this.receipt().settled;
574
- if (result.status === "ERROR" && settled && rootStop?.fullyIdle === true && !this.state.last_hook_rejection && current.operationId && this.paths.runtime) {
575
- const root = this.init.conversation_id, transcript = path.join(this.paths.runtime, "brain", root, ".system_generated", "logs", "transcript.jsonl");
576
- await captureAgyTranscript(this.paths.runtime, { provider_session_id: root, parent_provider_session_id: root, log_uri: new URL(`file://${transcript}`).href }, root, [root], (text, source) => {
577
- const boundary = recoveredAgyErrorBoundary(text, result.error, current.stepFloor, source, root);
578
- const hook = current.nativeHookEvents?.get(boundary?.invocation_step ?? boundary?.boundary_step);
579
- if (boundary && hook) terminalMetadata.recovered_error_boundary = { ...boundary, receipt: { ...boundary.receipt, native_hook_event_key: hook, daemon_instance: this.config.daemonId }, source };
580
- });
581
- }
582
583
  this.lastTerminalFailure = agyTerminalFailure(result, { ...terminalMetadata, native_turn_items: { observed: settled, possible_effects: true, pending: !settled }, provider_error_scope: "terminal_result", usage_ingest: this.usageIngest });
583
584
  try { await this.persist({ active_tree: !settled, last_terminal_failure: errorRecord(this.lastTerminalFailure) }); }
584
585
  catch (error) { return this.failTerminalObservation(current, result, terminalMetadata, error); }
@@ -602,6 +603,17 @@ export class Runtime {
602
603
  this.active = null;
603
604
  current.resolve(receipt);
604
605
  }
606
+ reconcilePendingHook(current) {
607
+ if (!current || this.active !== current) return Promise.resolve();
608
+ if (current.hookReconciliation) return current.hookReconciliation;
609
+ if (!current.pendingHookReconciliation) return Promise.resolve();
610
+ current.pendingHookReconciliation = false;
611
+ current.hookReconciliation = this.completePendingTurn().catch(error => {
612
+ current.observationError ??= error;
613
+ if (this.active === current) { this.drainingError ??= error; current.reject(error); }
614
+ }).finally(() => { current.hookReconciliation = null; });
615
+ return current.hookReconciliation;
616
+ }
605
617
  failTerminalObservation(current, result, metadata, cause) {
606
618
  this.state.terminal_observation_pending = true;
607
619
  const details = { ...metadata, native_outcome: result, settled: false, terminal_persisted: current?.terminalPersisted === true, observation_error: errorRecord(cause), native_turn_items: { observed: false, possible_effects: true, pending: true } };
@@ -712,7 +724,6 @@ export class Runtime {
712
724
  // proof (or verified prior clean retirement before new activity) closes it.
713
725
  const retained = this.state.retained_tree_settlement;
714
726
  const rootSettled = this.nativeProof?.turn_generation === this.turnGeneration && this.nativeProof.connection_id === this.connectionId && this.nativeProof.revision === (this.nativeRevision ?? 0)
715
- || observation?.stop?.source_bound === true && observation.stop?.fullyIdle === true && observation.stop.generation === this.turnGeneration
716
727
  || retained?.evidence === "verified_clean_daemon_shutdown" && retained.session_id === root && retained.turn_generation === this.turnGeneration && !this.child;
717
728
  const terminalPending = this.active?.terminalResult === result;
718
729
  // A positively inspected owned scope before its first Turn has no model
@@ -724,7 +735,8 @@ export class Runtime {
724
735
  if (clock) clock.sample(this.progress);
725
736
  const exhausted = clock && (clock.observationLost || clock.elapsed >= clock.timeoutMs);
726
737
  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() } : {}) };
727
- 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 ?? [])] } });
738
+ const hookDiagnostics = (this.state.native_hook_diagnostics ?? []).filter(item => item.turn_generation === this.turnGeneration);
739
+ 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 }), ...(hookDiagnostics.length ? { native_hook_diagnostics: hookDiagnostics } : {}), 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 ?? [])], ...(this.state.native_retirement_notifications?.length ? { pending_notifications: this.state.native_retirement_notifications } : {}) } });
728
740
  }
729
741
  async zeroUsage() { await forwardUsage(this.config, this.init.conversation_id, { usage: {} }, this.toolSnapshot(), []); }
730
742
  async prompt(text, assertDispatch = captureDispatchGuard(this), operationId = null, livenessTimeoutMs = 1_800_000) {
@@ -735,14 +747,13 @@ export class Runtime {
735
747
  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 });
736
748
  await this.reconcileRetainedAdmission();
737
749
  if (this.active || this.promptObservation) throw new DaemonError("operation_in_progress", "an Antigravity turn or its mandatory dispatch observation is already running");
738
- 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 });
739
- 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");
750
+ if (this.state.unclaimed_activity || ((this.state.active_tree || this.lastResult) && !this.receipt().settled)) throw new DaemonError("tree_not_settled", "the prior Antigravity agent tree is not fully settled");
740
751
  let resolveTurn, rejectTurn;
741
752
  const reply = new Promise((resolve, reject) => { resolveTurn = resolve; rejectTurn = reject; });
742
753
  // Cancellation can reject while admission persistence is still pending.
743
754
  void reply.catch(() => {});
744
755
  this.connectionId ??= randomUUID();
745
- const current = { resolve: resolveTurn, reject: rejectTurn, generation: ++this.turnGeneration, operationId, connectionId: this.connectionId, dispatched: false };
756
+ const current = { resolve: resolveTurn, reject: rejectTurn, generation: ++this.turnGeneration, operationId, connectionId: this.connectionId, dispatched: false, guard: assertDispatch };
746
757
  this.nativeAbort?.abort(); this.nativeProof = null; this.nativeInspection = null;
747
758
  current.clock = new ObservationClock({ timeoutMs: livenessTimeoutMs, onGap: gap => { void this.journal('observation_gap', { gap, operation_id: operationId }).catch(() => {}); } });
748
759
  this.observationEpisode = current;
@@ -756,11 +767,15 @@ export class Runtime {
756
767
  // Reserve before persistence so different operation IDs cannot overlap.
757
768
  current.publishProgress = operationProgressReporter();
758
769
  this.active = current;
759
- this.hookRequests = new Map();
770
+ this.lastNativeHookOperation = current;
771
+ // A callback's failure receipt may arrive after its Turn has returned.
772
+ // Keep its accepted request identity; it is an observation binding, not
773
+ // proof of which native Turn originally produced the callback.
774
+ this.hookRequests ??= new Map();
760
775
  const root = this.init?.conversation_id, prior = this.sessionObservations.get(root) ?? {};
761
776
  current.stepFloor = prior.last_step_index ?? -1;
762
777
  this.sessionObservations.set(root, { ...prior, step_floor: prior.last_step_index ?? null, stop: null, last_hook: null });
763
- this.lastProviderEvent = null; this.progressSteps = new Map(); this.progressHooks = new Set(); this.progress = undefined;
778
+ this.lastProviderEvent = null; this.progressSteps = new Map(); this.progressHooks = new Map(); this.progress = undefined;
764
779
  try {
765
780
  await this.persist({ active_tree: true, last_hook_rejection: null });
766
781
  assertDispatch();
@@ -783,7 +798,15 @@ export class Runtime {
783
798
  const clock = current.clock;
784
799
  let checking = false;
785
800
  const timer = setInterval(() => {
801
+ if (this.active === current && [...(this.hookRequests?.values() ?? [])].some(binding => binding.current && binding.current !== current)) {
802
+ void this.observeHookFailures().catch(() => { /* Retry the same retained request on the next observation. */ });
803
+ }
786
804
  if (this.active === current && current.terminalResult && this.nativeInspection?.state === 'inspection_pending' && !this.nativeFlight) this.queueNativeInspection();
805
+ if (this.active === current && current.pendingHookReconciliation && !checking) {
806
+ checking = true;
807
+ void this.reconcilePendingHook(current).finally(() => { checking = false; });
808
+ return;
809
+ }
787
810
  if (this.active !== current || checking || !clock.sample(this.progress)) return;
788
811
  checking = true; const cursor = this.progress?.cursor;
789
812
  void this.completePendingTurn().then(() => {
@@ -856,56 +879,169 @@ export class Runtime {
856
879
  if (observation?.stop) this.sessionObservations.set(id, { ...observation, stop: null });
857
880
  }
858
881
  }
859
- async childForHook(conversationId) {
882
+ qualifiedChildForHook(conversationId) {
883
+ // A fully source-bound in-memory ancestry is already the result of the
884
+ // observation loop. Avoid re-reading every transcript on each child hook.
885
+ const retained = this.descendants.get(conversationId), seenRetained = new Set();
886
+ let node = retained, expected = conversationId, qualified = Boolean(node && !this.state.native_topology_error);
887
+ while (qualified) {
888
+ const source = node?.native_source, parent = node?.parent_provider_session_id;
889
+ let sourceBound = source?.kind === 'agy.step_update.subagent_info'
890
+ && source.parent_provider_session_id === parent && Number.isSafeInteger(source.step_index) && source.step_index >= 0;
891
+ if (!sourceBound && typeof source?.locator === 'string' && /^[a-f0-9]{64}$/.test(source?.sha256 ?? '') && typeof parent === 'string') {
892
+ try {
893
+ const locator = fileURLToPath(source.locator);
894
+ sourceBound = locator.endsWith(path.sep + path.join('brain', parent, '.system_generated', 'logs', 'transcript.jsonl'));
895
+ } catch { /* An invalid locator never grants a fast-path identity. */ }
896
+ }
897
+ if (!node || node.provider_session_id !== expected || !sourceBound || !parent || seenRetained.has(expected)
898
+ || source.parent_provider_session_id && source.parent_provider_session_id !== parent) { qualified = false; break; }
899
+ seenRetained.add(node.provider_session_id);
900
+ if (parent === this.init?.conversation_id) break;
901
+ expected = parent;
902
+ node = this.descendants.get(parent);
903
+ if (!node) qualified = false;
904
+ }
905
+ return qualified ? retained : null;
906
+ }
907
+ async childForHook(conversationId, onProgress = null) {
860
908
  if (conversationId === this.init?.conversation_id) return null;
861
- await this.captureChildTools();
909
+ const retained = this.qualifiedChildForHook(conversationId);
910
+ if (retained) return retained;
911
+ await this.captureChildTools(onProgress);
862
912
  const deadline = Date.now() + 1_000;
863
- while (!this.descendants.has(conversationId) && Date.now() < deadline) { await new Promise(resolve => setTimeout(resolve, 25)); await this.captureChildTools(); }
913
+ while (!this.descendants.has(conversationId) && Date.now() < deadline) { await new Promise(resolve => setTimeout(resolve, 25)); await this.captureChildTools(onProgress); }
864
914
  const child = this.descendants.get(conversationId);
865
915
  if (!child) throw new DaemonError("agy_child_identity_unconfirmed", "Antigravity hook has no confirmed native parent", true, { conversation_id: conversationId });
866
916
  const seen = new Set([conversationId]); let current = child;
867
- while (current.parent_provider_session_id !== this.init?.conversation_id) {
917
+ for (;;) {
918
+ if (this.state.native_topology_error || current.native_source?.parent_provider_session_id && current.native_source.parent_provider_session_id !== current.parent_provider_session_id)
919
+ throw new DaemonError("agy_child_parent_unqualified", "Antigravity native ancestry is contradictory");
920
+ if (current.parent_provider_session_id === this.init?.conversation_id) break;
868
921
  const parent = this.descendants.get(current.parent_provider_session_id);
869
- 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 });
922
+ if (!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 });
870
923
  seen.add(parent.provider_session_id); current = parent;
871
924
  }
872
925
  if (this.state.native_topology_error) throw new DaemonError("agy_child_parent_unqualified", "Antigravity native ancestry is contradictory");
873
926
  return child;
874
927
  }
875
- async rejectHook(error, event, payload, current = this.active) {
876
- if (current && this.active !== current) return;
877
- const correctable = current && event === "PreToolUse" && isProvenShellCorrection(error);
878
- if (correctable && current.hookCorrectionGeneration !== current.generation) {
879
- // The request was correlated with this still-active Turn by
880
- // observeHookFailures. Persist before allowing native same-Turn recovery.
881
- const correction = { code: error.code, message: error.message, details: error.details, event, conversation_id: payload.conversationId ?? null,
882
- daemon_id: this.config.daemonId, operation_id: current.operationId, turn_generation: current.generation };
883
- try {
884
- await hookBudget(1_000, null).run(async () => { await this.journal("hook_correction", { correction }); await this.persist({ last_hook_correction: correction }); }, "correction_persistence");
885
- current.hookCorrectionGeneration = current.generation;
886
- return;
887
- } 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 } }); }
928
+ async registerWorkStart(event, payload, callbackRequestId = null) {
929
+ if (event !== "PreToolUse" || !this.config.runtime_owner || !this.config.ddFlowBin || !this.init?.conversation_id
930
+ || typeof payload.conversationId !== "string" || !payload.conversationId
931
+ || !Number.isSafeInteger(payload.stepIdx) || payload.stepIdx < 0
932
+ || payload.executionNum !== undefined && (!Number.isSafeInteger(payload.executionNum) || payload.executionNum < 0)
933
+ || !["run_command", "run_terminal_command", "RunTerminalCommand"].includes(payload.toolCall?.name)) return null;
934
+ const input = payload.toolCall?.args ?? {}, command = input.command ?? input.cmd ?? input.CommandLine;
935
+ if (!isStandaloneWorkStart(command)) return null;
936
+ assertDaemonDispatch(this.paths.dir);
937
+ const tool = createHash("sha256").update(JSON.stringify([this.config.daemonId, payload.conversationId, payload.stepIdx, payload.executionNum ?? null, event])).digest("hex");
938
+ this.retainedWorkStarts ??= new Map();
939
+ this.retainedWorkActivity ??= new Set();
940
+ if (!this.retainedWorkActivity.has(tool)) {
941
+ this.retainedWorkActivity.add(tool);
942
+ // A callback is activity even before its ancestry is published. It
943
+ // invalidates old idle, but cannot identify or finish a creator prompt.
944
+ this.childActivity(payload.conversationId);
888
945
  }
889
- 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 };
890
- this.state.last_hook_rejection = rejection;
891
- if (current && this.active === current) { this.active = null; current.reject(new DaemonError(rejection.code, rejection.message, false, rejection)); }
892
- try { await hookBudget(1_000, null).run(async () => { await this.journal("hook_rejected", { rejection }); await this.persist({ last_hook_rejection: rejection }); }, "failure_persistence"); }
893
- catch (storageError) { rejection.receipt_error = { code: storageError.code, message: storageError.message }; }
946
+ return await retainNativeWorkStart(this.paths.dir, { daemon_id: this.config.daemonId, source_harness: "antigravity-cli",
947
+ root_provider_session_id: this.init.conversation_id, provider_session_id: payload.conversationId, parent_provider_session_id: null,
948
+ tool_call_id: tool, event, native_command: command, native_cwd: this.config.cwd,
949
+ ...(callbackRequestId ? { callback_request_id: callbackRequestId } : {}),
950
+ native_payload: { schema_id: "dd-flow/agy-tool-event@1", event_id: tool, phase: "before", daemon_id: this.config.daemonId,
951
+ launch_cwd: this.config.cwd, conversation_id: payload.conversationId, execution_num: payload.executionNum, step_index: payload.stepIdx,
952
+ tool: payload.toolCall.name, input, workspace_paths: payload.workspacePaths, transcript_path: payload.transcriptPath,
953
+ profile: payload.model ? observedProfile(payload) : {} } }, this.active ?? this.lastNativeHookOperation, this.retainedWorkStarts);
954
+ }
955
+ async verifyWorkStart(params) {
956
+ const retained = this.retainedWorkStarts?.get(params.requestId);
957
+ if (!retained || !isDeepStrictEqual(retained.binding, params.binding)) throw new DaemonError("native_hook_unproven", "Unknown native Work start callback");
958
+ retained.guard(); assertDaemonDispatch(this.paths.dir);
959
+ if (retained.binding.root_provider_session_id !== this.init?.conversation_id) throw new DaemonError("native_hook_unproven", "Native Work start root changed");
960
+ const publish = operationProgressReporter();
961
+ const child = await this.childForHook(retained.binding.provider_session_id,
962
+ cursor => publish('verification_progress', { native_request_id: cursor, session_id: retained.binding.provider_session_id }));
963
+ if (retained.binding.provider_session_id !== this.init.conversation_id && this.qualifiedChildForHook(retained.binding.provider_session_id) !== child)
964
+ throw new DaemonError("native_hook_unproven", "Native Work start child has no source-bound ancestry");
965
+ retained.guard();
966
+ const parent = child?.parent_provider_session_id ?? null;
967
+ const verified = { ...retained.binding, parent_provider_session_id: parent, native_ancestry: "confirmed",
968
+ native_payload: { ...retained.binding.native_payload, parent_conversation_id: parent } };
969
+ if (retained.binding.callback_request_id) this.hookRequests?.delete(retained.binding.callback_request_id);
970
+ return { verified: true, registeredHookBinding: retained.binding, nativeHookBinding: verified };
971
+ }
972
+ async recordUnconfirmedHookFailure(error, event, payload, observer, requestId = null) {
973
+ // The request was observed while this operation was active, but AGY does
974
+ // not expose the native creator Turn. Preserve the refusal without
975
+ // attributing it to (or aborting) this potentially unrelated prompt.
976
+ const key = typeof requestId === 'string' && /^[A-Za-z0-9-]{1,128}$/.test(requestId) ? requestId : null;
977
+ const scope = key && JSON.stringify([key, observer?.operationId ?? null, observer?.generation ?? null]);
978
+ this.unconfirmedHookFlights ??= new Map();
979
+ if (scope && this.unconfirmedHookFlights.has(scope)) return await this.unconfirmedHookFlights.get(scope);
980
+ const flight = (async () => {
981
+ let diagnostic = key && (this.state.native_hook_diagnostics ?? []).find(item => item.request_id === key
982
+ && item.operation_id === (observer?.operationId ?? null) && item.turn_generation === (observer?.generation ?? null));
983
+ try {
984
+ if (!diagnostic) {
985
+ diagnostic = { code: 'native_hook_failed', message: 'Native hook failed without a verified creator Turn',
986
+ ...(key ? { request_id: key } : {}), primary_error: errorRecord(error), event, conversation_id: payload.conversationId ?? null,
987
+ daemon_id: this.config.daemonId, operation_id: observer?.operationId ?? null,
988
+ turn_generation: observer?.generation ?? null, native_creator: 'unconfirmed' };
989
+ // Do not grow in-memory state when durable journal writing fails.
990
+ await this.journal('unconfirmed_hook_failure', diagnostic);
991
+ this.state.native_hook_diagnostics = [...(this.state.native_hook_diagnostics ?? []), diagnostic];
992
+ }
993
+ await this.persist(); return true;
994
+ } catch { return false; /* Optional diagnostic cannot erase a native operation. */ }
995
+ })();
996
+ if (scope) this.unconfirmedHookFlights.set(scope, flight);
997
+ try { return await flight; }
998
+ finally { if (scope && this.unconfirmedHookFlights.get(scope) === flight) this.unconfirmedHookFlights.delete(scope); }
894
999
  }
895
1000
  async observeHookFailures() {
896
- for (const [requestId, binding] of this.hookRequests ?? []) {
897
- if (binding.current !== this.active || !this.active) continue;
898
- let failure;
899
- try { failure = JSON.parse(await readFile(path.join(this.paths.dir, "hook-failures", `${requestId}.json`), "utf8")); }
900
- catch (error) { if (error.code === "ENOENT") continue; failure = { code: "native_hook_receipt_invalid", message: error.message }; }
901
- if (failure.request_id !== undefined && failure.request_id !== requestId) continue;
902
- this.hookRequests.delete(requestId);
903
- await this.rejectHook(Object.assign(new Error(failure.message ?? "Native hook failed"), { code: failure.code, details: failure.details }), binding.event, binding.payload, binding.current);
904
- }
1001
+ if (!this.hookRequests?.size) return;
1002
+ if (this.hookFailuresFlight) return await this.hookFailuresFlight;
1003
+ const flight = (async () => {
1004
+ let names;
1005
+ try { names = new Set(await readdir(path.join(this.paths.dir, "hook-failures"))); }
1006
+ catch (error) { if (error.code === "ENOENT") return; throw error; }
1007
+ for (const [requestId, binding] of this.hookRequests ?? []) {
1008
+ if (!binding.current || !names.has(`${requestId}.json`)) continue;
1009
+ let failure;
1010
+ try { failure = JSON.parse(await readFile(path.join(this.paths.dir, "hook-failures", `${requestId}.json`), "utf8")); }
1011
+ catch (error) { if (error.code === "ENOENT") continue; failure = { code: "native_hook_receipt_invalid", message: error.message }; }
1012
+ if (failure.request_id !== undefined && failure.request_id !== requestId) continue;
1013
+ if (binding.current !== this.active) {
1014
+ if (failure.request_id !== requestId) continue;
1015
+ // The callback request was accepted while this observer operation was
1016
+ // active. AGY has no native creator ID, so this is diagnostic only:
1017
+ // never mutate either the old receipt or the current Turn.
1018
+ await this.journal("late_hook_receipt", { request_id: requestId, daemon_id: this.config.daemonId,
1019
+ observer_operation_id: binding.current.operationId ?? null, observer_turn_generation: binding.current.generation ?? null,
1020
+ native_creator: "unconfirmed", event: binding.event, failure: errorRecord(failure) });
1021
+ this.hookRequests.delete(requestId);
1022
+ continue;
1023
+ }
1024
+ const saved = await this.recordUnconfirmedHookFailure(Object.assign(new Error(failure.message ?? "Native hook failed"), { code: failure.code, details: failure.details }), binding.event, binding.payload, binding.current, requestId);
1025
+ if (saved) this.hookRequests.delete(requestId);
1026
+ }
1027
+ })();
1028
+ this.hookFailuresFlight = flight;
1029
+ try { await flight; } finally { if (this.hookFailuresFlight === flight) this.hookFailuresFlight = null; }
905
1030
  }
906
1031
  async observeHook(event, payload, budget = null) {
1032
+ const hookOwner = this.active, hookGeneration = this.turnGeneration;
907
1033
  const conversationId = payload.conversationId ?? this.init?.conversation_id;
908
- const child = await this.childForHook(conversationId); budget?.remaining("identity");
1034
+ const shell = ["run_command", "run_terminal_command", "RunTerminalCommand"].includes(payload.toolCall?.name);
1035
+ const args = payload.toolCall?.args ?? {}, command = args.command ?? args.cmd ?? args.CommandLine;
1036
+ const observationOnly = Boolean(this.config.runtime_owner && this.config.ddFlowBin && (event === "Stop" || !shell
1037
+ || typeof command === "string" && parseLifecycleCommand(command).participation === "none"));
1038
+ // Ordinary native facts cannot grant Work authority. Keep them cheap;
1039
+ // independent observation verifies ancestry/idle after the callback ACK.
1040
+ const child = observationOnly ? this.qualifiedChildForHook(conversationId) : await this.childForHook(conversationId); budget?.remaining("identity");
1041
+ if (this.active !== hookOwner || this.turnGeneration !== hookGeneration) {
1042
+ await this.journal("stale_hook", { hook_event: event, conversation_id: conversationId, reason: "owner_changed_during_identity" });
1043
+ return { stale: true };
1044
+ }
909
1045
  const prior = this.sessionObservations.get(conversationId) ?? {};
910
1046
  const execution = Number.isInteger(payload.executionNum) ? payload.executionNum : null;
911
1047
  const step = Number.isInteger(payload.stepIdx) ? payload.stepIdx : null;
@@ -915,37 +1051,52 @@ export class Runtime {
915
1051
  if (stale) { await this.journal("stale_hook", { hook_event: event, conversation_id: conversationId, step_index: step }); return { stale: true }; }
916
1052
  const fingerprint = createHash("sha256").update(JSON.stringify({ event, payload })).digest("hex");
917
1053
  if (event === "Stop" && prior.last_stop_hook === fingerprint && prior.last_stop_generation === this.turnGeneration) return { stale: true };
918
- this.progressHooks ??= new Set();
1054
+ this.progressHooks ??= new Map();
919
1055
  const scoped = this.active && (!child || child.activity_generation === this.active.generation);
920
- const sourceBound = Boolean(scoped && step !== null && step > (this.active.stepFloor ?? -1)
921
- && this.progressSteps?.get(conversationId)?.index === step);
1056
+ // Native hook payloads/step updates carry no creator prompt identity.
1057
+ // A late old step above the new floor is indistinguishable from a new one:
1058
+ // Stop remains a hint, never a whole-tree or child idle grant.
1059
+ const sourceBound = false;
922
1060
  const progressKey = JSON.stringify([conversationId, step, execution, event, payload.toolCall ?? null, payload.fullyIdle ?? null]);
923
- 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(); }
1061
+ if (scoped && step !== null && (["PreToolUse", "PostToolUse"].includes(event) || event === "Stop" && payload.fullyIdle === true)) {
1062
+ let progress = this.progressHooks.get(conversationId);
1063
+ if (!progress || progress.step !== step) { progress = { step, seen: new Set() }; this.progressHooks.set(conversationId, progress); }
1064
+ if (!progress.seen.has(progressKey)) { progress.seen.add(progressKey); this.markActivity(); }
1065
+ }
924
1066
  if (conversationId) this.sessionObservations.set(conversationId, { ...prior,
925
1067
  ...(step !== null ? { last_step_index: step, last_execution_num: execution ?? (step === prior.last_step_index ? prior.last_execution_num : null) } : {}),
926
1068
  last_hook: fingerprint, ...(event === "PreToolUse" ? { last_pre_hook: fingerprint } : {}),
927
1069
  ...(payload.transcriptPath ? { transcript_path: payload.transcriptPath } : {}),
928
1070
  ...(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 } } : {}) });
929
1071
  if (event === "PreToolUse" && prior.last_pre_hook !== fingerprint) this.childActivity(conversationId);
930
- 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" });
931
- const terminalPending = this.active?.terminalResult === this.lastResult;
932
- 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" });
933
1072
  const parent = child?.parent_provider_session_id ?? null;
934
1073
  if (event === "Stop" && !stale) {
935
1074
  const observation = this.sessionObservations.get(conversationId);
936
1075
  this.sessionObservations.set(conversationId, { ...observation, stop: { ...observation.stop, generation: this.turnGeneration } });
937
1076
  }
938
1077
  const observed = payload.model ? observedProfile(payload) : {};
939
- 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" });
1078
+ // An omitted model carries no profile evidence. The hook journal below
1079
+ // retains that fact; do not read/lock/fsync the model history for every
1080
+ // ordinary tool or Stop callback. Actual model observations stay intact.
1081
+ if (payload.model && !observationOnly) await observeModel({ journal: this.config.journal, harness: "antigravity-cli", sessionId: conversationId, parentSessionId: parent, requested: this.config, observed, source: { channel: "native.hook", scope: "configured" }, evidence: "configured" });
1082
+ if (this.active !== hookOwner || this.turnGeneration !== hookGeneration) {
1083
+ await this.journal("stale_hook", { hook_event: event, conversation_id: conversationId, reason: "owner_changed_during_model_observation" });
1084
+ return { stale: true };
1085
+ }
940
1086
  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");
941
- 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 });
942
- if (!stale && !child && event === "PreToolUse" && Number.isInteger(step) && this.active) { this.active.nativeHookEvents ??= new Map(); this.active.nativeHookEvents.set(step, `${eventId}:before`); }
1087
+ await this.persist(); await this.journal("hook", { stale, hook_event: event, event_id: eventId, conversation_id: conversationId, turn_generation: hookGeneration, execution_num: execution, step_index: step, termination_reason: payload.terminationReason ?? null, fully_idle: payload.fullyIdle ?? null, ...(payload.model ? {} : { model_observation: "unavailable" }), persistence_timing: this.persistTiming ?? null });
1088
+ if (this.active !== hookOwner || this.turnGeneration !== hookGeneration) return { stale: true };
943
1089
  this.queueNativeInspection();
944
- 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 };
1090
+ // A provider may wait for this hook's ACK before emitting the very event
1091
+ // needed to finish the turn. The fact above is persisted before ACK;
1092
+ // completion reconciliation belongs to the daemon, not the callback.
1093
+ if (hookOwner && this.active === hookOwner && this.turnGeneration === hookGeneration) hookOwner.pendingHookReconciliation = true;
1094
+ return { observationOnly, event_id: eventId, source_bound: sourceBound, 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 };
945
1095
  }
946
1096
  async finalizeProviderForControl() {
947
1097
  const pending = this.providerFinalization;
948
1098
  try {
1099
+ while (this.active?.pendingHookReconciliation || this.active?.hookReconciliation) await this.reconcilePendingHook(this.active);
949
1100
  try { await pending; }
950
1101
  catch (error) {
951
1102
  // Control retries only retained physical/resource cleanup, never a
@@ -958,6 +1109,7 @@ export class Runtime {
958
1109
  }
959
1110
  }
960
1111
  await this.promptObservation?.dispatchJournal;
1112
+ await this.observeHookFailures();
961
1113
  } catch (error) {
962
1114
  if (!this.drainingError) throw error;
963
1115
  this.drainingError.cleanup_error = errorRecord(error);
@@ -975,29 +1127,109 @@ export class Runtime {
975
1127
  // In particular, the previous Turn's SUCCESS cannot certify a cancelled
976
1128
  // in-flight Turn that exited without its own result.
977
1129
  const currentResult = !current || current.terminalResult === this.lastResult || this.lastResult !== priorResult;
978
- if (currentResult && this.receipt().settled) await this.retireNativeServices();
979
1130
  const clean = currentResult && this.receipt().settled;
980
1131
  return { ...this.receipt(), clean, settled: Boolean(clean), native_settled: Boolean(clean), ...(clean ? {} : { native_settlement: { confirmed: false, reason: "native_terminal_unconfirmed" } }) };
981
1132
  }
982
1133
  async retireNativeServices() {
1134
+ if (!this.nativeQualification?.qualified) return;
1135
+ this.nativeResources ??= new Map((this.state.native_resources_entries ?? []).map(item => [item.resource_id, item]));
983
1136
  const services = [...(this.nativeResources?.values() ?? [])].filter(item => item.classification === 'support_service' && item.state !== 'stopped');
984
- // Unknown classification/unsupported binaries keep the existing cleanup
985
- // blocker. A fixture PID printed to stdout is never a production mapping.
986
- if (!services.length || !this.nativeQualification?.qualified) return;
1137
+ const schedules = [...(this.nativeResources?.values() ?? [])].filter(item => item.state !== 'stopped' &&
1138
+ (item.kind === 'schedule' || item.classification === 'unknown'));
1139
+ // Unknown task semantics can be reconciled by exact native handle inventory,
1140
+ // not by inventing a shell outcome or trusting a PID printed to stdout.
987
1141
  const revision = this.nativeRevision ?? 0;
988
1142
  const endpoint = this.nativeStopper ? null : await ownedAgyEndpoint(this.nativeLaunch);
989
1143
  const input = { endpoint, sessionId: this.init.conversation_id, cwd: this.config.cwd,
990
1144
  csrfToken: this.nativeCsrfToken, remainingMs: 10_000, rootRelation: this.nativeQualification.rootRelation };
991
- const stopped = await (this.nativeStopper ?? stopAgyNativeTree)(input, [...this.descendants.keys()]);
992
- const observed = await (this.nativeObserver ?? inspectAgyNative)(input);
1145
+ const clock = new ObservationClock({ timeoutMs: input.remainingMs });
1146
+ let progress = 0;
1147
+ const requestInput = () => {
1148
+ if (clock.sample()) throw new DaemonError('native_resources_unproven', 'AGY native retirement observation budget exhausted');
1149
+ const remainingMs = Math.max(1, Math.ceil(clock.timeoutMs - clock.elapsed));
1150
+ return { ...input, remainingMs };
1151
+ };
1152
+ const inventory = async () => {
1153
+ if (this.state.native_topology_error) throw new DaemonError('native_resources_unproven', 'AGY native topology is incomplete');
1154
+ const root = this.init.conversation_id, nodes = [{ provider_session_id: root, parent_provider_session_id: '' }, ...this.descendants.values()];
1155
+ for (const node of nodes.slice(1)) {
1156
+ const seen = new Set([node.provider_session_id]); let parent = node.parent_provider_session_id;
1157
+ while (parent !== root) {
1158
+ if (!parent || seen.has(parent)) throw new DaemonError('native_resources_unproven', 'AGY native resource ancestry is unbound');
1159
+ seen.add(parent); parent = this.descendants.get(parent)?.parent_provider_session_id;
1160
+ }
1161
+ }
1162
+ const result = [];
1163
+ for (const node of nodes) {
1164
+ const snapshot = await (this.nativeStateObserver ?? inspectAgyNativeState)({ ...requestInput(),
1165
+ sessionId: node.provider_session_id, rootSessionId: root, parentSessionId: node.parent_provider_session_id });
1166
+ result.push({ session_id: node.provider_session_id, snapshot, ...agyNativeInventory(snapshot, node.provider_session_id) });
1167
+ clock.sample(++progress); // A completed node read, not observer polling.
1168
+ }
1169
+ return result;
1170
+ };
1171
+ // Historical transcript envelopes need not explain whether a timer fired
1172
+ // or was cancelled. Fresh native absence proves retirement, not success.
1173
+ // The transcript ledger is partial. It cannot decide whether a fresh native
1174
+ // inventory is needed: even a missing ledger may hide an active daemon task.
1175
+ let states = await inventory();
1176
+ const required = states.find(state => !state.idle || state.tasks.some(task => task.isDaemon !== true));
1177
+ if (required) {
1178
+ this.childActivity(required.session_id);
1179
+ throw new DaemonError('native_resources_unproven', 'AGY required execution became active before native resource retirement');
1180
+ }
1181
+ const notificationKey = (session, trajectory, message) => JSON.stringify([session, trajectory, message.id, message.sender, message.recipient]);
1182
+ const notifications = new Map((this.state.native_retirement_notifications ?? []).filter(item => item.owner_daemon_id === this.config.daemonId)
1183
+ .map(item => [notificationKey(item.session_id, item.trajectory_id, item), item]));
1184
+ if (states.some(state => state.messages.some(message => !notifications.has(notificationKey(state.session_id, state.snapshot.trajectoryId, message)))))
1185
+ throw new DaemonError('native_resources_unproven', 'AGY has pending delivery before native resource retirement');
1186
+ const stopSenders = new Set(['system', ...states.map(state => state.session_id), ...states.flatMap(state => state.tasks.map(task => task.taskId))]);
1187
+ const trajectories = new Map(states.map(state => [state.session_id, state.snapshot.trajectoryId]));
1188
+ const activeTasks = states.some(state => state.snapshot.hasActiveChildren === true || state.tasks.length > 0);
1189
+ const stopped = services.length || activeTasks
1190
+ ? await (this.nativeStopper ?? stopAgyNativeTree)(requestInput(), [...this.descendants.keys()]) : null;
1191
+ if (stopped) clock.sample(++progress);
1192
+ const observed = await (this.nativeObserver ?? inspectAgyNative)(requestInput());
1193
+ clock.sample(++progress);
993
1194
  if (!this.nativeStopper && !isDeepStrictEqual(await ownedAgyEndpoint(this.nativeLaunch), endpoint)
994
1195
  || !observed.idle || revision !== (this.nativeRevision ?? 0) || !this.receipt().settled)
995
1196
  throw new DaemonError('native_resources_unproven', 'AGY native service retirement lost its owner/execution fence');
1197
+ {
1198
+ states = await inventory();
1199
+ if (states.some(state => !state.idle || trajectories.get(state.session_id) !== state.snapshot.trajectoryId ||
1200
+ state.tasks.length) ||
1201
+ observed.trajectory_id && observed.trajectory_id !== trajectories.get(this.init.conversation_id) ||
1202
+ revision !== (this.nativeRevision ?? 0) || !this.receipt().settled ||
1203
+ !this.nativeStopper && !isDeepStrictEqual(await ownedAgyEndpoint(this.nativeLaunch), endpoint))
1204
+ throw new DaemonError('native_resources_unproven', 'AGY schedule retirement lacks fresh idle inventory and required delivery');
1205
+ for (const state of states) for (const message of state.messages) {
1206
+ const key = notificationKey(state.session_id, state.snapshot.trajectoryId, message);
1207
+ if (notifications.has(key)) continue;
1208
+ if (!stopped || typeof message.id !== 'string' || !message.id || message.recipient !== state.session_id || !stopSenders.has(message.sender))
1209
+ throw new DaemonError('native_resources_unproven', 'AGY post-stop delivery has no owned native control provenance');
1210
+ // Native cancellation creates notices after required execution is
1211
+ // already settled. They remain pending technical evidence until the
1212
+ // fenced provider is closed; do not prompt a model or claim delivery.
1213
+ notifications.set(key, { id: message.id, sender: message.sender, recipient: message.recipient, session_id: state.session_id,
1214
+ trajectory_id: state.snapshot.trajectoryId, owner_daemon_id: this.config.daemonId, generation: this.turnGeneration, revision,
1215
+ status: 'pending', disposition: 'after_owned_native_stop', method: stopped.method });
1216
+ }
1217
+ for (const schedule of schedules) {
1218
+ const state = states.find(item => item.session_id === schedule.provider_session_id);
1219
+ if (!state || state.tasks.some(task => task.taskId === schedule.resource_id))
1220
+ throw new DaemonError('native_resources_unproven', 'AGY schedule owner or retirement is unproven');
1221
+ this.nativeResources.set(schedule.resource_id, { ...schedule, state: 'stopped', native_retirement: {
1222
+ method: state.snapshot.method, reason: 'native_task_inventory_absent', revision,
1223
+ trajectory_id: state.snapshot.trajectoryId, observed_at: state.snapshot.observed_at } });
1224
+ }
1225
+ }
996
1226
  // This is native handle retirement, not physical cleanup. Shared shutdown
997
1227
  // still re-observes the retained local PID/birth inventory after close.
998
1228
  for (const service of services) this.nativeResources.set(service.resource_id, { ...service, state: 'stopped',
999
1229
  native_retirement: { method: stopped.method, stopped_session_ids: stopped.stopped_session_ids, revision } });
1000
- await this.persist({ native_resources_entries: [...this.nativeResources.values()] });
1230
+ await this.persist({ native_resources_entries: [...this.nativeResources.values()], native_retirement_notifications: [...notifications.values()] });
1231
+ if (revision !== (this.nativeRevision ?? 0) || !this.receipt().settled)
1232
+ throw new DaemonError('native_resources_unproven', 'AGY resource retirement was fenced during publication');
1001
1233
  }
1002
1234
  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 };
1003
1235
  }
@@ -1059,30 +1291,55 @@ async function dispatch(runtime, request) {
1059
1291
  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(); }
1060
1292
  if (request.operation === "session.resume") { await runtime.start(params.sessionId ?? runtime.init?.conversation_id ?? null); runtime.requireSessionIdentity(params.sessionId); return runtime.receipt(); }
1061
1293
  if (request.operation === "session.cancel") { runtime.requireSessionIdentity(params.sessionId); return await runtime.cancel(); }
1294
+ if (request.operation === "hook.verify") {
1295
+ if (params.daemonId !== runtime.config.daemonId) throw new DaemonError("agy_hook_binding_invalid", "Antigravity hook binding does not name this daemon");
1296
+ assertDaemonDispatch(runtime.paths.dir);
1297
+ if (params.binding?.native_admission === "assigned_work_start") return await runtime.verifyWorkStart(params);
1298
+ const retained = runtime.verifiedHookFacts?.get(params.requestId);
1299
+ if (!retained || !isDeepStrictEqual(retained.binding, params.binding)) throw new DaemonError("native_hook_unproven", "Hook fact is not bound to a verified native delivery");
1300
+ retained.guard();
1301
+ return { verified: true, nativeHookBinding: retained.binding };
1302
+ }
1303
+ if (request.operation === "hook.complete") {
1304
+ if (params.daemonId !== runtime.config.daemonId)
1305
+ throw new DaemonError("agy_hook_binding_invalid", "Antigravity hook binding does not name this daemon");
1306
+ if (typeof params.requestId !== 'string' || !/^[A-Za-z0-9-]{1,128}$/.test(params.requestId))
1307
+ throw new DaemonError("agy_hook_binding_invalid", "Antigravity callback request ID is invalid");
1308
+ // Administrative ACK only: the success of a callback is not proof that a
1309
+ // Work mutation occurred or that a native agent Turn is complete.
1310
+ return { retired: runtime.hookRequests?.delete(params.requestId) === true };
1311
+ }
1062
1312
  if (request.operation === "hook.observe") {
1063
1313
  // Check the process binding before correlating a request with an active
1064
1314
  // turn. A stale/foreign daemon must not poison an unrelated operation.
1065
1315
  if (runtime.config.hookBinding === "environment-v1" && params.daemonId !== runtime.config.daemonId) throw new DaemonError("agy_hook_binding_invalid", "Antigravity hook binding does not name this daemon", false, { expected_daemon_id: runtime.config.daemonId, observed_daemon_id: params.daemonId ?? null });
1066
1316
  const current = runtime.active, budget = hookBudget(25_000, params.deadlineAt);
1067
1317
  runtime.hookRequests ??= new Map();
1068
- if (/^[A-Za-z0-9-]{1,128}$/.test(request.id)) runtime.hookRequests.set(request.id, { current, event: params.event, payload: { conversationId: params.payload?.conversationId, toolCall: { name: params.payload?.toolCall?.name } } });
1318
+ if (/^[A-Za-z0-9-]{1,128}$/.test(request.id)) runtime.hookRequests.set(request.id, { current: current ?? runtime.lastNativeHookOperation, event: params.event, payload: { conversationId: params.payload?.conversationId, toolCall: { name: params.payload?.toolCall?.name } } });
1069
1319
  try {
1320
+ const deferred = await budget.run(() => runtime.registerWorkStart(params.event, params.payload ?? {}, request.id), "registration");
1321
+ if (deferred) return { fastAck: true, nativeHookBinding: deferred };
1070
1322
  const result = await budget.run(async () => {
1071
1323
  if (runtime.hookRequests.has(request.id) && current?.operationId) {
1072
1324
  const directory = path.join(runtime.paths.dir, "operations", createHash("sha256").update(current.operationId).digest("hex"), "hooks");
1073
1325
  await mkdir(directory, { recursive: true, mode: 0o700 });
1074
- await writeJson(path.join(directory, `${request.id}.json`), { schema_id: "dd-flow/hook-request@1", request_id: request.id, daemon_id: runtime.config.daemonId, operation_id: current.operationId, turn_generation: current.generation, event: params.event, conversation_id: params.payload?.conversationId ?? null, tool: params.payload?.toolCall?.name ?? null });
1326
+ await writeJson(path.join(directory, `${request.id}.json`), { schema_id: "dd-flow/hook-request@1", request_id: request.id, daemon_id: runtime.config.daemonId, operation_id: current.operationId, turn_generation: current.generation, native_creator: "unconfirmed", event: params.event, conversation_id: params.payload?.conversationId ?? null, tool: params.payload?.toolCall?.name ?? null });
1075
1327
  }
1076
1328
  budget.remaining("observation");
1077
1329
  return await runtime.observeHook(params.event, params.payload ?? {}, budget);
1078
1330
  }, "observation");
1331
+ // The AGY stream does not prove which native prompt produced a step.
1332
+ // An unseen old step can exceed a newer prompt's observed floor. Do not
1333
+ // mint an optional CLI admission from event order; legacy hook performs
1334
+ // its existing scoped checks until a native creator binding exists.
1079
1335
  await runtime.observeHookFailures(); budget.remaining("observation");
1080
- if (current && runtime.state.last_hook_rejection?.turn_generation === current.generation) throw Object.assign(new Error(runtime.state.last_hook_rejection.message), runtime.state.last_hook_rejection);
1336
+ if (result.stale) runtime.hookRequests.delete(request.id);
1081
1337
  return result;
1082
1338
  }
1083
- catch (error) { await runtime.rejectHook(error, params.event, params.payload ?? {}, current); throw error; }
1339
+ catch (error) { await runtime.recordUnconfirmedHookFailure(error, params.event, params.payload ?? {}, current, request.id); throw error; }
1084
1340
  }
1085
1341
  if (request.operation === "daemon.stop") return await completeDaemonShutdown(runtime.paths.dir, runtime.state, patch => runtime.persist(patch), () => runtime.prepareStop(params.cancelTree === true), [["provider", () => runtime.close(false)], ["daemon_resource", () => runtime.finishResource()]], async () => {
1342
+ await runtime.retireNativeServices();
1086
1343
  await runtime.captureChildTools();
1087
1344
  return runtime.receipt().native_resources;
1088
1345
  });