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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (254) hide show
  1. package/CHANGELOG.md +130 -0
  2. package/README.md +13 -8
  3. package/dist/build-info.json +7 -7
  4. package/dist/cli/command-inputs.js +13 -3
  5. package/dist/cli/command-metadata.js +51 -16
  6. package/dist/cli/diagnostic-invocation.js +46 -0
  7. package/dist/cli/diagnostic.js +120 -0
  8. package/dist/cli/help.js +67 -1
  9. package/dist/cli/hook-ingress.js +27 -3
  10. package/dist/cli/memory-flow-admission.js +9 -3
  11. package/dist/cli/memory-init-invocation.js +10 -3
  12. package/dist/cli/memory-init.js +77 -68
  13. package/dist/cli/memory-storage-admission.js +3 -2
  14. package/dist/cli/memory-storage-qualification.js +105 -0
  15. package/dist/cli/memory-storage-transition.js +123 -0
  16. package/dist/cli/memory-upgrade.js +189 -0
  17. package/dist/cli/native-hook-ingress.js +14 -4
  18. package/dist/cli/native-hook-preparation.js +55 -0
  19. package/dist/cli/response-file.js +2 -1
  20. package/dist/cli/run-cli.js +79 -40
  21. package/dist/cli/setup.js +2 -1
  22. package/dist/cli.js +18 -0
  23. package/dist/domain/diagnostic-aspects.js +103 -0
  24. package/dist/domain/diagnostic-flows.js +8 -0
  25. package/dist/domain/stage-handler-identities.js +8 -0
  26. package/dist/harness-runtime/bin/dd-agy.mjs +3 -3
  27. package/dist/harness-runtime/bin/dd-grok.mjs +3 -3
  28. package/dist/harness-runtime/bin/dd-zcode.mjs +5 -5
  29. package/dist/harness-runtime/lib/agy-error-boundary.mjs +58 -0
  30. package/dist/harness-runtime/lib/agy-native-inspection.mjs +216 -0
  31. package/dist/harness-runtime/lib/agy-native_pb.js +117 -0
  32. package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
  33. package/dist/harness-runtime/lib/daemon-operations.mjs +84 -10
  34. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +366 -52
  35. package/dist/harness-runtime/lib/dd-agy.mjs +17 -3
  36. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +27 -13
  37. package/dist/harness-runtime/lib/dd-codex.mjs +4 -0
  38. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +4 -3
  39. package/dist/harness-runtime/lib/dd-droid.mjs +71 -29
  40. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +73 -16
  41. package/dist/harness-runtime/lib/dd-grok.mjs +6 -3
  42. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +66 -20
  43. package/dist/harness-runtime/lib/dd-opencode.mjs +30 -3
  44. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +7 -6
  45. package/dist/harness-runtime/lib/dd-zcode.mjs +44 -14
  46. package/dist/harness-runtime/lib/delegation-instructions.mjs +1 -1
  47. package/dist/harness-runtime/lib/driver-recovery.mjs +9 -8
  48. package/dist/harness-runtime/lib/entity-references.d.mts +2 -0
  49. package/dist/harness-runtime/lib/entity-references.mjs +9 -0
  50. package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -1
  51. package/dist/harness-runtime/lib/managed-daemon.mjs +24 -9
  52. package/dist/harness-runtime/lib/native-children.d.mts +8 -1
  53. package/dist/harness-runtime/lib/native-children.mjs +114 -8
  54. package/dist/harness-runtime/lib/native-hook-command.mjs +4 -4
  55. package/dist/harness-runtime/lib/native-request-proof.mjs +1 -1
  56. package/dist/harness-runtime/lib/observation-clock.d.mts +2 -2
  57. package/dist/harness-runtime/lib/observation-clock.mjs +7 -4
  58. package/dist/harness-runtime/lib/opencode-plugin.mjs +3 -3
  59. package/dist/harness-runtime/lib/operation-errors.mjs +1 -1
  60. package/dist/harness-runtime/lib/provider-overload-policy.d.mts +5 -3
  61. package/dist/harness-runtime/lib/provider-overload-policy.mjs +20 -7
  62. package/dist/harness-runtime/lib/runner-events.mjs +3 -1
  63. package/dist/harness-runtime/lib/setup-progress.mjs +8 -6
  64. package/dist/harness-runtime/lib/tool-observations.mjs +1 -1
  65. package/dist/schemas/diagnostic-stage-result.schema.json +354 -0
  66. package/dist/schemas/diagnostic-work-result.schema.json +296 -0
  67. package/dist/schemas/execution-bundle.schema.json +7 -1
  68. package/dist/schemas/mb-upgrade-stage-result.schema.json +863 -0
  69. package/dist/schemas/memory-repair-work-result.schema.json +9 -2
  70. package/dist/schemas/project-flow-pack-manifest.schema.json +1 -1
  71. package/dist/schemas/release-impact.schema.json +3 -3
  72. package/dist/schemas/stage-start-response.schema.json +4 -0
  73. package/dist/services/accepted-intent.js +13 -1
  74. package/dist/services/canon.js +43 -7
  75. package/dist/services/check-environment.js +21 -0
  76. package/dist/services/check-runner-evidence.js +267 -15
  77. package/dist/services/cli-operation-classifier.js +1 -1
  78. package/dist/services/cli-upgrade-allowlist.js +1 -1
  79. package/dist/services/code-checks.js +34 -19
  80. package/dist/services/compatibility-preflight.js +5 -2
  81. package/dist/services/controller-execution.js +10 -4
  82. package/dist/services/controller-fanout.js +34 -10
  83. package/dist/services/controller-memory-task.js +45 -0
  84. package/dist/services/controller-memory-worker.js +49 -0
  85. package/dist/services/dashboard.js +2 -2
  86. package/dist/services/descriptor-run-settlement.js +5 -3
  87. package/dist/services/descriptor-run-start.js +46 -20
  88. package/dist/services/descriptor-stage-coordinator.js +66 -13
  89. package/dist/services/descriptor-stage-settlement.js +2 -1
  90. package/dist/services/descriptor-stage-start.js +40 -4
  91. package/dist/services/diagnostic-evidence.js +73 -0
  92. package/dist/services/diagnostic-report-publication.js +72 -0
  93. package/dist/services/diagnostic-report.js +67 -0
  94. package/dist/services/diagnostic-run-bundle.js +74 -0
  95. package/dist/services/diagnostic-snapshot.js +159 -0
  96. package/dist/services/diagnostic-stage-handlers.js +108 -0
  97. package/dist/services/diagnostic-stage-results.js +132 -0
  98. package/dist/services/diagnostic-start.js +84 -0
  99. package/dist/services/diagnostic-work-batch.js +34 -0
  100. package/dist/services/diagnostic-work-collection.js +97 -0
  101. package/dist/services/diagnostic-work-registration.js +101 -0
  102. package/dist/services/diagnostic-work-results.js +93 -0
  103. package/dist/services/engines.js +64 -14
  104. package/dist/services/eval-snapshots.js +186 -119
  105. package/dist/services/execution-bundles.js +19 -7
  106. package/dist/services/external-work-launch.js +5 -3
  107. package/dist/services/flow-catalog.js +13 -4
  108. package/dist/services/frozen-engine.js +148 -11
  109. package/dist/services/hook-memory-routing.js +188 -0
  110. package/dist/services/hook-recovery-guard.js +14 -0
  111. package/dist/services/hook-session-observation.js +24 -0
  112. package/dist/services/hooks.js +27 -8
  113. package/dist/services/hybrid-contracts.js +4 -13
  114. package/dist/services/hybrid-digest.js +14 -0
  115. package/dist/services/installed-flow-source.js +47 -4
  116. package/dist/services/lifecycle-invocations.js +179 -38
  117. package/dist/services/memory-accepted-composition.js +5 -4
  118. package/dist/services/memory-completed-correction-wave.js +23 -15
  119. package/dist/services/memory-compose-handler.js +163 -21
  120. package/dist/services/memory-composition-acceptance.js +186 -10
  121. package/dist/services/memory-composition-graph.js +42 -11
  122. package/dist/services/memory-composition-registration.js +23 -8
  123. package/dist/services/memory-composition-work-custody.js +14 -9
  124. package/dist/services/memory-composition-work-results.js +15 -3
  125. package/dist/services/memory-corrected-review-batch.js +10 -3
  126. package/dist/services/memory-corrected-review-findings.js +4 -2
  127. package/dist/services/memory-corrected-review-inputs.js +10 -5
  128. package/dist/services/memory-corrected-review-receipt.js +17 -9
  129. package/dist/services/memory-corrected-review-registration.js +3 -2
  130. package/dist/services/memory-correction-check-receipt.js +20 -4
  131. package/dist/services/memory-custody-snapshot.js +36 -0
  132. package/dist/services/memory-finish-evidence.js +44 -22
  133. package/dist/services/memory-finish-handler.js +37 -23
  134. package/dist/services/memory-finish-integration.js +218 -48
  135. package/dist/services/memory-finish-readiness.js +66 -19
  136. package/dist/services/memory-git-integration.js +87 -18
  137. package/dist/services/memory-historical-corrected-review.js +21 -15
  138. package/dist/services/memory-historical-original-review.js +22 -9
  139. package/dist/services/memory-historical-repair-registration.js +14 -8
  140. package/dist/services/memory-historical-second-review.js +30 -11
  141. package/dist/services/memory-init-contexts.js +44 -2
  142. package/dist/services/memory-init-controller-start.js +27 -8
  143. package/dist/services/memory-init-git-policy.js +50 -21
  144. package/dist/services/memory-init-lint-capability.js +24 -6
  145. package/dist/services/memory-init-native-capacity.js +17 -4
  146. package/dist/services/memory-init-preflight.js +21 -17
  147. package/dist/services/memory-init-request-candidate.js +73 -17
  148. package/dist/services/memory-init-request-input.js +23 -7
  149. package/dist/services/memory-init-run-bundle.js +49 -12
  150. package/dist/services/memory-init-storage.js +17 -7
  151. package/dist/services/memory-init-worktrunk-capability.js +3 -7
  152. package/dist/services/memory-layer.js +71 -6
  153. package/dist/services/memory-lint-config.js +111 -2
  154. package/dist/services/memory-native-review-copy.js +5 -67
  155. package/dist/services/memory-normal-flow-entry.js +12 -0
  156. package/dist/services/memory-ordinary-defaults.js +22 -5
  157. package/dist/services/memory-preflight-handler.js +15 -0
  158. package/dist/services/memory-repair-current-view.js +1 -1
  159. package/dist/services/memory-repair-document-delta.js +70 -33
  160. package/dist/services/memory-repair-history.js +12 -7
  161. package/dist/services/memory-repair-physical-binding.js +1 -119
  162. package/dist/services/memory-repair-publication-plan.js +5 -3
  163. package/dist/services/memory-repair-publication-source.js +1 -1
  164. package/dist/services/memory-repair-publication.js +3 -2
  165. package/dist/services/memory-repair-registration.js +2 -1
  166. package/dist/services/memory-repair-work-baseline.js +4 -2
  167. package/dist/services/memory-repair-work-completion.js +32 -12
  168. package/dist/services/memory-repair-work-results.js +21 -2
  169. package/dist/services/memory-repair-workspace.js +11 -2
  170. package/dist/services/memory-repair-writer-copy.js +2 -1
  171. package/dist/services/memory-result-binding.js +78 -12
  172. package/dist/services/memory-retained-review-authority.js +17 -8
  173. package/dist/services/memory-retained-second-baseline.js +1 -1
  174. package/dist/services/memory-retained-second-repairs.js +12 -6
  175. package/dist/services/memory-review-correction-plan.js +47 -12
  176. package/dist/services/memory-review-handler.js +17 -17
  177. package/dist/services/memory-review-inputs.js +55 -9
  178. package/dist/services/memory-review-limits.js +10 -0
  179. package/dist/services/memory-review-receipt.js +2 -2
  180. package/dist/services/memory-review-registration.js +13 -4
  181. package/dist/services/memory-review-work-custody.js +3 -1
  182. package/dist/services/memory-review-work-input.js +26 -7
  183. package/dist/services/memory-review-work-results.js +26 -4
  184. package/dist/services/memory-second-corrected-review-inputs.js +5 -3
  185. package/dist/services/memory-second-correction-plan.js +2 -1
  186. package/dist/services/memory-second-repair-launch.js +4 -4
  187. package/dist/services/memory-second-repair-source.js +1 -1
  188. package/dist/services/memory-stage-dispatch.js +38 -1
  189. package/dist/services/memory-stage-results.js +113 -22
  190. package/dist/services/memory-tool-probe.js +25 -0
  191. package/dist/services/memory-upgrade-analysis-handler.js +243 -0
  192. package/dist/services/memory-upgrade-bootstrap-readiness.js +251 -0
  193. package/dist/services/memory-upgrade-evidence.js +100 -0
  194. package/dist/services/memory-upgrade-inventory.js +442 -0
  195. package/dist/services/memory-upgrade-publication-custody.js +73 -0
  196. package/dist/services/memory-upgrade-publication.js +413 -0
  197. package/dist/services/memory-upgrade-selection.js +430 -0
  198. package/dist/services/memory-upgrade-start.js +60 -0
  199. package/dist/services/memory-upgrade-storage-readiness.js +340 -0
  200. package/dist/services/memory-verification-acceptance.js +38 -17
  201. package/dist/services/memory-verification-checks.js +92 -27
  202. package/dist/services/memory-verification-receipt.js +6 -5
  203. package/dist/services/memory-verify-handler.js +15 -13
  204. package/dist/services/memory-work-result-json.js +12 -0
  205. package/dist/services/memory-work-result-preparation.js +5 -4
  206. package/dist/services/merge-server.js +4 -2
  207. package/dist/services/migrations.js +127 -98
  208. package/dist/services/native-daemon-history.js +2 -1
  209. package/dist/services/physical-startup.js +45 -0
  210. package/dist/services/project-registry.js +97 -0
  211. package/dist/services/project-summary.js +1 -1
  212. package/dist/services/projects.js +3 -93
  213. package/dist/services/prompt-delivery.js +71 -0
  214. package/dist/services/provider-capacity.js +112 -46
  215. package/dist/services/recovery-snapshot-database.js +32 -4
  216. package/dist/services/release-cache.js +8 -0
  217. package/dist/services/repair-intents.js +1 -12
  218. package/dist/services/repair-receipt.js +13 -0
  219. package/dist/services/run-control-worker.js +35 -18
  220. package/dist/services/run-control.js +2 -1
  221. package/dist/services/run-controller-adapter.js +2 -1
  222. package/dist/services/run-controller-process.js +4 -7
  223. package/dist/services/run-controller-recovery.js +8 -2
  224. package/dist/services/run-controller.js +215 -94
  225. package/dist/services/run-fork.js +1 -1
  226. package/dist/services/run-recovery-guard.js +30 -0
  227. package/dist/services/run-recovery.js +11 -44
  228. package/dist/services/run-start-custody.js +17 -5
  229. package/dist/services/runs.js +6 -1
  230. package/dist/services/runtime-scope-resume.js +12 -6
  231. package/dist/services/runtime-scope-stop.js +3 -2
  232. package/dist/services/runtime-scope-worker.js +46 -19
  233. package/dist/services/runtime-service.js +2 -1
  234. package/dist/services/sessions.js +3 -24
  235. package/dist/services/setup.js +63 -13
  236. package/dist/services/stage-handlers.js +22 -7
  237. package/dist/services/stage-pause.js +111 -44
  238. package/dist/services/version-status.js +1 -1
  239. package/dist/services/vnext-code.js +2 -2
  240. package/dist/services/vnext-fanout.js +51 -9
  241. package/dist/services/vnext-plan-review.js +3 -5
  242. package/dist/services/work-registry.js +117 -39
  243. package/dist/services/workspace-bootstrap.js +2 -1
  244. package/dist/services/workspace-files.js +1 -1
  245. package/dist/shared/entity-references.js +1 -8
  246. package/dist/storage/data-preservation.js +65 -0
  247. package/dist/storage/database.js +53 -19
  248. package/dist/storage/import-guard.js +84 -27
  249. package/dist/storage/memory-upgrade-schema-bootstrap.js +38 -0
  250. package/dist/storage/paired-writer-migration.js +424 -0
  251. package/dist/storage/writer-contract.js +11 -4
  252. package/dist/storage/writer-migration.js +313 -52
  253. package/dist/storage/writer-restore.js +217 -0
  254. package/package.json +13 -6
@@ -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 {
@@ -7,12 +7,13 @@ import { captureDispatchGuard, cancelPendingDispatch } from "./dispatch-fence.mj
7
7
  import { createHash, randomUUID } from "node:crypto";
8
8
  import { chmod, lstat, mkdir, open, readFile, rename, unlink, writeFile } from "node:fs/promises";
9
9
  import net from "node:net";
10
- import { durableDaemonDispatch, completeDaemonShutdown, assertDaemonDispatch, assertDaemonRestartable, trackDaemonConnections, retireDaemonEndpoint, retireFailedDaemon } from "./daemon-operations.mjs";
10
+ import { durableDaemonDispatch, completeDaemonShutdown, commitDaemonShutdown, assertDaemonDispatch, assertDaemonRestartable, trackDaemonConnections, retireDaemonEndpoint, retireFailedDaemon } from "./daemon-operations.mjs";
11
11
  import { ObservationClock } from "./observation-clock.mjs";
12
12
  import { parseCapacityOperation } from "./provider-overload-policy.mjs";
13
13
  import path from "node:path";
14
14
  import { spawn } from "node:child_process";
15
15
  import { waitForSettlement } from "./session-settlement.mjs";
16
+ import { nativeExecutionReceipt } from "./native-children.mjs";
16
17
  import { errorRecord } from "./operation-errors.mjs";
17
18
  import { CODEX_REQUEST_TIMEOUT_MS, LATE_RESPONSE_GRACE_MS, CodexBridge, codexReadPhase, readCodexHistory, retainedCodexTurn, cancelSessionWithBridge, createSessionWithBridge, inspectSessionWithBridge, promptSessionWithBridge, startSessionWithBridge } from "./dd-codex.mjs";
18
19
  import { assertDaemonOwnership, assertStartupChild, prepareRuntimeOwner, cleanupFailedDaemonStart, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, scheduleDaemonProcessHeartbeat, registerDaemonProcess, startManagedBridge, stopProcessGroup } from "./managed-daemon.mjs";
@@ -77,7 +78,7 @@ export async function inspectCodexTree(bridge, options, { remember = () => {}, a
77
78
  options = { ...options, readPhase: options.readPhase ?? codexReadPhase(bridge, options) };
78
79
  const sessions = new Set([options.sessionId]);
79
80
  const observed = await observeCodexSessions(bridge, options, sessions, active, remember);
80
- return { ...observed.observations[0], settled: !observed.active && !observed.sessions.length, unsettled_sessions: observed.sessions, tree_session_ids: [...sessions], tree_observations: observed.observations.map(item => ({ provider_session_id: item.provider_session_id, status: item.status, settled: item.settled })) };
81
+ return nativeExecutionReceipt({ ...observed.observations[0], settled: !observed.active && !observed.sessions.length, unsettled_sessions: observed.sessions, tree_session_ids: [...sessions], tree_observations: observed.observations.map(item => ({ provider_session_id: item.provider_session_id, status: item.status, settled: item.settled })) }, { daemonId: options.daemonId });
81
82
  }
82
83
 
83
84
  export async function cancelCodexTree(bridge, options, { remember = () => {}, active = () => null, timeoutMs = 5_000 } = {}) {
@@ -120,32 +121,44 @@ export async function startDaemon(options) {
120
121
  }
121
122
  export async function stopDaemon(options) { const timeoutMs = options.timeoutMs ?? 30_000; return await confirmDaemonStopped(() => callDaemon(options.stateDir, "daemon.stop", { cancelTree: options.cancelTree === true }, timeoutMs), locations(options.stateDir).socket, timeoutMs, { readState: () => readState(locations(options.stateDir).state) }); }
122
123
 
124
+ export async function prepareCodexStop({ bridge, state, sessions, active, save, stateDir, cancelTree }) {
125
+ if (cancelTree) cancelPendingDispatch(bridge);
126
+ if (active() && !cancelTree) throw Object.assign(new Error("productive operation is running"), { code: "tree_not_settled" });
127
+ let revision;
128
+ await waitForSettlement({
129
+ knownSessions: () => [...sessions],
130
+ observe: () => { revision = state.native_execution_revision; return observeCodexSessions(bridge, state.config, sessions, active); },
131
+ ...(cancelTree ? { cancel: async sessionId => { try { await cancelSessionWithBridge(bridge, { ...state.config, sessionId }); } catch (error) { if (error.code !== "turn_not_active") throw error; } } } : {})
132
+ });
133
+ if (active() || state.native_execution_revision !== revision) throw Object.assign(new Error("native activity changed during settlement observation"), { code: "tree_not_settled" });
134
+ commitDaemonShutdown(stateDir);
135
+ await save({ active_tree: false });
136
+ return { clean: true, settled: true, native_settled: true, native_settlement: { confirmed: true, revision } };
137
+ }
138
+
123
139
  export async function serveDaemon(stateDir) {
124
140
  const paths = locations(stateDir); const state = await readState(paths.state); if (!state || state.schema_id !== stateSchema) throw Object.assign(new Error("daemon state missing"), { code: "daemon_state_missing" });
125
141
  const bridge = new CodexBridge({ ...state.config, env: { ...codexBridgeEnv(state.config, state.daemon_id), CODEX_HOME: process.env.CODEX_HOME, DD_FLOW_CODEX_STATE_DIR: paths.dir } }); await startManagedBridge({ config: state.config, state, bridge, kind: "codex-provider", publish: snapshot => writeState(paths.state, snapshot), log: paths.log }); const sessions = new Set(state.sessions ?? []); let active = null; let server; let persist = Promise.resolve();
126
142
  scheduleDaemonProcessHeartbeat(state.config, state.resource_process);
127
143
  const save = (patch = {}) => { Object.assign(state, patch, { pid: process.pid, updated_at: new Date().toISOString(), sessions: [...sessions] }); const snapshot = structuredClone(state); persist = persist.catch(() => {}).then(() => writeState(paths.state, snapshot)); return persist; };
144
+ bridge.options.onNativeActivity = () => {
145
+ state.native_execution_revision = (state.native_execution_revision ?? 0) + 1;
146
+ if (state.active_tree !== true) void save({ active_tree: true }).catch(error => { bridge.rejectAll(error); });
147
+ };
128
148
  const dispatch = async (operation, params, operationId) => {
129
149
  const assertDispatch = captureDispatchGuard(bridge);
130
150
  if (operation === "hook.verify") {
131
151
  if (params.daemonId !== state.daemon_id) throw Object.assign(new Error("Hook daemon identity differs"), { code: "native_hook_unproven" });
132
152
  return { ...bridge.verifyNativeHook(params), daemonId: state.daemon_id, ddFlowHome: state.config.ddFlowHome, projectRoot: state.config.projectRoot };
133
153
  }
134
- if (operation === "daemon.status") return { daemon_id: state.daemon_id, pid: process.pid, cwd: state.cwd, journal: state.journal, config: state.config, sessions: [...sessions], active_operation: active, shutdown_state: state.shutdown_state };
154
+ if (operation === "daemon.status") return { daemon_id: state.daemon_id, pid: process.pid, cwd: state.cwd, journal: state.journal, config: state.config, sessions: [...sessions], active_operation: active, active_tree: state.active_tree ?? null, shutdown_state: state.shutdown_state };
135
155
  // The runner may need to retire an orphaned daemon after its caller was
136
156
  // interrupted. `daemon.stop` is the one control operation that remains
137
157
  // available while a request is marked active; callers must first prove the
138
158
  // provider Session itself is idle.
139
159
  if (operation === "daemon.stop") {
140
160
  return await completeDaemonShutdown(paths.dir, state, save, async () => {
141
- if (params.cancelTree) cancelPendingDispatch(bridge);
142
- if (active && !params.cancelTree) throw Object.assign(new Error("productive operation is running"), { code: "tree_not_settled" });
143
- await waitForSettlement({
144
- knownSessions: () => [...sessions],
145
- observe: () => observeCodexSessions(bridge, state.config, sessions, () => active),
146
- ...(params.cancelTree ? { cancel: async sessionId => { try { await cancelSessionWithBridge(bridge, { ...state.config, sessionId }); } catch (error) { if (error.code !== "turn_not_active") throw error; } } } : {})
147
- });
148
- return { clean: true };
161
+ return await prepareCodexStop({ bridge, state, sessions, active: () => active, save, stateDir: paths.dir, cancelTree: params.cancelTree });
149
162
  }, [
150
163
  ["provider", () => bridge.close()],
151
164
  ["provider_resource", () => finishDaemonProcess(state.config, state.provider_process)],
@@ -154,7 +167,7 @@ export async function serveDaemon(stateDir) {
154
167
  }
155
168
  if (["session.create", "session.prompt", "session.start", "session.fork", "session.resume"].includes(operation)) assertDaemonDispatch(paths.dir);
156
169
  if (active && !["session.inspect", "session.cancel"].includes(operation)) throw Object.assign(new Error(`${active} is in progress`), { code: "operation_busy", retryable: true });
157
- if (operation === "session.create") { active = operation; try { await save({ active_operation: active }); const result = await createSessionWithBridge(bridge, { ...state.config, ...params }); sessions.add(result.provider_session_id); return result; } finally { active = null; await save({ active_operation: null }); } }
170
+ if (operation === "session.create") { active = operation; try { await save({ active_operation: active, active_tree: true }); const result = await createSessionWithBridge(bridge, { ...state.config, ...params }); sessions.add(result.provider_session_id); return result; } finally { active = null; await save({ active_operation: null }); } }
158
171
  if (operation === "session.prompt") {
159
172
  parseCapacityOperation(operationId);
160
173
  // Admission can wait for a permit. Observe again after it returns, not
@@ -163,6 +176,7 @@ export async function serveDaemon(stateDir) {
163
176
  // journal entries and must not both pass the idle check while inspecting.
164
177
  active = operation;
165
178
  try {
179
+ await save({ active_operation: active, active_tree: true });
166
180
  const tree = await inspectCodexTree(bridge, { ...state.config, ...params });
167
181
  assertDispatch();
168
182
  if (tree.settled !== true) throw Object.assign(new Error("native Session tree changed while waiting for admission"), { code: "tree_not_settled", details: { provider_session_id: params.sessionId, unsettled_sessions: tree.unsettled_sessions } });
@@ -173,7 +187,7 @@ export async function serveDaemon(stateDir) {
173
187
  }
174
188
  finally { active = null; await save({ active_operation: null }); }
175
189
  }
176
- if (operation === "session.start") { sessions.add(params.sessionId); active = operation; try { await save({ active_operation: active }); return await startSessionWithBridge(bridge, { ...state.config, ...params, assertDispatch, operationId }); } finally { active = null; await save({ active_operation: null }); } }
190
+ if (operation === "session.start") { sessions.add(params.sessionId); active = operation; try { await save({ active_operation: active, active_tree: true }); return await startSessionWithBridge(bridge, { ...state.config, ...params, assertDispatch, operationId }); } finally { active = null; await save({ active_operation: null }); } }
177
191
  if (operation === "session.inspect" || operation === "session.resume") {
178
192
  const owned = new Set(sessions);
179
193
  for (const id of owned) for (const child of bridge.directChildren?.(id) ?? []) owned.add(child.provider_session_id);
@@ -180,6 +180,10 @@ export class CodexBridge {
180
180
 
181
181
  receive(line) {
182
182
  let message; try { message = JSON.parse(line); } catch { this.journal.write("malformed", { line }); return; }
183
+ // Read-induced idle/status notifications are not new execution. Native
184
+ // turn/item events and newly active threads invalidate an idle proof.
185
+ if (/^(turn|item)\//.test(message.method ?? "")
186
+ || message.method === "thread/status/changed" && message.params?.status?.type === "active") this.options.onNativeActivity?.();
183
187
  this.observeItemPhase(message);
184
188
  try { this.observeTopology(message); }
185
189
  catch (error) { this.topologyError = error; this.rejectAll(error); return; }
@@ -66,7 +66,7 @@ export async function serveDaemon(stateDir) {
66
66
  runtime.requireIdentity(params.sessionId); const cleanup = await runtime.closeTree(true); let inspected;
67
67
  try { inspected = await runtime.inspect(params.sessionId, { ignoreActive: true }); }
68
68
  catch (error) { inspected = { harness: "droid-cli", provider_session_id: params.sessionId, descendants: [...runtime.topology.values()], inspection_error: errorRecord(error) }; }
69
- return { ...inspected, cancelled: true, settled: cleanup.settled, cleanup };
69
+ return { ...inspected, cancelled: true, settled: inspected.settled === true && cleanup.native_settled === true, native_settled: inspected.settled === true && cleanup.native_settled === true, physical_stopped: cleanup.physical_stopped, cleanup };
70
70
  }
71
71
  if (operation === "daemon.stop") {
72
72
  stopping = true;
@@ -78,9 +78,10 @@ export async function serveDaemon(stateDir) {
78
78
  if (runtime.rootId) try { await runtime.inspect(runtime.rootId, { ignoreActive: true }); } catch (error) { cleanup.inspection_error = errorRecord(error); }
79
79
  // Native evidence failures remain visible, but cannot strand copied
80
80
  // credentials or the daemon after the owned process tree is gone.
81
- const clean = !cleanup.forced && !cleanup.topology_error && !cleanup.inspection_error;
82
- return { clean, cleanup };
81
+ const clean = cleanup.native_settled === true && !cleanup.inspection_error;
82
+ return { clean, settled: cleanup.native_settled === true, native_settled: cleanup.native_settled === true, physical_stopped: cleanup.physical_stopped, cleanup };
83
83
  }, [
84
+ ["provider", async () => { if (!runtime.closed) throw new DroidError("provider_process_stop_incomplete", "Droid provider has not closed"); }],
84
85
  ["auth", () => removeDroidAuth(state.config)],
85
86
  ["daemon_resource", () => finishDaemonProcess(state.config, state.resource_process)]
86
87
  ]);
@@ -22,7 +22,7 @@ function metadataReader(factory) { if (!metadataReaders.has(factory)) metadataRe
22
22
  import { confirmDaemonProcess, finishDaemonProcess, scheduleDaemonProcessHeartbeat, registerDaemonProcess, stopProcessGroup } from "./managed-daemon.mjs";
23
23
  import { renderAdapterPolicy, lifecycleRetryInstruction } from "./delegation-instructions.mjs";
24
24
  import { classifyProviderError, safeProviderError } from "./provider-overload-policy.mjs";
25
- import { settledNativeTree } from "./native-children.mjs";
25
+ import { settledNativeTree, nativeExecutionReceipt } from "./native-children.mjs";
26
26
 
27
27
  export const DROID_CONTRACT = "dd-droid-harness@1";
28
28
  export const DROID_PROTOCOL = "1.201.0";
@@ -256,7 +256,7 @@ export class DroidRuntime {
256
256
  }
257
257
  async observeSettings(id, settings, source, extra = {}) {
258
258
  const observed = this.profile(settings);
259
- const event = await observeModel({ journal: this.config.journal, harness: "droid-cli", sessionId: id, parentSessionId: id === this.rootId ? null : this.rootId,
259
+ const event = await observeModel({ journal: this.config.journal, harness: "droid-cli", sessionId: id, parentSessionId: id === this.rootId ? null : this.topology.get(id)?.parent_provider_session_id ?? this.rootId,
260
260
  requested: this.config, observed, source, ...extra });
261
261
  if (id === this.rootId) this.observedProfile = observed;
262
262
  this.modelEventIds ??= new Map();
@@ -267,8 +267,9 @@ export class DroidRuntime {
267
267
  return observed;
268
268
  }
269
269
  validateMetadata(id, native) {
270
- if (!native || path.resolve(native.header.cwd) !== this.config.cwd || native.header.id !== id || (id !== this.rootId && native.header.callingSessionId !== this.rootId)) throw new DroidError("session_identity_mismatch", "Native model observation is outside the owned Session tree");
271
- if (id !== this.rootId && !this.topology.has(id)) this.topology.set(id, { provider_session_id: id, parent_provider_session_id: this.rootId, tool_use_id: native.header.callingToolUseId, status: "unknown", source: "native.metadata" });
270
+ const parent = native?.header.callingSessionId;
271
+ if (!native || path.resolve(native.header.cwd) !== this.config.cwd || native.header.id !== id || (id !== this.rootId && (id === parent || parent !== this.rootId && !this.topology.has(parent) || this.topology.has(id) && this.topology.get(id).parent_provider_session_id !== parent))) throw new DroidError("session_identity_mismatch", "Native model observation is outside the owned Session tree");
272
+ if (id !== this.rootId && !this.topology.has(id)) this.topology.set(id, { provider_session_id: id, parent_provider_session_id: parent, tool_use_id: native.header.callingToolUseId, status: "unknown", source: "native.metadata" });
272
273
  }
273
274
  async observeModels() {
274
275
  if (!this.rootId || !this.loaded) return;
@@ -276,9 +277,12 @@ export class DroidRuntime {
276
277
  const info = await stat(catalog).catch(error => { if (error.code === "ENOENT") return null; throw error; });
277
278
  if (info && info.mtimeMs !== this.modelCatalogMtime) {
278
279
  const value = await readJson(catalog);
279
- for (const task of value?.invocations ?? []) if (task.parentSessionId === this.rootId && task.childSessionId) {
280
- const native = await this.metadata.read(task.childSessionId);
281
- if (native) this.validateMetadata(task.childSessionId, native);
280
+ const tasks = Array.isArray(value?.invocations) ? value.invocations : [];
281
+ for (let size = -1; size !== this.topology.size;) { size = this.topology.size;
282
+ for (const task of tasks) if (task?.childSessionId && (task.parentSessionId === this.rootId || this.topology.has(task.parentSessionId))) {
283
+ const native = await this.metadata.read(task.childSessionId);
284
+ if (native) this.validateMetadata(task.childSessionId, native);
285
+ }
282
286
  }
283
287
  this.modelCatalogMtime = info.mtimeMs;
284
288
  }
@@ -286,7 +290,7 @@ export class DroidRuntime {
286
290
  const routing = await readDroidRoutingLog(log, this.state.model_log_cursor);
287
291
  for (const transition of routing.events) {
288
292
  const native = await this.metadata.read(transition.session_id);
289
- if (!native || (transition.session_id !== this.rootId && native.header.callingSessionId !== this.rootId)) continue;
293
+ if (!native || (transition.session_id !== this.rootId && native.header.callingSessionId !== this.rootId && !this.topology.has(native.header.callingSessionId))) continue;
290
294
  this.validateMetadata(transition.session_id, native);
291
295
  await this.observeSettings(transition.session_id, { ...native.settings, modelId: transition.model, model: transition.model },
292
296
  { path: log, channel: "native_router", event_sha256: transition.event_sha256 }, { reason: transition.reason, nativeTimestamp: transition.native_timestamp, previousModel: transition.previous_model });
@@ -439,39 +443,72 @@ export class DroidRuntime {
439
443
  return { ...receipt, result: { ...outcome, status: status(outcome.reason) }, assistant_text: binding.assistant_text ?? "", turn_id: binding.turn_id, recovered_from: "native.agent_turn_outcome" };
440
444
  }
441
445
  async refreshTopology() {
442
- let catalog;
443
- try { catalog = await readJson(path.join(this.paths.factory, "task-invocations.json")); }
444
- catch (error) { if (!(error instanceof SyntaxError)) throw error; this.topologyIncomplete = true; return [...this.topology.values()]; }
446
+ let catalog, missing = false;
447
+ try { catalog = JSON.parse(await readFile(path.join(this.paths.factory, "task-invocations.json"), "utf8")); }
448
+ catch (error) { if (error.code === "ENOENT") missing = true; else { if (!(error instanceof SyntaxError)) throw error; this.topologyIncomplete = true; return [...this.topology.values()]; } }
445
449
  this.topologyIncomplete = false;
446
- const invocations = catalog?.invocations ?? [];
447
450
  const root = this.rootId ? await readDroidSession(this.paths.factory, this.rootId) : null;
448
- const rootTools = new Map((root?.records ?? []).flatMap(record => record.message?.content ?? []).filter(block => block.type === "tool_use").map(block => [block.id, block]));
451
+ const taskTools = native => new Map((native?.records ?? []).flatMap(record => record.message?.content ?? []).filter(block => block.type === "tool_use" && block.name === "Task").map(block => [block.id, block]));
452
+ const validCatalog = catalog && !Array.isArray(catalog) && Array.isArray(catalog.invocations);
453
+ if (!validCatalog) {
454
+ const noChildren = !this.topology.size && !taskTools(root).size;
455
+ const noDispatch = !this.active && !this.lastResult && !this.state.active_tree && (!root || root.complete && root.records.every(record => record.type === "session_start"));
456
+ const completeNoTask = root?.complete && terminal(root.outcomes.at(-1)?.reason) && root.tool_calls.completeness === "complete";
457
+ this.topologyIncomplete = !(missing && noChildren && (noDispatch || completeNoTask));
458
+ return [...this.topology.values()];
459
+ }
460
+ const invocations = catalog.invocations, owned = new Set([this.rootId, ...this.topology.keys()]);
461
+ // Resolve ancestry before validating records so malformed foreign entries
462
+ // do not block this tree, while malformed reachable children do.
463
+ for (let size = -1; size !== owned.size;) { size = owned.size; for (const task of invocations) if (owned.has(task?.parentSessionId) && task?.childSessionId) owned.add(task.childSessionId); }
464
+ const parents = new Map([[this.rootId, root]]);
465
+ for (const id of owned) if (id !== this.rootId) parents.set(id, await readDroidSession(this.paths.factory, id));
466
+ for (const [parentId, native] of parents) {
467
+ if (!native?.complete) this.topologyIncomplete = true;
468
+ for (const toolId of taskTools(native).keys()) if (!invocations.some(task => task?.parentSessionId === parentId && task?.parentToolUseId === toolId)) this.topologyIncomplete = true;
469
+ }
449
470
  const groups = new Map();
450
471
  for (const task of invocations) {
451
- if (task.parentSessionId !== this.rootId || !task.childSessionId || !task.parentToolUseId) continue;
472
+ if (!task || typeof task !== "object") { this.topologyIncomplete = true; continue; }
473
+ if (!owned.has(task.parentSessionId) && !owned.has(task.childSessionId) && ![...parents.values()].some(native => taskTools(native).has(task.parentToolUseId))) continue;
474
+ if (![task.parentSessionId, task.childSessionId, task.parentToolUseId, task.taskInvocationId, task.status].every(value => typeof value === "string" && value.length)) { this.topologyIncomplete = true; continue; }
452
475
  const tasks = groups.get(task.childSessionId) ?? []; tasks.push(task); groups.set(task.childSessionId, tasks);
453
476
  }
477
+ for (const id of this.topology.keys()) if (!groups.has(id)) this.topologyIncomplete = true;
454
478
  for (const [id, tasks] of groups) {
455
- const native = await readDroidSession(this.paths.factory, id);
479
+ const native = parents.get(id), parentId = tasks[0].parentSessionId, rootTools = taskTools(parents.get(parentId));
456
480
  if (!native || !native.settings) { this.topologyIncomplete = true; continue; }
457
- if (native.header.callingSessionId !== this.rootId) throw new DroidError("droid_child_identity_invalid", "Task invocation disagrees with physical child parent");
481
+ if (id === this.rootId || id === parentId || tasks.some(task => task.parentSessionId !== parentId) || native.header.callingSessionId !== parentId || path.resolve(native.header.cwd) !== this.config.cwd) throw new DroidError("droid_child_identity_invalid", "Task invocation disagrees with physical child parent/workspace");
482
+ if (new Set(tasks.map(task => task.taskInvocationId)).size !== tasks.length) this.topologyIncomplete = true;
458
483
  const creationTool = native.header.callingToolUseId;
459
484
  for (const task of tasks) {
485
+ if (!rootTools.has(task.parentToolUseId)) {
486
+ const block = (parents.get(parentId)?.records ?? []).flatMap(record => record.message?.content ?? []).find(block => block.type === "tool_use" && block.id === task.parentToolUseId);
487
+ if (block) throw new DroidError("droid_child_identity_invalid", "Task invocation is bound to a non-Task tool");
488
+ this.topologyIncomplete = true; continue;
489
+ }
460
490
  if (task.parentToolUseId === creationTool) continue;
461
491
  const tool = rootTools.get(task.parentToolUseId);
462
492
  if (tool?.name !== "Task" || tool.input?.resume !== id) throw new DroidError("droid_child_identity_invalid", "Repeated child invocation has no matching native Task resume evidence");
463
493
  }
464
494
  await this.observeSettings(id, native.settings, { path: native.transcript_path, sha256: native.source_sha256 });
465
- const prior = this.topology.get(id);
466
495
  // Task resume adds a Turn to the same physical child, not another
467
496
  // Session. Its transcript retains the original creation edge. Work
468
497
  // reassignment is checked by dd-flow's physical Session binding.
469
- const ordered = tasks.toSorted((a, b) => (a.createdAt ?? 0) - (b.createdAt ?? 0));
470
- const latest = ordered.at(-1); const running = tasks.some(task => !terminal(task.status));
471
- const closed = running && prior?.source === "adapter.tree_closed";
498
+ const order = [...rootTools.keys()];
499
+ const ordered = tasks.toSorted((a, b) => order.indexOf(a.parentToolUseId) - order.indexOf(b.parentToolUseId));
500
+ const latest = ordered.at(-1); const running = !terminal(latest.status);
472
501
  const outcome = native.outcomes.at(-1), nativeTurn = outcome && terminal(outcome.reason) && typeof outcome.turnId === "string" ? outcome.turnId : null;
473
- const evidence = !running && nativeTurn ? { terminal_turn_id: nativeTurn, native_source: "droid.agent_turn_outcome", native_turn_items: { observed: native.complete && native.tool_calls.completeness === "complete", possible_effects: native.tool_calls.total > 0, pending: native.tool_calls.pending > 0 }, ...(outcome.error && typeof outcome.error === "object" ? { provider_error: safeProviderError(outcome.error) } : {}) } : {};
474
- this.topology.set(id, { provider_session_id: id, parent_provider_session_id: this.rootId, tool_use_id: creationTool, latest_tool_use_id: latest.parentToolUseId, task_id: latest.taskInvocationId, task_invocation_ids: ordered.map(task => task.taskInvocationId), status: closed ? prior.status : running ? "running" : status(latest.status), source: closed ? prior.source : "droid.task_invocations", transcript_path: native.transcript_path, ...evidence });
502
+ const parentResult = (parents.get(parentId)?.records ?? []).flatMap(record => record.message?.content ?? []).some(block => block.type === "tool_result" && block.tool_use_id === latest.parentToolUseId);
503
+ const exactTurn = nativeTurn && (tasks.length === 1 || latest.turnId === nativeTurn);
504
+ // A resumed Task's previous terminal Turn is not its current outcome.
505
+ // The matching native Task result proves completion without attributing
506
+ // a previous error/Turn ID to this invocation.
507
+ const evidence = !running && nativeTurn && (tasks.length === 1 || parentResult || exactTurn) ? {
508
+ ...(exactTurn ? { terminal_turn_id: nativeTurn } : {}), native_source: "droid.agent_turn_outcome",
509
+ native_turn_items: { observed: native.complete && native.tool_calls.completeness === "complete", possible_effects: native.tool_calls.total > 0, pending: native.tool_calls.pending > 0 },
510
+ ...(exactTurn && outcome.error && typeof outcome.error === "object" ? { provider_error: safeProviderError(outcome.error) } : {}) } : {};
511
+ this.topology.set(id, { provider_session_id: id, parent_provider_session_id: parentId, tool_use_id: creationTool, latest_tool_use_id: latest.parentToolUseId, task_id: latest.taskInvocationId, task_invocation_ids: ordered.map(task => task.taskInvocationId), status: running ? "running" : status(latest.status), source: "droid.task_invocations", transcript_path: native.transcript_path, ...evidence });
475
512
  }
476
513
  return [...this.topology.values()];
477
514
  }
@@ -491,9 +528,10 @@ export class DroidRuntime {
491
528
  async inspect(id, { ignoreActive = false, ingestUsage = true } = {}) {
492
529
  this.requireIdentity(id); await this.captureProcesses(); const children = await this.refreshTopology(); const source = await readDroidSession(this.paths.factory, id);
493
530
  const usage = []; for (const sessionId of [id, ...children.map(child => child.provider_session_id)]) usage.push(await this.snapshotUsage(sessionId, { ingest: ingestUsage }));
494
- const settled = !this.topologyIncomplete && (!this.active || ignoreActive) && (this.closed || this.workingState === "idle") && children.every(child => terminal(child.status));
495
- const rootSettled = (!this.active || ignoreActive) && (this.closed || this.workingState === "idle") && terminal((source?.outcomes?.at(-1) ?? this.lastResult)?.reason);
496
- return { harness: "droid-cli", runtime_family: "droid", session: { harness_id: "droid-cli", session_id: id }, provider_session_id: id, adapter_session_id: id, cwd: this.config.cwd, observed_profile: source?.settings ? await this.observeSettings(id, source.settings, { path: source.transcript_path, sha256: source.source_sha256 }) : this.observedProfile, observed_runtime: { droid: this.config.version, factory_protocol: this.protocol ?? DROID_PROTOCOL, dd_harness_contract: DROID_CONTRACT }, status: settled ? "idle" : "running", settled, ...settledNativeTree(id, children, settled, rootSettled && !this.topologyIncomplete), terminal_turn_id: source?.outcomes?.at(-1)?.turnId ?? this.lastResult?.turnId, descendants: children, transcript_path: source?.transcript_path ?? null, usage, result: this.lastResult, provider_pid: this.closed ? null : this.child?.pid ?? null };
531
+ const settled = !this.topologyIncomplete && (!this.active || ignoreActive) && (this.closed || this.workingState === "idle") && source?.tool_calls.pending === 0 && children.every(child => terminal(child.status) && child.native_turn_items?.observed === true && child.native_turn_items.pending === false);
532
+ const rootSettled = source?.complete && source.tool_calls.completeness === "complete" && source.tool_calls.pending === 0 && (!this.active || ignoreActive) && (this.closed || this.workingState === "idle") && terminal((source?.outcomes?.at(-1) ?? this.lastResult)?.reason);
533
+ return nativeExecutionReceipt({ harness: "droid-cli", runtime_family: "droid", session: { harness_id: "droid-cli", session_id: id }, provider_session_id: id, adapter_session_id: id, cwd: this.config.cwd, observed_profile: source?.settings ? await this.observeSettings(id, source.settings, { path: source.transcript_path, sha256: source.source_sha256 }) : this.observedProfile, observed_runtime: { droid: this.config.version, factory_protocol: this.protocol ?? DROID_PROTOCOL, dd_harness_contract: DROID_CONTRACT }, status: settled ? "idle" : "running", settled, ...settledNativeTree(id, children, settled, rootSettled && !this.topologyIncomplete), terminal_turn_id: source?.outcomes?.at(-1)?.turnId ?? this.lastResult?.turnId, descendants: children, transcript_path: source?.transcript_path ?? null, usage, result: this.lastResult, provider_pid: this.closed ? null : this.child?.pid ?? null,
534
+ ...(this.topologyIncomplete ? { native_settlement: { confirmed: false, state: "unproven", reason: "native_task_inventory_incomplete" } } : {}) }, { daemonId: this.config.daemonId });
497
535
  }
498
536
  async observeHook(payload, eventId, deadlineAt = null) {
499
537
  try { return await this.observeHookChecked(payload, eventId, hookBudget(23_000, deadlineAt)); }
@@ -508,7 +546,8 @@ export class DroidRuntime {
508
546
  const foreign = message => new DroidError("droid_hook_identity_invalid", message, { scope: "foreign_hook", provider_session_id: id });
509
547
  if (!native || path.resolve(payload.cwd) !== this.config.cwd || await realpath(payload.transcript_path) !== await realpath(native.transcript_path)) throw foreign("Hook is outside its controlled native Session/workspace");
510
548
  const parent = native.header.callingSessionId ?? null;
511
- if (parent && parent !== this.rootId) throw foreign("Hook child belongs to another root");
549
+ if (parent && parent !== this.rootId && !this.topology.has(parent)) throw foreign("Hook child belongs to another root");
550
+ if (parent && (id === parent || this.topology.has(id) && this.topology.get(id).parent_provider_session_id !== parent)) throw foreign("Hook child ancestry changed");
512
551
  if (!parent && this.rootId && id !== this.rootId) throw foreign("Hook came from an unrelated root");
513
552
  if (path.resolve(native.header.cwd) !== this.config.cwd) throw new DroidError("droid_hook_identity_invalid", "Owned native Session workspace changed");
514
553
  if (parent && !terminal(this.topology.get(id)?.status)) this.topology.set(id, { ...(this.topology.get(id) ?? {}), provider_session_id: id, parent_provider_session_id: parent, tool_use_id: native.header.callingToolUseId, status: "running", source: "droid.session_start" });
@@ -521,7 +560,7 @@ export class DroidRuntime {
521
560
  if (!this.config.ddFlowBin || payload.tool_name !== "Execute") return {};
522
561
  // Usage is ingested by inspect/turn completion, never ahead of admission.
523
562
  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));
563
+ 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
564
  } catch (error) {
526
565
  if (event === "PreToolUse" && isProvenShellCorrection(error)) {
527
566
  const refusal = errorRecord(error);
@@ -576,8 +615,11 @@ export class DroidRuntime {
576
615
  let topologyError = null;
577
616
  try { await this.refreshTopology(); }
578
617
  catch (error) { topologyError = errorRecord(error); await this.journal("closed_tree_topology_error", { error: topologyError }); }
579
- for (const [id, child] of this.topology) if (!terminal(child.status)) this.topology.set(id, { ...child, status: "cancelled", source: "adapter.tree_closed", native_status: child.status });
580
- const cleanup = { settled: true, forced, observed_processes: this.ownedProcesses.length, ...(topologyError ? { topology_error: topologyError } : {}) };
618
+ const root = this.rootId ? await readDroidSession(this.paths.factory, this.rootId) : null;
619
+ const nativeSettled = !forced && !topologyError && !this.topologyIncomplete
620
+ && (!this.rootId || root?.complete && (terminal(root.outcomes.at(-1)?.reason) || !this.active && !this.lastResult && root.records.every(record => record.type === "session_start")) && root.tool_calls.pending === 0)
621
+ && [...this.topology.values()].every(child => terminal(child.status) && child.native_turn_items?.observed === true && child.native_turn_items.pending === false);
622
+ const cleanup = { settled: true, physical_stopped: true, native_settled: nativeSettled, forced, observed_processes: this.ownedProcesses.length, ...(topologyError ? { topology_error: topologyError } : {}) };
581
623
  await this.save({ active_tree: false, cleanup });
582
624
  return cleanup;
583
625
  }
@@ -21,7 +21,7 @@ import path from "node:path";
21
21
 
22
22
  import { AcpBridge } from "./dd-zcode.mjs";
23
23
  import { safeProviderError } from "./provider-overload-policy.mjs";
24
- import { settledNativeTree } from "./native-children.mjs";
24
+ import { settledNativeTree, nativeExecutionReceipt } from "./native-children.mjs";
25
25
  import { observeModel, readModelObservations } from "./model-observations.mjs";
26
26
  import { cancelSessionWithBridge, createSessionWithBridge, doctor, forkSessionWithBridge, inspectSessionWithBridge, promptSessionWithBridge } from "./dd-grok.mjs";
27
27
  import { prepareRuntimeOwner, cleanupFailedDaemonStart, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, scheduleDaemonProcessHeartbeat, registerDaemonProcess, startManagedBridge, stopProcessGroup } from "./managed-daemon.mjs";
@@ -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;
@@ -416,7 +465,7 @@ export class Runtime {
416
465
  if (operation === "session.create") return await this.productive(operation, async () => { const result = await createSessionWithBridge(this.bridge, { ...this.options(params), onSessionCreated: async (session) => { this.track(session); this.loadedSessionId = session.provider_session_id; await this.persist(); } }, this.initialized); this.loadedSessionId = result.provider_session_id; return result; }, null, operationId);
417
466
  if (operation === "session.prompt") { await this.requireSettled(); return await this.productive(operation, async () => { const result = await promptSessionWithBridge(this.bridge, { ...this.options(params), assertDispatch, operationId }); this.loadedSessionId = params.sessionId; return result; }, params.sessionId, operationId); }
418
467
  if (operation === "session.fork") { await this.requireSettled(); return await this.productive(operation, async () => { const result = await forkSessionWithBridge(this.bridge, this.options(params)); this.loadedSessionId = params.sessionId; return result; }); }
419
- if (operation === "session.inspect" || operation === "session.resume") { const before = new Map(this.descendants); const result = await inspectSessionWithBridge(this.bridge, this.options({ ...params, liveSession: true })); this.track(result, before); await this.persist(); const settled = !this.active && !this.treeRunning(result); const descendants = this.topology(result.provider_session_id); const terminal = this.sessions.get(result.provider_session_id)?.native_root_receipt; return { ...result, settled, descendants, ...settledNativeTree(result.provider_session_id, descendants, settled, !this.active && !this.rootRunning(result)), ...(terminal?.kind === "failed" && terminal.request_id !== undefined ? { terminal_turn_id: String(terminal.request_id) } : {}) }; }
468
+ if (operation === "session.inspect" || operation === "session.resume") { const before = new Map(this.descendants); const result = await inspectSessionWithBridge(this.bridge, this.options({ ...params, liveSession: true })); this.track(result, before); await this.persist(); const settled = !this.active && !this.treeRunning(result); const descendants = this.topology(result.provider_session_id); const terminal = this.sessions.get(result.provider_session_id)?.native_root_receipt; return nativeExecutionReceipt({ ...result, settled, descendants, ...settledNativeTree(result.provider_session_id, descendants, settled, !this.active && !this.rootRunning(result)), ...(terminal?.kind === "failed" && terminal.request_id !== undefined ? { terminal_turn_id: String(terminal.request_id) } : {}) }, { daemonId: this.state.daemon_id, generation: this.controlGeneration ?? 0 }); }
420
469
  if (operation === "session.cancel") { if (!this.sessions.has(params.sessionId)) throw new DaemonError("session_identity_mismatch", "cancellation Session is not owned by this daemon"); cancelPendingDispatch(this); const result = await cancelSessionWithBridge(this.bridge, this.options({ ...params, liveSession: true })); await this.persist({ active_tree: Boolean(this.active) || !result.settled }); return result; }
421
470
  if (operation === "session.archive") { await this.requireSettled(); return await this.productive(operation, async () => { let session = this.sessions.get(params.sessionId); if (!session?.cwd) { const result = await inspectSessionWithBridge(this.bridge, this.options({ ...params, liveSession: true })); this.track(result); session = this.sessions.get(result.provider_session_id); } return await exportSessionArchive(this.paths, session.provider_session_id, absolute(session.cwd, "Grok Build session cwd"), params.archiveDir, this.state.versions); }); }
422
471
  if (operation === "daemon.stop") {
@@ -427,7 +476,7 @@ export class Runtime {
427
476
  observe: async () => ({ sessions: await this.refreshTree(), active: Boolean(this.active), active_operation: this.active }),
428
477
  ...(params.cancelTree ? { cancel: id => cancelSessionWithBridge(this.bridge, this.options({ sessionId: id, liveSession: true })) } : {})
429
478
  });
430
- return { clean: true };
479
+ return { clean: true, settled: true, native_settled: true };
431
480
  }, [
432
481
  ["provider", () => this.bridge.close()],
433
482
  ["provider_resource", () => finishDaemonProcess(this.state.config, this.state.provider_process)],
@@ -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) {