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

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 (197) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +13 -8
  3. package/dist/build-info.json +7 -7
  4. package/dist/cli/command-inputs.js +9 -3
  5. package/dist/cli/command-metadata.js +33 -14
  6. package/dist/cli/help.js +32 -1
  7. package/dist/cli/hook-ingress.js +27 -3
  8. package/dist/cli/memory-flow-admission.js +7 -3
  9. package/dist/cli/memory-init-invocation.js +10 -3
  10. package/dist/cli/memory-init.js +74 -68
  11. package/dist/cli/memory-storage-admission.js +3 -2
  12. package/dist/cli/memory-storage-qualification.js +105 -0
  13. package/dist/cli/memory-storage-transition.js +123 -0
  14. package/dist/cli/memory-upgrade.js +189 -0
  15. package/dist/cli/native-hook-ingress.js +14 -4
  16. package/dist/cli/native-hook-preparation.js +55 -0
  17. package/dist/cli/response-file.js +2 -1
  18. package/dist/cli/run-cli.js +70 -32
  19. package/dist/cli/setup.js +2 -1
  20. package/dist/cli.js +12 -0
  21. package/dist/domain/stage-handler-identities.js +8 -0
  22. package/dist/harness-runtime/bin/dd-agy.mjs +3 -3
  23. package/dist/harness-runtime/bin/dd-grok.mjs +3 -3
  24. package/dist/harness-runtime/bin/dd-zcode.mjs +5 -5
  25. package/dist/harness-runtime/lib/agy-error-boundary.mjs +58 -0
  26. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +62 -20
  27. package/dist/harness-runtime/lib/dd-agy.mjs +17 -3
  28. package/dist/harness-runtime/lib/dd-droid.mjs +1 -1
  29. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +70 -13
  30. package/dist/harness-runtime/lib/dd-grok.mjs +6 -3
  31. package/dist/harness-runtime/lib/dd-zcode.mjs +44 -14
  32. package/dist/harness-runtime/lib/delegation-instructions.mjs +1 -1
  33. package/dist/harness-runtime/lib/entity-references.d.mts +2 -0
  34. package/dist/harness-runtime/lib/entity-references.mjs +9 -0
  35. package/dist/harness-runtime/lib/managed-daemon.mjs +13 -6
  36. package/dist/harness-runtime/lib/native-hook-command.mjs +4 -4
  37. package/dist/harness-runtime/lib/opencode-plugin.mjs +3 -3
  38. package/dist/harness-runtime/lib/operation-errors.mjs +1 -1
  39. package/dist/harness-runtime/lib/provider-overload-policy.d.mts +4 -2
  40. package/dist/harness-runtime/lib/provider-overload-policy.mjs +19 -6
  41. package/dist/harness-runtime/lib/runner-events.mjs +3 -1
  42. package/dist/harness-runtime/lib/setup-progress.mjs +8 -6
  43. package/dist/schemas/mb-upgrade-stage-result.schema.json +863 -0
  44. package/dist/schemas/memory-repair-work-result.schema.json +9 -2
  45. package/dist/schemas/release-impact.schema.json +3 -3
  46. package/dist/schemas/stage-start-response.schema.json +4 -0
  47. package/dist/services/accepted-intent.js +13 -1
  48. package/dist/services/canon.js +43 -7
  49. package/dist/services/cli-operation-classifier.js +1 -1
  50. package/dist/services/cli-upgrade-allowlist.js +1 -1
  51. package/dist/services/code-checks.js +11 -4
  52. package/dist/services/compatibility-preflight.js +5 -2
  53. package/dist/services/controller-fanout.js +21 -4
  54. package/dist/services/controller-memory-task.js +45 -0
  55. package/dist/services/controller-memory-worker.js +48 -0
  56. package/dist/services/dashboard.js +2 -2
  57. package/dist/services/descriptor-run-start.js +26 -19
  58. package/dist/services/descriptor-stage-coordinator.js +30 -10
  59. package/dist/services/descriptor-stage-start.js +40 -4
  60. package/dist/services/engines.js +64 -14
  61. package/dist/services/eval-snapshots.js +104 -93
  62. package/dist/services/external-work-launch.js +5 -3
  63. package/dist/services/flow-catalog.js +8 -4
  64. package/dist/services/frozen-engine.js +113 -11
  65. package/dist/services/hook-memory-routing.js +188 -0
  66. package/dist/services/hook-recovery-guard.js +14 -0
  67. package/dist/services/hook-session-observation.js +24 -0
  68. package/dist/services/hooks.js +27 -8
  69. package/dist/services/hybrid-contracts.js +2 -13
  70. package/dist/services/hybrid-digest.js +14 -0
  71. package/dist/services/installed-flow-source.js +25 -4
  72. package/dist/services/lifecycle-invocations.js +33 -5
  73. package/dist/services/memory-accepted-composition.js +5 -4
  74. package/dist/services/memory-completed-correction-wave.js +23 -15
  75. package/dist/services/memory-compose-handler.js +163 -21
  76. package/dist/services/memory-composition-acceptance.js +186 -10
  77. package/dist/services/memory-composition-graph.js +42 -11
  78. package/dist/services/memory-composition-registration.js +23 -8
  79. package/dist/services/memory-composition-work-custody.js +14 -9
  80. package/dist/services/memory-composition-work-results.js +15 -3
  81. package/dist/services/memory-corrected-review-batch.js +10 -3
  82. package/dist/services/memory-corrected-review-findings.js +4 -2
  83. package/dist/services/memory-corrected-review-inputs.js +10 -5
  84. package/dist/services/memory-corrected-review-receipt.js +17 -9
  85. package/dist/services/memory-corrected-review-registration.js +3 -2
  86. package/dist/services/memory-correction-check-receipt.js +20 -4
  87. package/dist/services/memory-custody-snapshot.js +36 -0
  88. package/dist/services/memory-finish-evidence.js +44 -22
  89. package/dist/services/memory-finish-handler.js +37 -23
  90. package/dist/services/memory-finish-integration.js +218 -48
  91. package/dist/services/memory-finish-readiness.js +66 -19
  92. package/dist/services/memory-git-integration.js +87 -18
  93. package/dist/services/memory-historical-corrected-review.js +21 -15
  94. package/dist/services/memory-historical-original-review.js +22 -9
  95. package/dist/services/memory-historical-repair-registration.js +14 -8
  96. package/dist/services/memory-historical-second-review.js +30 -11
  97. package/dist/services/memory-init-contexts.js +28 -2
  98. package/dist/services/memory-init-controller-start.js +19 -8
  99. package/dist/services/memory-init-git-policy.js +50 -21
  100. package/dist/services/memory-init-lint-capability.js +21 -3
  101. package/dist/services/memory-init-native-capacity.js +13 -0
  102. package/dist/services/memory-init-preflight.js +11 -1
  103. package/dist/services/memory-init-request-candidate.js +73 -17
  104. package/dist/services/memory-init-request-input.js +23 -7
  105. package/dist/services/memory-init-run-bundle.js +49 -12
  106. package/dist/services/memory-init-storage.js +17 -7
  107. package/dist/services/memory-layer.js +68 -4
  108. package/dist/services/memory-lint-config.js +111 -2
  109. package/dist/services/memory-native-review-copy.js +5 -67
  110. package/dist/services/memory-normal-flow-entry.js +12 -0
  111. package/dist/services/memory-ordinary-defaults.js +22 -5
  112. package/dist/services/memory-preflight-handler.js +15 -0
  113. package/dist/services/memory-repair-current-view.js +1 -1
  114. package/dist/services/memory-repair-document-delta.js +70 -33
  115. package/dist/services/memory-repair-history.js +12 -7
  116. package/dist/services/memory-repair-physical-binding.js +1 -119
  117. package/dist/services/memory-repair-publication-plan.js +5 -3
  118. package/dist/services/memory-repair-publication-source.js +1 -1
  119. package/dist/services/memory-repair-publication.js +3 -2
  120. package/dist/services/memory-repair-registration.js +2 -1
  121. package/dist/services/memory-repair-work-baseline.js +4 -2
  122. package/dist/services/memory-repair-work-completion.js +32 -12
  123. package/dist/services/memory-repair-work-results.js +21 -2
  124. package/dist/services/memory-repair-workspace.js +11 -2
  125. package/dist/services/memory-repair-writer-copy.js +2 -1
  126. package/dist/services/memory-result-binding.js +78 -12
  127. package/dist/services/memory-retained-review-authority.js +17 -8
  128. package/dist/services/memory-retained-second-baseline.js +1 -1
  129. package/dist/services/memory-retained-second-repairs.js +12 -6
  130. package/dist/services/memory-review-correction-plan.js +47 -12
  131. package/dist/services/memory-review-handler.js +17 -17
  132. package/dist/services/memory-review-inputs.js +55 -9
  133. package/dist/services/memory-review-limits.js +10 -0
  134. package/dist/services/memory-review-receipt.js +2 -2
  135. package/dist/services/memory-review-registration.js +13 -4
  136. package/dist/services/memory-review-work-custody.js +3 -1
  137. package/dist/services/memory-review-work-input.js +26 -7
  138. package/dist/services/memory-review-work-results.js +26 -4
  139. package/dist/services/memory-second-corrected-review-inputs.js +5 -3
  140. package/dist/services/memory-second-correction-plan.js +2 -1
  141. package/dist/services/memory-second-repair-launch.js +4 -4
  142. package/dist/services/memory-second-repair-source.js +1 -1
  143. package/dist/services/memory-stage-dispatch.js +38 -1
  144. package/dist/services/memory-stage-results.js +113 -22
  145. package/dist/services/memory-upgrade-analysis-handler.js +243 -0
  146. package/dist/services/memory-upgrade-bootstrap-readiness.js +251 -0
  147. package/dist/services/memory-upgrade-evidence.js +100 -0
  148. package/dist/services/memory-upgrade-inventory.js +442 -0
  149. package/dist/services/memory-upgrade-publication-custody.js +73 -0
  150. package/dist/services/memory-upgrade-publication.js +413 -0
  151. package/dist/services/memory-upgrade-selection.js +430 -0
  152. package/dist/services/memory-upgrade-start.js +60 -0
  153. package/dist/services/memory-upgrade-storage-readiness.js +340 -0
  154. package/dist/services/memory-verification-acceptance.js +38 -17
  155. package/dist/services/memory-verification-checks.js +77 -23
  156. package/dist/services/memory-verification-receipt.js +6 -5
  157. package/dist/services/memory-verify-handler.js +15 -13
  158. package/dist/services/memory-work-result-json.js +12 -0
  159. package/dist/services/memory-work-result-preparation.js +5 -4
  160. package/dist/services/merge-server.js +4 -2
  161. package/dist/services/migrations.js +127 -98
  162. package/dist/services/physical-startup.js +45 -0
  163. package/dist/services/project-registry.js +97 -0
  164. package/dist/services/project-summary.js +1 -1
  165. package/dist/services/projects.js +3 -93
  166. package/dist/services/prompt-delivery.js +71 -0
  167. package/dist/services/provider-capacity.js +74 -9
  168. package/dist/services/recovery-snapshot-database.js +32 -4
  169. package/dist/services/repair-intents.js +1 -12
  170. package/dist/services/repair-receipt.js +13 -0
  171. package/dist/services/run-control-worker.js +35 -18
  172. package/dist/services/run-controller-process.js +2 -6
  173. package/dist/services/run-controller-recovery.js +6 -1
  174. package/dist/services/run-controller.js +131 -80
  175. package/dist/services/run-recovery-guard.js +30 -0
  176. package/dist/services/run-recovery.js +11 -44
  177. package/dist/services/run-start-custody.js +17 -5
  178. package/dist/services/runtime-scope-worker.js +46 -19
  179. package/dist/services/sessions.js +3 -24
  180. package/dist/services/setup.js +63 -13
  181. package/dist/services/stage-handlers.js +9 -7
  182. package/dist/services/stage-pause.js +108 -44
  183. package/dist/services/version-status.js +1 -1
  184. package/dist/services/vnext-fanout.js +21 -9
  185. package/dist/services/vnext-plan-review.js +3 -5
  186. package/dist/services/work-registry.js +80 -33
  187. package/dist/services/workspace-files.js +1 -1
  188. package/dist/shared/entity-references.js +1 -8
  189. package/dist/storage/data-preservation.js +65 -0
  190. package/dist/storage/database.js +53 -19
  191. package/dist/storage/import-guard.js +84 -27
  192. package/dist/storage/memory-upgrade-schema-bootstrap.js +38 -0
  193. package/dist/storage/paired-writer-migration.js +424 -0
  194. package/dist/storage/writer-contract.js +11 -4
  195. package/dist/storage/writer-migration.js +313 -52
  196. package/dist/storage/writer-restore.js +217 -0
  197. package/package.json +8 -4
@@ -19,14 +19,15 @@ import { agyWorkspaceHooks } from "./agy-hooks.mjs";
19
19
  import { durableDaemonDispatch, completeDaemonShutdown, assertDaemonDispatch, assertDaemonRestartable, trackDaemonConnections, retireDaemonEndpoint, retireFailedDaemon } from "./daemon-operations.mjs";
20
20
  import path from "node:path";
21
21
  import { observeModel } from "./model-observations.mjs";
22
- import { AGY_VERSION_TIMEOUT_MS, AgyError, agyTerminalFailure, assertProfile, executable, observedProfile, runAgy, usageSnapshot } from "./dd-agy.mjs";
22
+ import { AGY_VERSION_TIMEOUT_MS, AgyError, agyTerminalFailure, agyModelArgs, assertProfile, executable, observedProfile, runAgy, usageSnapshot } from "./dd-agy.mjs";
23
+ import { recoveredAgyErrorBoundary } from "./agy-error-boundary.mjs";
23
24
  import { settledNativeTree } from "./native-children.mjs";
24
25
  import { retainStartupFailure, prepareRuntimeOwner, cleanupFailedStart, cleanupFailedDaemonStart, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, scheduleDaemonProcessHeartbeat, registerDaemonProcess, stopProcessGroup } from "./managed-daemon.mjs";
25
26
  import { readNativeLines, streamNativeDiagnostics, nativeObservationError, admitNativeRequestBuffer } from "./native-streams.mjs";
26
27
 
27
28
  const REQUEST_SCHEMA = "dd-agy/daemon-request@1", RESPONSE_SCHEMA = "dd-agy/daemon-response@1", STATE_SCHEMA = "dd-agy/daemon-state@1";
28
29
  class DaemonError extends AgyError {}
29
- export async function captureAgyTranscript(runtimeRoot, child, rootSessionId, ownedSessionIds = [rootSessionId]) {
30
+ export async function captureAgyTranscript(runtimeRoot, child, rootSessionId, ownedSessionIds = [rootSessionId], inspect = null) {
30
31
  const session = { harness_id: "antigravity-cli", session_id: child.provider_session_id };
31
32
  let handle;
32
33
  try {
@@ -43,6 +44,7 @@ export async function captureAgyTranscript(runtimeRoot, child, rootSessionId, ow
43
44
  const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
44
45
  const bytes = buffer.subarray(0, bytesRead), prefix = bytes.subarray(0, bytes.lastIndexOf(10) + 1);
45
46
  const source = { locator: child.log_uri, byte_start: 0, byte_end: prefix.length, sha256: createHash("sha256").update(prefix).digest("hex") };
47
+ if (prefix.length === bytesRead && stat.size <= maximum) inspect?.(prefix.toString("utf8"), source);
46
48
  const events = agyTranscriptObservations(prefix.toString("utf8"), child.provider_session_id, child.parent_provider_session_id ?? rootSessionId, source);
47
49
  if (prefix.length !== bytesRead || stat.size > maximum) events.push({ session, reason: stat.size > maximum ? "agy_transcript_size_limit" : "agy_transcript_partial_tail" });
48
50
  return events;
@@ -224,8 +226,8 @@ export const stopDaemon = async options => { const timeoutMs = options.timeoutMs
224
226
  export class Runtime {
225
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; }
226
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; }
227
- async journal(kind, payload = {}) { const line = `${JSON.stringify({ schema_id: "dd-agy/daemon-event@1", event_id: randomUUID(), observed_at: new Date().toISOString(), kind, ...payload })}\n`; for (const journal of new Set([this.paths.journal, this.config.journal].filter(Boolean))) { await mkdir(path.dirname(journal), { recursive: true, mode: 0o700 }); await appendFile(journal, line, { mode: 0o600 }); } }
228
- args(sessionId) { return ["--input-format", "stream-json", "--output-format", "stream-json", "--model", this.config.model, "--effort", this.config.reasoning, "--mode", this.config.mode, "--dangerously-skip-permissions", "--print-timeout", "0", `--gemini_dir=${this.paths.gemini}`, "--app_data_dir=runtime", ...(sessionId ? ["--conversation", sessionId] : ["--new-project"])]; }
229
+ 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"])]; }
229
231
  requireSessionIdentity(requestedId) {
230
232
  const observedId = this.init?.conversation_id ?? null;
231
233
  if (!requestedId || requestedId === observedId) return observedId;
@@ -288,7 +290,7 @@ export class Runtime {
288
290
  const providerProcess = this.providerProcess;
289
291
  try { await this.persist({ provider_process: providerProcess }); }
290
292
  catch (error) { await cleanupFailedStart(this.config, providerProcess, null, error); throw error; }
291
- try { assertDaemonDispatch(this.paths?.dir); this.child = spawn(target.command, target.args, { cwd: this.config.cwd, env: sanitizedEnv(this.config), detached: true, stdio: ["pipe", "pipe", "pipe"] }); }
293
+ 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"] }); }
292
294
  catch (error) { await cleanupFailedStart(this.config, providerProcess, null, error); throw error; }
293
295
  const providerChild = this.child;
294
296
  let allowProviderFinalization;
@@ -334,10 +336,7 @@ export class Runtime {
334
336
  // child can close while the asynchronous write is still pending.
335
337
  if (event.event === "result" && ["ERROR", "CANCELLED"].includes(event.result?.status)) {
336
338
  this.lastTerminalFailure ??= this.state.last_terminal_failure;
337
- const repeated = Boolean(this.lastTerminalFailure && this.lastTerminalFailure.details?.provider_result?.error === event.result.error
338
- && this.lastTerminalFailure.details.provider_session_id === (event.result.conversation_id || this.init?.conversation_id || null)
339
- && (this.active?.previousError === event.result.error || this.lastResult?.status === "ERROR" && this.lastResult.error === event.result.error));
340
- event.terminal_metadata = repeated ? this.lastTerminalFailure.details : { observed_at: new Date().toISOString(), provider_session_id: event.result.conversation_id || this.init?.conversation_id || null,
339
+ event.terminal_metadata = { observed_at: new Date().toISOString(), provider_session_id: event.result.conversation_id || this.init?.conversation_id || null,
341
340
  operation_id: this.active?.operationId ?? null, turn_generation: this.active?.generation ?? null, journal_locator: this.paths.journal, native_source: "agy.result" };
342
341
  if (!this.init) this.startupError ??= agyTerminalFailure(event.result, event.terminal_metadata);
343
342
  this.receivedTerminalMetadata = event.terminal_metadata;
@@ -399,16 +398,24 @@ export class Runtime {
399
398
  return;
400
399
  }
401
400
  const current = this.active;
402
- const terminalMetadata = { received_at: new Date().toISOString(), ...(retainedMetadata ?? this.receivedTerminalMetadata ?? (this.lastTerminalFailure && this.lastTerminalFailure.details?.provider_result?.error === result.error
403
- && this.lastTerminalFailure.details.provider_session_id === (result.conversation_id || this.init?.conversation_id || null)
404
- && (current?.previousError === result.error || this.lastResult?.status === "ERROR" && this.lastResult.error === result.error) ? this.lastTerminalFailure.details
405
- : { observed_at: new Date().toISOString(), provider_session_id: result.conversation_id || this.init?.conversation_id || null, operation_id: current?.operationId ?? null,
406
- turn_generation: current?.generation ?? null, journal_locator: this.paths.journal, native_source: "agy.result" })) };
401
+ if (current?.dispatched === false) {
402
+ const error = new DaemonError("operation_observation_lost", "Unexpected native terminal before this stream request was dispatched", false, { operation_id: current.operationId, turn_generation: current.generation, native_outcome: "unknown" });
403
+ this.active = null; current.reject(error);
404
+ await this.journal("unexpected_terminal_before_dispatch", { result, operation_id: current.operationId });
405
+ await this.persist({ unclaimed_activity: true, active_tree: true });
406
+ return;
407
+ }
408
+ const terminalMetadata = { received_at: new Date().toISOString(), ...(retainedMetadata ?? this.receivedTerminalMetadata ?? { observed_at: new Date().toISOString(), provider_session_id: result.conversation_id || this.init?.conversation_id || null, operation_id: current?.operationId ?? null,
409
+ turn_generation: current?.generation ?? null, journal_locator: this.paths.journal, native_source: "agy.result" }) };
407
410
  this.receivedTerminalMetadata = null;
408
411
  // The ordered stream returns one result per input. Model usage counters
409
412
  // are not request IDs: native command rejections can report zero turns.
410
413
  // prompt() drains earlier events before accepting another input.
411
- if (!current && this.lastResult) { await this.journal("stale_terminal_observed", { result }); return; }
414
+ if (!current && this.lastResult) {
415
+ await this.journal("stale_terminal_observed", { result });
416
+ if (!isDeepStrictEqual(result, this.lastResult)) await this.persist({ unclaimed_activity: true, active_tree: true });
417
+ return;
418
+ }
412
419
  this.lastResult = result; this.markTerminalActivity(result);
413
420
  const rootStop = this.sessionObservations.get(this.init?.conversation_id)?.stop;
414
421
  const pendingTerminal = current && result.status === "SUCCESS";
@@ -418,7 +425,12 @@ export class Runtime {
418
425
  try {
419
426
  await this.persist({ active_tree: true, last_result: result, last_terminal_metadata: terminalMetadata });
420
427
  if (current) current.terminalPersisted = true;
421
- if (providerEvent) await this.journal("provider_event", { event: providerEvent });
428
+ const terminalPosition = await this.journal("provider_event", { event: providerEvent ?? { event: "result", result } });
429
+ terminalMetadata.stream_terminal_position = terminalPosition;
430
+ if (current?.outboundPosition && current.dispatchJournalSettled && !current.dispatchObservationError && this.config.daemonId && current.connectionId && current.operationId) terminalMetadata.request_proof = {
431
+ kind: "stream_request", daemon_instance: this.config.daemonId, connection_id: current.connectionId, operation_id: current.operationId,
432
+ turn_generation: current.generation, session_id: this.init.conversation_id, outbound_position: current.outboundPosition, terminal_position: terminalPosition
433
+ };
422
434
  await this.captureChildTools();
423
435
  } catch (error) { return this.failTerminalObservation(current, result, terminalMetadata, error); }
424
436
  try { this.usageIngest = await forwardUsage(this.config, result.conversation_id ?? this.init.conversation_id, result, this.toolSnapshot(), [...this.descendants.values()]); }
@@ -429,9 +441,19 @@ export class Runtime {
429
441
  if (!current) return;
430
442
  if (this.active !== current) return;
431
443
  if (result.status === "ERROR" || result.status === "CANCELLED") {
444
+ await this.observeHookFailures();
445
+ if (this.active !== current) return;
432
446
  this.active = null;
433
447
  const settled = this.receipt().settled;
434
- this.lastTerminalFailure = agyTerminalFailure(result, { ...terminalMetadata, native_turn_items: { observed: settled, possible_effects: true, pending: !settled }, provider_error_scope: current.previousError && current.previousError === result.error ? "repeated_conversation_error" : "terminal_result", usage_ingest: this.usageIngest });
448
+ if (result.status === "ERROR" && settled && rootStop?.fullyIdle === true && !this.state.last_hook_rejection && current.operationId && this.paths.runtime) {
449
+ const root = this.init.conversation_id, transcript = path.join(this.paths.runtime, "brain", root, ".system_generated", "logs", "transcript.jsonl");
450
+ 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) => {
451
+ const boundary = recoveredAgyErrorBoundary(text, result.error, current.stepFloor);
452
+ const hook = current.nativeHookEvents?.get(boundary?.boundary_step);
453
+ if (boundary && hook) terminalMetadata.recovered_error_boundary = { ...boundary, receipt: { ...boundary.receipt, native_hook_event_key: hook, daemon_instance: this.config.daemonId }, source };
454
+ });
455
+ }
456
+ this.lastTerminalFailure = agyTerminalFailure(result, { ...terminalMetadata, native_turn_items: { observed: settled, possible_effects: true, pending: !settled }, provider_error_scope: "terminal_result", usage_ingest: this.usageIngest });
435
457
  try { await this.persist({ active_tree: !settled, last_terminal_failure: errorRecord(this.lastTerminalFailure) }); }
436
458
  catch (error) { return this.failTerminalObservation(current, result, terminalMetadata, error); }
437
459
  return current.reject(this.lastTerminalFailure);
@@ -491,7 +513,8 @@ export class Runtime {
491
513
  const reply = new Promise((resolve, reject) => { resolveTurn = resolve; rejectTurn = reject; });
492
514
  // Cancellation can reject while admission persistence is still pending.
493
515
  void reply.catch(() => {});
494
- const current = { resolve: resolveTurn, reject: rejectTurn, generation: ++this.turnGeneration, operationId, previousError: this.lastResult?.error ?? null };
516
+ this.connectionId ??= randomUUID();
517
+ const current = { resolve: resolveTurn, reject: rejectTurn, generation: ++this.turnGeneration, operationId, connectionId: this.connectionId, dispatched: false };
495
518
  let resolveDispatchJournal;
496
519
  const dispatchJournal = current.dispatchJournal = new Promise(resolve => { resolveDispatchJournal = resolve; });
497
520
  this.promptObservation = current;
@@ -504,6 +527,7 @@ export class Runtime {
504
527
  this.active = current;
505
528
  this.hookRequests = new Map();
506
529
  const root = this.init?.conversation_id, prior = this.sessionObservations.get(root) ?? {};
530
+ current.stepFloor = prior.last_step_index ?? -1;
507
531
  this.sessionObservations.set(root, { ...prior, step_floor: prior.last_step_index ?? null, stop: null, last_hook: null });
508
532
  this.lastProviderEvent = null; this.progressSteps = new Map(); this.progressHooks = new Set(); this.progress = undefined;
509
533
  try {
@@ -511,10 +535,11 @@ export class Runtime {
511
535
  assertDispatch();
512
536
  assertDaemonDispatch(this.paths?.dir);
513
537
  if (this.active !== current) throw new DaemonError("operation_cancelled", "prompt reservation was superseded");
538
+ current.outboundPosition = this.journalPosition = (this.journalPosition ?? 0) + 1;
514
539
  current.dispatched = true;
515
540
  this.child.stdin.write(`${JSON.stringify({ event: "user", message: { content: text } })}\n`, error => {
516
541
  if (error) { this.drainingError ??= nativeObservationError(error); current.dispatchObservationError = this.drainingError; current.reject(this.drainingError); finishDispatchJournal(); }
517
- else void Promise.resolve().then(() => this.journal("native_dispatch", { operation_id: operationId, provider_session_id: this.init?.conversation_id, native_request_id: current.generation })).then(finishDispatchJournal, cause => {
542
+ else void Promise.resolve().then(() => this.journal("native_dispatch", { operation_id: operationId, provider_session_id: this.init?.conversation_id, turn_generation: current.generation, connection_id: current.connectionId }, current.outboundPosition)).then(finishDispatchJournal, cause => {
518
543
  this.drainingError ??= nativeObservationError(cause); current.dispatchObservationError = this.drainingError;
519
544
  current.reject(this.drainingError); finishDispatchJournal();
520
545
  });
@@ -552,6 +577,20 @@ export class Runtime {
552
577
  });
553
578
  }
554
579
  return result;
580
+ } catch (error) {
581
+ await dispatchJournal;
582
+ if (error.code === "native_outcome_observation_failed") throw error;
583
+ if (current.dispatchObservationError) throw current.dispatchObservationError;
584
+ const metadata = current.terminalMetadata;
585
+ if (error === this.lastTerminalFailure && metadata && !metadata.request_proof && current.outboundPosition && metadata.stream_terminal_position && this.config.daemonId && current.operationId) {
586
+ metadata.request_proof = { kind: "stream_request", daemon_instance: this.config.daemonId, connection_id: current.connectionId, operation_id: current.operationId,
587
+ turn_generation: current.generation, session_id: this.init.conversation_id, outbound_position: current.outboundPosition, terminal_position: metadata.stream_terminal_position };
588
+ const proven = agyTerminalFailure(current.terminalResult, { ...error.details, ...metadata });
589
+ this.lastTerminalFailure = proven;
590
+ await this.persist({ last_terminal_metadata: metadata, last_terminal_failure: errorRecord(proven) });
591
+ throw proven;
592
+ }
593
+ throw error;
555
594
  } finally {
556
595
  clearInterval(timer); current.observerReturned = true;
557
596
  if (current.dispatchJournalSettled && this.promptObservation === current) this.promptObservation = null;
@@ -606,7 +645,10 @@ export class Runtime {
606
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;
607
646
  const observed = payload.model ? observedProfile(payload) : {};
608
647
  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" });
609
- const eventId = createHash("sha256").update(JSON.stringify({ daemon: this.config.daemonId, event, conversationId, fully_idle: payload.fullyIdle ?? null, termination_reason: payload.terminationReason ?? null, execution: payload.executionNum ?? null, step: payload.stepIdx ?? null, tool: payload.toolCall?.name ?? null, args: payload.toolCall?.args ?? null })).digest("hex"); await this.persist(); await this.journal("hook", { stale, hook_event: event, event_id: eventId, conversation_id: conversationId, turn_generation: this.turnGeneration, execution_num: execution, step_index: step, termination_reason: payload.terminationReason ?? null, fully_idle: payload.fullyIdle ?? null, persistence_timing: this.persistTiming ?? null }); await this.completePendingTurn(); return { event_id: eventId, daemon_id: this.config.daemonId, launch_cwd: this.config.cwd, parent_conversation_id: parent, profile: observed, requested_profile: { provider: this.config.provider, model: this.config.model, reasoning: this.config.reasoning, mode: this.config.mode }, project_root: this.config.projectRoot, dd_flow_bin: this.config.ddFlowBin, dd_flow_home: this.config.ddFlowHome };
648
+ 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
+ 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
+ if (!stale && !child && event === "PreToolUse" && Number.isInteger(step) && this.active) { this.active.nativeHookEvents ??= new Map(); this.active.nativeHookEvents.set(step, `${eventId}:before`); }
651
+ 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 };
610
652
  }
611
653
  async finalizeProviderForControl() {
612
654
  const pending = this.providerFinalization;
@@ -4,7 +4,7 @@ import path from "node:path";
4
4
  import { SETUP_STARTUP_MS } from './setup-progress.mjs';
5
5
 
6
6
  import { checkObservedProfile } from "./model-observations.mjs";
7
- import { classifyProviderError, safeProviderError } from "./provider-overload-policy.mjs";
7
+ import { classifyProviderError, safeProviderError, streamRequestKey } from "./provider-overload-policy.mjs";
8
8
 
9
9
  const DEFAULT_MODEL = "gemini-3.1-pro-high";
10
10
  export const AGY_VERSION_TIMEOUT_MS = 30_000;
@@ -14,14 +14,22 @@ export class AgyError extends Error {
14
14
  }
15
15
 
16
16
  export function agyTerminalFailure(result, metadata = {}) {
17
- const native = typeof result.error === "object" && result.error ? result.error : { message: result.error ?? "Antigravity returned ERROR" };
17
+ let native = typeof result.error === "object" && result.error ? result.error : { message: result.error ?? "Antigravity returned ERROR" };
18
+ if (typeof result.error === "string" && /^AGY_ERROR\s*[: ]/.test(result.error)) {
19
+ try {
20
+ const envelope = JSON.parse(result.error.replace(/^AGY_ERROR\s*:?\s*/, ""));
21
+ const error = envelope.error ?? envelope;
22
+ if (error && typeof error === "object" && !Array.isArray(error)) native = error;
23
+ } catch { /* A malformed envelope remains a native failure, never evidence. */ }
24
+ }
18
25
  const category = classifyProviderError(native);
19
26
  const legacy = { provider_quota_exhausted: "agy_provider_quota_exhausted", provider_limit_unknown: "agy_provider_limit_unknown", provider_auth_failed: "agy_provider_account", provider_failed: "agy_provider_failed" };
20
27
  let code = result.status === "CANCELLED" ? "agy_turn_cancelled" : legacy[category] ?? category;
21
28
  // A local generation/usage count is not a native request identity.
22
29
  const identity = result.turn_id ?? result.request_id;
23
30
  const turn = typeof identity === "string" && identity ? identity : null;
24
- const proofMissing = result.status === "ERROR" && ["provider_capacity_transient", "provider_rate_limited"].includes(category) && !turn;
31
+ const stream = streamRequestKey(metadata.request_proof);
32
+ const proofMissing = result.status === "ERROR" && ["provider_capacity_transient", "provider_rate_limited"].includes(category) && !turn && !stream;
25
33
  if (proofMissing) code = "provider_overload_evidence_unavailable";
26
34
  return new AgyError(code, String(native.message ?? "Antigravity returned ERROR"), code === "provider_capacity_transient" || code === "provider_rate_limited", {
27
35
  provider_result: result, ...metadata, provider_error: safeProviderError(native), native_source: "agy.result", terminal_status: result.status === "ERROR" ? "failed" : "cancelled",
@@ -57,6 +65,12 @@ export function observedProfile(init = {}, requested = {}) {
57
65
 
58
66
  export function assertProfile(requested, observed) { return checkObservedProfile(requested, observed, { required: requested.permission_mode ? ["permission_mode"] : [] }); }
59
67
 
68
+ export function agyModelArgs(model, reasoning) {
69
+ const encoded = /^gemini-.+-(low|medium|high)$/.exec(model)?.[1];
70
+ if (encoded && encoded !== reasoning) throw new AgyError("agy_profile_invalid", "Requested reasoning conflicts with the selected Gemini model");
71
+ return ["--model", model, ...(encoded ? [] : ["--effort", reasoning])];
72
+ }
73
+
60
74
  export async function doctor(options = {}) {
61
75
  const bin = options.bin ?? process.env.DD_AGY_BIN ?? "agy", temporary = await mkdtemp(path.join(os.tmpdir(), "dd-agy-doctor-"));
62
76
  try {
@@ -521,7 +521,7 @@ export class DroidRuntime {
521
521
  if (!this.config.ddFlowBin || payload.tool_name !== "Execute") return {};
522
522
  // Usage is ingested by inspect/turn completion, never ahead of admission.
523
523
  try {
524
- return await this.flow("event", "handle", { schema_id: "dd-flow/droid-tool-event@1", phase: event === "PreToolUse" ? "before" : "after", event_id: eventId, daemon_id: this.config.daemonId, session: { provider_session_id: id, parent_provider_session_id: parent, directory: this.config.cwd }, tool: payload.tool_name, input: payload.tool_input, profile, transcript_path: native.transcript_path, outcome: { status: payload.tool_response?.is_error ? "error" : "completed" } }, budget.remaining("admission", 15_000));
524
+ return await this.flow("event", "handle", { schema_id: "dd-flow/droid-tool-event@1", phase: event === "PreToolUse" ? "before" : "after", event_id: eventId, daemon_id: this.config.daemonId, session: { provider_session_id: id, parent_provider_session_id: parent, directory: this.config.cwd }, tool: payload.tool_name, input: payload.tool_input, profile, transcript_path: native.transcript_path, outcome: { status: payload.tool_response?.is_error ? "error" : "completed" } }, budget.remaining("admission"));
525
525
  } catch (error) {
526
526
  if (event === "PreToolUse" && isProvenShellCorrection(error)) {
527
527
  const refusal = errorRecord(error);
@@ -187,8 +187,53 @@ export async function stopDaemon(options) { const timeoutMs = options.timeoutMs
187
187
  export class Runtime {
188
188
  constructor(paths, state, bridge, initialized) { this.paths = paths; this.state = state; this.bridge = bridge; this.initialized = initialized; this.sessions = new Map((state.sessions ?? []).map((item) => [item.provider_session_id, item])); this.descendants = new Map((state.descendants ?? []).map(child => [child.provider_session_id, child])); for (const session of this.sessions.values()) if (session.parent_provider_session_id && !this.descendants.has(session.provider_session_id)) this.descendants.set(session.provider_session_id, { provider_session_id: session.provider_session_id, parent_provider_session_id: session.parent_provider_session_id, status: "unknown", source: "legacy.daemon_state", outcome_error: { code: "native_child_outcome_unavailable", message: "Retained Grok child has no durable outcome receipt" } }); this.toolUsage = new Map(); this.loadedSessionId = null; this.active = null; this.server = null; this.ready = false; this.persisting = Promise.resolve(); }
189
189
  persist(patch = {}) { Object.assign(this.state, patch, { pid: process.pid, updated_at: new Date().toISOString(), sessions: [...this.sessions.values()], descendants: [...this.descendants.values()] }); const snapshot = structuredClone(this.state); this.persisting = this.persisting.catch(() => {}).then(() => writeState(this.paths.state, snapshot)); return this.persisting; }
190
- options(params) { const sessionId = params.sessionId; const session = sessionId ? this.sessions.get(sessionId) : null; const root = session?.root_provider_session_id ?? sessionId; return { ...this.state.config, ...params, cwd: params.cwd ?? session?.cwd ?? this.state.config.cwd, initialized: this.initialized, daemonId: this.state.daemon_id, rootProviderSessionId: root, toolUsage: this.toolUsage, allowBackground: true, liveSession: params.liveSession ?? Boolean(sessionId && sessionId === this.loadedSessionId) }; }
191
- track(result, before = null) { if (!result?.provider_session_id) return; const session = this.sessions.get(result.provider_session_id); const root = result.parent_provider_session_id ? (this.sessions.get(result.parent_provider_session_id)?.root_provider_session_id ?? result.parent_provider_session_id) : (session?.root_provider_session_id ?? result.provider_session_id); this.sessions.set(result.provider_session_id, { native_root_receipt: session?.native_root_receipt ?? null, provider_session_id: result.provider_session_id, adapter_session_id: result.adapter_session_id ?? result.provider_session_id, parent_provider_session_id: result.parent_provider_session_id ?? session?.parent_provider_session_id ?? null, root_provider_session_id: root, cwd: result.cwd ?? result.target?.newCwd ?? result.info?.cwd ?? session?.cwd ?? null }); for (const child of this.running(result)) this.recordDescendant({ ...child, stale_activity: Boolean(before && before.get(child.sessionId ?? child.subagentId ?? child.provider_session_id) !== this.descendants.get(child.sessionId ?? child.subagentId ?? child.provider_session_id)) }, root, "x.ai/subagent/list_running", "running"); for (const child of result.descendants ?? []) this.recordDescendant(child, root, child.source ?? "x.ai/subagent/event", child.status ?? "unknown"); }
190
+ async reduceTerminal(outcome) {
191
+ const session = this.sessions.get(outcome.session_id), turn = session?.native_prompt_operation;
192
+ if (outcome.method !== "session/prompt" || !turn || typeof outcome.connection_id !== "string" || !outcome.connection_id
193
+ || !Number.isSafeInteger(outcome.request_id) || outcome.request_id < 1 || !Number.isSafeInteger(outcome.native_sequence) || outcome.native_sequence < 1 || turn.daemon_id !== this.state.daemon_id
194
+ || turn.connection_id !== outcome.connection_id || turn.operation_id !== outcome.operation_id || turn.generation !== outcome.generation
195
+ || turn.native_request_id !== outcome.request_id) return;
196
+ const message = outcome.terminal_response;
197
+ const kind = message?.error ? "failed" : message?.result?.stopReason;
198
+ if (!["failed", "end_turn", "cancelled"].includes(kind)) return;
199
+ const receipt = { kind, ...turn, request_id: outcome.request_id, native_sequence: outcome.native_sequence };
200
+ if (session.native_root_receipt && (session.native_root_receipt.kind !== kind || session.native_root_receipt.request_id !== outcome.request_id)) {
201
+ this.state.observation_error = { code: "native_terminal_outcome_conflict", message: "Conflicting native terminal outcomes for the same prompt" };
202
+ await this.persist();
203
+ throw new DaemonError(this.state.observation_error.code, this.state.observation_error.message);
204
+ }
205
+ if (session.native_root_receipt) return;
206
+ if (session.native_activity && session.native_activity.connection_id === outcome.connection_id && session.native_activity.sequence > outcome.native_sequence) {
207
+ return await this.invalidateTerminal(session);
208
+ }
209
+ session.native_root_receipt = receipt;
210
+ await this.persist();
211
+ }
212
+ async invalidateTerminal(session) {
213
+ session.native_root_receipt = null;
214
+ this.state.observation_error = { code: "native_activity_after_terminal", message: "Productive root activity followed its native terminal outcome" };
215
+ await this.persist();
216
+ }
217
+ async observeRootActivity(message, observation) {
218
+ const session = this.sessions.get(message?.params?.sessionId), turn = session?.native_prompt_operation;
219
+ if (!observation?.productive || !turn || session.parent_provider_session_id || turn.connection_id !== observation.connection_id) return;
220
+ if (session.native_activity?.connection_id !== observation.connection_id || session.native_activity.sequence < observation.sequence) session.native_activity = { connection_id: observation.connection_id, sequence: observation.sequence };
221
+ const receipt = session.native_root_receipt;
222
+ if (receipt?.connection_id === observation.connection_id && observation.sequence > receipt.native_sequence) await this.invalidateTerminal(session);
223
+ }
224
+ async drainTerminal() { await this.bridge.terminalOutcomes; await this.persisting; if (this.bridge.terminalProjectionError) throw this.bridge.terminalProjectionError; }
225
+ settledRoot(id) { return !this.active && !this.rootRunning({ provider_session_id: id }) && this.topology(id).every(child => !this.rootRunning({ provider_session_id: child.provider_session_id }) && !child.outcome_error && !child.activity_status); }
226
+ async restoreTerminals() {
227
+ if (!this.state.config?.journal) return;
228
+ let contents;
229
+ try { contents = await readFile(this.state.config.journal, "utf8"); } catch (error) { if (error.code === "ENOENT") return; throw error; }
230
+ for (const line of contents.split("\n").filter(Boolean)) {
231
+ const event = JSON.parse(line);
232
+ if (["native_terminal_outcome", "native_terminal_after_hook_failure"].includes(event.kind)) await this.reduceTerminal(event.payload);
233
+ }
234
+ }
235
+ options(params) { const sessionId = params.sessionId; const session = sessionId ? this.sessions.get(sessionId) : null; const root = session?.root_provider_session_id ?? sessionId; return { ...this.state.config, ...params, cwd: params.cwd ?? session?.cwd ?? this.state.config.cwd, initialized: this.initialized, daemonId: this.state.daemon_id, rootProviderSessionId: root, toolUsage: this.toolUsage, allowBackground: true, rootSettled: id => this.settledRoot(id), liveSession: params.liveSession ?? Boolean(sessionId && sessionId === this.loadedSessionId) }; }
236
+ track(result, before = null) { if (!result?.provider_session_id) return; const session = this.sessions.get(result.provider_session_id); const root = result.parent_provider_session_id ? (this.sessions.get(result.parent_provider_session_id)?.root_provider_session_id ?? result.parent_provider_session_id) : (session?.root_provider_session_id ?? result.provider_session_id); this.sessions.set(result.provider_session_id, { ...session, native_root_receipt: session?.native_root_receipt ?? null, provider_session_id: result.provider_session_id, adapter_session_id: result.adapter_session_id ?? result.provider_session_id, parent_provider_session_id: result.parent_provider_session_id ?? session?.parent_provider_session_id ?? null, root_provider_session_id: root, cwd: result.cwd ?? result.target?.newCwd ?? result.info?.cwd ?? session?.cwd ?? null }); for (const child of this.running(result)) this.recordDescendant({ ...child, stale_activity: Boolean(before && before.get(child.sessionId ?? child.subagentId ?? child.provider_session_id) !== this.descendants.get(child.sessionId ?? child.subagentId ?? child.provider_session_id)) }, root, "x.ai/subagent/list_running", "running"); for (const child of result.descendants ?? []) this.recordDescendant(child, root, child.source ?? "x.ai/subagent/event", child.status ?? "unknown"); }
192
237
  recordDescendant(value, root, source, fallback) {
193
238
  const id = value?.provider_session_id ?? value?.session_id ?? value?.sessionId ?? value?.childSessionId ?? value?.subagentId ?? value?.child_session_id ?? value?.subagent_id;
194
239
  const parent = value?.parent_provider_session_id ?? value?.parentSessionId ?? value?.parent_session_id ?? root;
@@ -302,13 +347,14 @@ export class Runtime {
302
347
  }
303
348
  return modelFacts;
304
349
  }
305
- topology(root) { if (this.state.observation_error) { const error = this.state.observation_error; throw new DaemonError(error.code, error.message, false, error.details); } return [...this.descendants.values()].filter((child) => child.parent_provider_session_id === root); }
350
+ topology(root) { if (this.state.observation_error) { const error = this.state.observation_error; throw new DaemonError(error.code, error.message, false, error.details); } return [...this.descendants.values()].filter((child) => child.parent_provider_session_id === root || this.sessions.get(child.provider_session_id)?.root_provider_session_id === root); }
306
351
  observeToolCall(message) { this.toolObservations ??= new ToolObservations(); this.toolObservations.add(acpToolObservation(message, { harness: "grok-acp" })); const sessionId = message?.params?.sessionId; if (sessionId) this.toolUsage.set(sessionId, this.toolObservations.summary(sessionId)); }
307
352
  running(result) { return result?.evidence?.subagents?.subagents ?? result?.subagents?.subagents ?? []; }
308
353
  treeRunning(result) {
309
354
  const evidence = result?.evidence ?? result;
310
355
  const children = evidence?.subagents?.subagents;
311
- return this.rootRunning({ ...result, ...evidence }) || !Array.isArray(children) || children.length > 0 || Boolean(evidence?.subagents?.observation_error);
356
+ return this.rootRunning({ ...result, ...evidence }) || !Array.isArray(children) || children.length > 0 || Boolean(evidence?.subagents?.observation_error)
357
+ || this.topology(result?.provider_session_id).some(child => this.rootRunning({ provider_session_id: child.provider_session_id }) || child.outcome_error || child.activity_status);
312
358
  }
313
359
  rootRunning(result) {
314
360
  const status = String(result?.info?.status ?? result?.info?.state ?? result?.info?.data?.status ?? "unknown").toLowerCase();
@@ -316,7 +362,7 @@ export class Runtime {
316
362
  // Grok may retire a completed child Session before session/info can be
317
363
  // inspected. A retained TaskOutput for that exact attempt is native
318
364
  // terminal proof; missing session/info alone is not.
319
- if (status === "unknown" && child?.source === "x.ai/tool/TaskOutput" && !child.outcome_error && !child.activity_status
365
+ if (status === "unknown" && ["x.ai/tool/TaskOutput", "x.ai/subagent/event"].includes(child?.source) && child.attempt_id && !child.outcome_error && !child.activity_status
320
366
  && ["completed", "failed", "cancelled"].includes(child.status)) return false;
321
367
  // Grok 1.0.17 omits status from session/info. Only a durable native
322
368
  // creation/end_turn receipt can then establish an idle root. Dispatch
@@ -326,6 +372,7 @@ export class Runtime {
326
372
  return !["completed", "failed", "cancelled", "stopped", "idle"].includes(status);
327
373
  }
328
374
  async refreshTree(cancel = false) {
375
+ await this.drainTerminal();
329
376
  const running = [];
330
377
  for (const session of [...this.sessions.values()].filter(item => !item.parent_provider_session_id)) {
331
378
  const id = session.provider_session_id;
@@ -357,15 +404,21 @@ export class Runtime {
357
404
  const timer = setInterval(() => { void this.observeHookFailures(current).catch(error => current.reject(error)); }, 100);
358
405
  // Only a new native turn invalidates its own root proof. Creation, fork
359
406
  // and local archive export must not erase an unrelated Session's receipt.
360
- if (name === "session.prompt" && this.sessions.has(sessionId)) this.sessions.get(sessionId).native_root_receipt = null;
407
+ if (name === "session.prompt" && this.sessions.has(sessionId)) {
408
+ const session = this.sessions.get(sessionId);
409
+ session.native_root_receipt = null;
410
+ this.bridge.dispatchGeneration = current.generation;
411
+ session.native_prompt_operation = { daemon_id: this.state.daemon_id, connection_id: this.bridge.connectionId, operation_id: operationId, generation: current.generation };
412
+ }
361
413
  try {
362
414
  await this.persist({ active_tree: true, active_operation: name });
363
415
  assertDaemonDispatch(this.paths?.dir);
364
416
  const before = new Map(this.descendants);
365
417
  const result = await Promise.race([task(), hookFailed]);
418
+ await this.drainTerminal();
366
419
  this.track(result, before);
367
420
  const session = this.sessions.get(result.provider_session_id);
368
- if (session && (["session.create", "session.fork"].includes(name) || ["end_turn", "cancelled"].includes(result.turn?.stopReason))) session.native_root_receipt = { kind: name === "session.prompt" ? result.turn.stopReason : "created", daemon_id: this.state.daemon_id };
421
+ if (session && ["session.create", "session.fork"].includes(name)) session.native_root_receipt = { kind: "created", daemon_id: this.state.daemon_id };
369
422
  // Archive requires an idle tree before entering this exclusive section
370
423
  // and performs no native work. Its file receipt is not tree evidence.
371
424
  const running = name === "session.archive" ? false : this.treeRunning(result);
@@ -375,11 +428,7 @@ export class Runtime {
375
428
  // A matched JSON-RPC error completes this native prompt request. It is
376
429
  // root-turn proof, not proof that any descendants have stopped. A lost
377
430
  // response, bridge error, or failure before dispatch earns no receipt.
378
- const response = error?.details?.terminal_response;
379
- if (name === "session.prompt" && response?.method === "session/prompt" && response.session_id === sessionId) {
380
- const session = this.sessions.get(sessionId);
381
- if (session) session.native_root_receipt = { kind: "failed", daemon_id: this.state.daemon_id, request_id: response.request_id };
382
- }
431
+ try { await this.drainTerminal(); } catch (storage) { error.cleanup_error ??= errorRecord(storage); }
383
432
  try { await hookBudget(1_000, null).run(() => this.persist({ active_tree: true, active_operation: null }), "failure_persistence"); }
384
433
  catch (storage) { error.cleanup_error = errorRecord(storage); }
385
434
  throw error;
@@ -441,10 +490,11 @@ export class Runtime {
441
490
 
442
491
  export async function serveDaemon(stateDir) {
443
492
  const paths = locations(stateDir); const state = await readState(paths.dir); if (!state || state.schema_id !== STATE_SCHEMA) throw new DaemonError("daemon_state_missing", "daemon state is missing or incompatible"); let runtime;
444
- const bridge = new AcpBridge({ ...state.config, initializeTimeoutMs: 60_000, commandArgs: ["agent", "--no-leader", "--always-approve", "--model", state.config.model, "--reasoning-effort", state.config.reasoning, "stdio"], env: isolatedEnv(state.config), clientInfo: { name: "dd-grok", version: "0.1.0" }, permission: "allow", onNotification: async (message) => {
493
+ const bridge = new AcpBridge({ ...state.config, initializeTimeoutMs: 60_000, commandArgs: ["agent", "--no-leader", "--always-approve", "--model", state.config.model, "--reasoning-effort", state.config.reasoning, "stdio"], env: isolatedEnv(state.config), clientInfo: { name: "dd-grok", version: "0.1.0" }, permission: "allow", onNotification: async (message, observation) => {
445
494
  const sessionId = message?.params?.sessionId;
446
495
  if (!runtime || !sessionId) return;
447
496
  try {
497
+ await runtime.observeRootActivity(message, observation);
448
498
  runtime.observeToolCall(message);
449
499
  for (const fact of runtime.observeSubagentEvent(message)) {
450
500
  const source = { channel: "x.ai/subagent_spawned", scope: "configured", attempt_id: fact.attemptId, parent_prompt_id: fact.parentPromptId,
@@ -463,6 +513,13 @@ export async function serveDaemon(stateDir) {
463
513
  const rootProviderSessionId = session?.root_provider_session_id ?? sessionId;
464
514
  await observeAcpToolCall(state.config, { daemonId: state.daemon_id, rootProviderSessionId, parentProviderSessionId: session?.parent_provider_session_id ?? null, observedProfile: bridge.modelProfiles?.get(sessionId) ?? {} }, message);
465
515
  } }); const initialized = await startManagedBridge({ config: state.config, state, bridge, kind: "grok-provider", publish: snapshot => writeState(paths.state, snapshot), log: paths.log }); runtime = new Runtime(paths, state, bridge, initialized);
516
+ bridge.configure({ onTerminalOutcome: outcome => runtime.reduceTerminal(outcome), onPromptDispatch: async dispatch => {
517
+ const turn = runtime.sessions.get(dispatch.session_id)?.native_prompt_operation;
518
+ if (!turn || turn.operation_id !== dispatch.operation_id || turn.connection_id !== dispatch.connection_id || turn.generation !== dispatch.generation) throw new DaemonError("native_dispatch_binding_mismatch", "Native prompt dispatch differs from retained operation");
519
+ turn.native_request_id = dispatch.native_request_id;
520
+ await runtime.persist();
521
+ } });
522
+ await runtime.restoreTerminals();
466
523
  for (const fact of await readModelObservations(state.config.journal)) {
467
524
  if (fact.harness === "grok-acp" && fact.source?.channel === "x.ai/subagent_spawned" && runtime.sessions.has(fact.session_id)
468
525
  && (runtime.descendants.get(fact.session_id)?.attempt_id ?? null) === (fact.source.attempt_id ?? null) && fact.observed?.model) {
@@ -102,7 +102,7 @@ async function identity(bridge, options) {
102
102
  return sessionId;
103
103
  }
104
104
 
105
- async function cancelTree(bridge, sessionId) {
105
+ async function cancelTree(bridge, sessionId, options = {}) {
106
106
  const before = await subagents(bridge, sessionId).catch(error => ({ observation_error: { code: error.code, message: error.message } })); const cancellations = [];
107
107
  for (const child of before.subagents ?? []) {
108
108
  const childId = child.sessionId ?? child.subagentId;
@@ -119,7 +119,10 @@ async function cancelTree(bridge, sessionId) {
119
119
  if (!(after.subagents ?? []).length) {
120
120
  const root = await sessionInfo(bridge, sessionId).catch(error => ({ observation_error: { code: error.code, message: error.message } }));
121
121
  const status = root?.status ?? root?.state ?? root?.data?.status ?? null;
122
- return { before, after, cancellations, root_status: status, settled: ["completed", "failed", "cancelled", "stopped", "idle"].includes(String(status).toLowerCase()) };
122
+ await bridge.terminalOutcomes;
123
+ const retained = options.rootSettled?.(sessionId) === true;
124
+ if (status === null && !retained) continue;
125
+ return { before, after, cancellations, root_status: status, settled: options.rootSettled ? retained : ["completed", "failed", "cancelled", "stopped", "idle"].includes(String(status).toLowerCase()) };
123
126
  }
124
127
  }
125
128
  const root = await sessionInfo(bridge, sessionId).catch(() => null);
@@ -163,7 +166,7 @@ export async function promptSessionWithBridge(bridge, options) {
163
166
  export async function inspectSession(options) { requireJournal(options); return await withBridge(options, async (bridge, initialized) => await inspectSessionWithBridge(bridge, { ...options, initialized })); }
164
167
  export async function inspectSessionWithBridge(bridge, options) { requireJournal(options); const sessionId = await identity(bridge, options); const evidence = await inspect(bridge, sessionId, options); evidence.tool_calls = sessionToolCalls(options, bridge, sessionId); const usage_ingest_error = await forwardUsageBestEffort(options, sessionId, evidence.usage, evidence.tool_calls); if (usage_ingest_error) evidence.usage_ingest_error = usage_ingest_error; return receipt(options, { harness: "grok-acp", provider_session_id: sessionId, adapter_session_id: sessionId, ...evidence }); }
165
168
  export async function cancelSession(options) { requireJournal(options); return await withBridge(options, async (bridge) => await cancelSessionWithBridge(bridge, options)); }
166
- export async function cancelSessionWithBridge(bridge, options) { requireJournal(options); const sessionId = await identity(bridge, options); return receipt(options, { harness: "grok-acp", provider_session_id: sessionId, adapter_session_id: sessionId, ...await cancelTree(bridge, sessionId) }); }
169
+ export async function cancelSessionWithBridge(bridge, options) { requireJournal(options); const sessionId = await identity(bridge, options); return receipt(options, { harness: "grok-acp", provider_session_id: sessionId, adapter_session_id: sessionId, ...await cancelTree(bridge, sessionId, options) }); }
167
170
  export async function forkSession(options) { requireJournal(options); return await withBridge(options, async (bridge) => await forkSessionWithBridge(bridge, options)); }
168
171
  export async function forkSessionWithBridge(bridge, options) {
169
172
  requireJournal(options); const sessionId = await identity(bridge, options); const target = options.target;
@@ -3,7 +3,7 @@ import { ToolObservations, acpToolObservation, zcodePhysicalSession } from "./to
3
3
  import { hookBudget, invokeNativeHook } from "./native-hook-command.mjs";
4
4
  import { parseLifecycleCommand, continuationCommand, commandOption } from "./lifecycle-command.mjs";
5
5
  import { spawn } from "node:child_process";
6
- import { createHash } from "node:crypto";
6
+ import { createHash, randomUUID } from "node:crypto";
7
7
  import { createReadStream } from 'node:fs';
8
8
  import { publishSetup, SETUP_STARTUP_MS } from './setup-progress.mjs';
9
9
  import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
@@ -141,6 +141,10 @@ export class AcpBridge {
141
141
  this.expiredRequests = new Map();
142
142
  this.sessionActivity = new Map();
143
143
  this.notifications = Promise.resolve();
144
+ this.terminalOutcomes = Promise.resolve();
145
+ this.terminalRequests = new Map();
146
+ this.connectionId = randomUUID();
147
+ this.receivedSequence = 0;
144
148
  this.lifecycleNotifications = new Set();
145
149
  this.notificationError = null;
146
150
  this.bridgeStderr = "";
@@ -202,16 +206,27 @@ export class AcpBridge {
202
206
  receive(line) {
203
207
  let message;
204
208
  try { message = JSON.parse(line); } catch { this.journal.write("malformed", { line }); return; }
209
+ const observation = { connection_id: this.connectionId, sequence: ++this.receivedSequence, productive: isProductiveSessionNotification(message) };
205
210
  const request = message.id !== undefined ? this.pending.get(message.id) : null;
206
211
  const expired = message.id !== undefined ? this.expiredRequests.get(message.id) : null;
207
212
  const isResponse = message.id !== undefined && !message.method;
208
213
  const rejected = isResponse && this.rejectedHookRequests?.get(message.id);
209
- if (rejected) {
210
- this.rejectedHookRequests.delete(message.id);
211
- const terminal = { request_id: message.id, method: rejected.method, session_id: rejected.sessionId, terminal_response: message };
214
+ if (rejected || isResponse && (request?.method === "session/prompt" || this.terminalRequests.has(message.id))) {
215
+ const owner = rejected ?? request ?? this.terminalRequests.get(message.id);
216
+ const terminal = { request_id: message.id, method: owner.method, session_id: owner.sessionId, terminal_response: message,
217
+ connection_id: this.connectionId, operation_id: owner.operationId ?? null, generation: owner.generation ?? null, native_sequence: observation.sequence };
218
+ this.terminalOutcomes = this.terminalOutcomes.then(async () => {
219
+ await this.journal.write(rejected ? "native_terminal_after_hook_failure" : "native_terminal_outcome", terminal);
220
+ await this.journal.flush();
221
+ await this.options.onTerminalOutcome?.(terminal);
222
+ this.terminalRequests.set(message.id, owner);
223
+ while (this.terminalRequests.size > 128) this.terminalRequests.delete(this.terminalRequests.keys().next().value);
224
+ if (rejected) this.rejectedHookRequests.delete(message.id);
225
+ });
226
+ void this.terminalOutcomes.catch(storage => { this.terminalProjectionError ??= storage; if (rejected) rejected.error.cleanup_error ??= errorRecord(storage); });
227
+ if (rejected) {
212
228
  rejected.error.details = { ...rejected.error.details, native_terminal_after_hook_failure: terminal };
213
- try { void Promise.resolve(this.journal.write("native_terminal_after_hook_failure", terminal)).catch(storage => { rejected.error.cleanup_error ??= errorRecord(storage); }); }
214
- catch (storage) { rejected.error.cleanup_error ??= errorRecord(storage); }
229
+ }
215
230
  }
216
231
  if (!isResponse || request) this.journal.writeInbound(message, request?.method);
217
232
  if (typeof message.params?.sessionId === "string" && isProductiveSessionNotification(message)) this.observeActivity(message.params.sessionId, message.params.update);
@@ -305,7 +320,7 @@ export class AcpBridge {
305
320
  return;
306
321
  }
307
322
  if (message.method) this.notifications = this.notifications
308
- .then(async () => { await this.observeModelUpdate(message); await this.options.onNotification?.(message); })
323
+ .then(async () => { await this.observeModelUpdate(message); await this.options.onNotification?.(message, observation); })
309
324
  .catch(async (error) => {
310
325
  const failure = { code: error.code ?? "notification_observation_failed", message: error.message, ...(error.details ? { details: error.details } : {}), session_id: message.params?.sessionId ?? null, tool_call_id: message.params?.update?.toolCallId ?? null, event_time: now(), delivery_stage: "acp_evidence", completeness: "incomplete" };
311
326
  try { await this.journal.write("notification_failure", failure); }
@@ -405,6 +420,7 @@ export class AcpBridge {
405
420
  }
406
421
 
407
422
  request(method, params = {}, timeoutMs = 30_000, { idleSessionId = null, idleTimeoutMs = null, inactivityMode = "reject" } = {}) {
423
+ if (method === "session/prompt" && (this.rejectedHookRequests?.size ?? 0) + [...this.pending.values()].filter(request => request.method === "session/prompt").length >= 32) return Promise.reject(Object.assign(new Error("Unreconciled native terminal evidence limit reached"), { code: "native_terminal_evidence_pending" }));
408
424
  if (timeoutMs !== null) validateObservationDuration(timeoutMs);
409
425
  if (idleTimeoutMs !== null) validateObservationDuration(idleTimeoutMs);
410
426
  if ((this.lifecycleFailure || this.notificationError?.code === "profile_integrity_violation") && ["session/new", "session/prompt", "session/fork"].includes(method)) return Promise.reject(this.lifecycleFailure ?? this.notificationError);
@@ -467,9 +483,19 @@ export class AcpBridge {
467
483
  void this.journal.write("inactivity_observation_failure", { method, request_id: id, session_id: idleSessionId, error: { code: error?.code ?? "inactivity_observation_failed", message: error?.message ?? String(error) } }).catch(() => {});
468
484
  });
469
485
  }, Math.max(10, Math.min(1_000, Math.floor(validIdleTimeout / 4)))) : null;
470
- this.pending.set(id, { id, resolve, reject, timer, idleTimer, method, sessionId: params.sessionId ?? null, dispatchedAt: Date.now(), publish: operationProgressReporter() });
471
- this.send({ jsonrpc: "2.0", id, method, params });
472
- if (method === "session/prompt") this.journal.write("native_dispatch", { operation_id: this.dispatchOperationId ?? process.env.DD_EVAL_OPERATION_ID ?? null, session_id: params.sessionId, native_request_id: id });
486
+ this.pending.set(id, { id, resolve, reject, timer, idleTimer, method, sessionId: params.sessionId ?? null, operationId: this.dispatchOperationId ?? process.env.DD_EVAL_OPERATION_ID ?? null, generation: this.dispatchGeneration ?? null, dispatchedAt: Date.now(), publish: operationProgressReporter() });
487
+ if (method === "session/prompt") {
488
+ void (async () => {
489
+ const dispatch = { operation_id: this.pending.get(id)?.operationId, session_id: params.sessionId, native_request_id: id,
490
+ connection_id: this.connectionId, generation: this.pending.get(id)?.generation };
491
+ await this.journal.write("native_dispatch_prepared", dispatch);
492
+ await this.journal.flush();
493
+ await this.options.onPromptDispatch?.(dispatch);
494
+ if (!this.pending.has(id) || this.pending.get(id).timedOut || this.lifecycleFailure) return;
495
+ this.send({ jsonrpc: "2.0", id, method, params });
496
+ await this.journal.write("native_dispatch", dispatch);
497
+ })().catch(finish);
498
+ } else this.send({ jsonrpc: "2.0", id, method, params });
473
499
  });
474
500
  }
475
501
 
@@ -488,14 +514,14 @@ export class AcpBridge {
488
514
  rejectAll(error) {
489
515
  for (const [id, pending] of this.pending) {
490
516
  if (this.notificationError === error && pending.method === "session/prompt") {
491
- this.rejectedHookRequests ??= new Map(); this.rejectedHookRequests.set(id, { method: pending.method, sessionId: pending.sessionId, error });
492
- if (this.rejectedHookRequests.size > 32) this.rejectedHookRequests.delete(this.rejectedHookRequests.keys().next().value);
517
+ this.rejectedHookRequests ??= new Map(); this.rejectedHookRequests.set(id, { method: pending.method, sessionId: pending.sessionId, operationId: pending.operationId, generation: pending.generation, error });
493
518
  }
494
519
  clearTimeout(pending.timer); if (pending.idleTimer) clearInterval(pending.idleTimer); pending.reject(error);
495
520
  }
496
521
  this.pending.clear();
497
522
  }
498
523
  async flush() {
524
+ await this.terminalOutcomes;
499
525
  if (this.notificationError) throw this.notificationError;
500
526
  try { await Promise.all([...this.lifecycleNotifications]); await this.notifications; await this.journal.flush(); }
501
527
  catch (error) { throw this.notificationError ?? error; }
@@ -507,6 +533,7 @@ export class AcpBridge {
507
533
  // Draining them must not relabel a successful physical shutdown as failed.
508
534
  return await closeBridgeProcess(this, async () => {
509
535
  await Promise.all([this.nativeDrain, this.diagnostics]);
536
+ await this.terminalOutcomes;
510
537
  await Promise.allSettled([...this.lifecycleNotifications, this.notifications]);
511
538
  if (this.notificationError) await hookBudget(1_000, null).run(() => this.journal.flush(), "failure_drain");
512
539
  else await this.journal.flush();
@@ -964,6 +991,7 @@ export function zcodeLifecycleQualification(sha256, bridgeCommit, harnessContrac
964
991
  native_nested_children: false, reason: qualified ? null : "native_event_contract_unqualified" };
965
992
  }
966
993
 
994
+ let doctorDigestScanOrdinal = 0;
967
995
  export async function doctor(options = {}) {
968
996
  const zcodeAcp = await commandOutput(options.bin, ["--version"]);
969
997
  const zcodeAcpCommit = await commandOutput(options.bin, ["--dd-harness-commit"]);
@@ -971,11 +999,13 @@ export async function doctor(options = {}) {
971
999
  const zcodePath = options.zcodePath ?? process.env.ZCODE_BIN ?? process.env.ZCODE_PATH ?? "/Applications/ZCode.app/Contents/Resources/glm/zcode.cjs";
972
1000
  const zcode = await commandOutput(zcodePath, ["--version"]);
973
1001
  const hash = createHash('sha256'); let bytes = 0;
1002
+ const scan = `zcode-digest:${zcodePath}:scan-${++doctorDigestScanOrdinal}`;
1003
+ publishSetup('probe_started', scan);
974
1004
  for await (const chunk of createReadStream(zcodePath)) {
975
1005
  hash.update(chunk); bytes += chunk.length;
976
- if (bytes % (4 * 1024 * 1024) < chunk.length) publishSetup('identity_digest_progress', bytes);
1006
+ if (bytes % (4 * 1024 * 1024) < chunk.length) publishSetup('identity_digest_progress', `${scan}:${bytes}`);
977
1007
  }
978
- const sha256 = hash.digest('hex'); publishSetup('identity_digest_completed');
1008
+ const sha256 = hash.digest('hex'); publishSetup('identity_digest_completed', scan);
979
1009
  const qualification = zcodeLifecycleQualification(sha256, zcodeAcpCommit, ddHarness);
980
1010
  return { compatible: qualification.status === "qualified", contract_supported: supportedZcodeHarnessContract(ddHarness), lifecycle_qualification: qualification, observed_runtime: { zcode, zcode_acp: zcodeAcp, zcode_acp_commit: zcodeAcpCommit, dd_harness_contract: ddHarness } };
981
1011
  }