@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
@@ -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;
@@ -10,8 +10,9 @@ import { createHash, randomBytes, randomUUID } from "node:crypto";
10
10
  import { appendFile, chmod, lstat, mkdir, open, readFile, realpath, unlink, writeFile } from "node:fs/promises";
11
11
  import { spawn } from "node:child_process";
12
12
  import net from "node:net";
13
- import { durableDaemonDispatch, completeDaemonShutdown, trackDaemonConnections, retireDaemonEndpoint, retireFailedDaemon, assertDaemonDispatch, assertDaemonRestartable } from "./daemon-operations.mjs";
13
+ import { durableDaemonDispatch, completeDaemonShutdown, commitDaemonShutdown, trackDaemonConnections, retireDaemonEndpoint, retireFailedDaemon, assertDaemonDispatch, assertDaemonRestartable } from "./daemon-operations.mjs";
14
14
  import { waitForSettlement } from "./session-settlement.mjs";
15
+ import { nativeExecutionReceipt } from "./native-children.mjs";
15
16
  import { errorRecord, isProvenShellCorrection, reportedError } from "./operation-errors.mjs";
16
17
  import { checkObservedProfile, observeModel } from "./model-observations.mjs";
17
18
  import os from "node:os";
@@ -201,15 +202,35 @@ export class Runtime {
201
202
  async journal(operation, result) { const line = `${JSON.stringify({ schema_id: "dd-opencode/daemon-event@1", event_id: randomUUID(), observed_at: new Date().toISOString(), operation, active_tree: this.state.active_tree, session_id: result?.provider_session_id ?? null })}\n`; for (const journal of new Set([this.paths.journal, this.state.config.journal].filter(Boolean))) { await mkdir(path.dirname(journal), { recursive: true, mode: 0o700 }); await appendFile(journal, line, { mode: 0o600 }); } }
202
203
  async childTree(id, seen = new Set()) {
203
204
  if (seen.has(id)) throw new DaemonError("session_identity_mismatch", "OpenCode native topology contains a cycle");
204
- seen.add(id); const direct = await this.client.children(id) ?? [], nested = [];
205
+ seen.add(id); const direct = await this.ownedChildren(id), nested = [];
205
206
  for (const child of direct) {
206
207
  if (!child.id || child.parentID !== id) throw new DaemonError("session_identity_mismatch", "OpenCode child has a foreign native parent");
207
208
  nested.push(child, ...await this.childTree(child.id, seen));
208
209
  }
209
210
  return nested;
210
211
  }
211
- async describe(id) {
212
- const [info, messages, children, statuses] = await Promise.all([this.client.inspect(id), this.client.messages(id), this.childTree(id), this.client.statuses()]);
212
+ async ownedChildren(id) {
213
+ const direct = await this.client.children(id), children = new Map();
214
+ if (!Array.isArray(direct)) throw new DaemonError("native_child_observation_unavailable", "OpenCode child inventory is unavailable");
215
+ for (const child of direct) children.set(child.id, child);
216
+ for (const record of this.sessions.values()) if (record.parent_provider_session_id === id && !children.has(record.provider_session_id)) {
217
+ let child;
218
+ try { child = await this.client.inspect(record.provider_session_id); }
219
+ catch (cause) { throw new DaemonError("native_child_observation_unavailable", "Retained OpenCode child disappeared without completion proof", false, { provider_session_id: record.provider_session_id, cause: errorPayload(cause) }); }
220
+ if (child?.id !== record.provider_session_id) throw new DaemonError("session_identity_mismatch", "OpenCode retained child read returned another Session");
221
+ children.set(child.id, child);
222
+ }
223
+ for (const child of children.values()) if (!child.id || child.parentID !== id || child.fork) throw new DaemonError("session_identity_mismatch", "OpenCode child has a foreign native parent");
224
+ return [...children.values()];
225
+ }
226
+ async revision(id) {
227
+ if (!this.client.logs) return null;
228
+ const { watermark } = await this.client.logs(id);
229
+ return JSON.stringify([id, watermark]);
230
+ }
231
+ async describe(id, refreshes = 1) {
232
+ const before = new Map([[id, await this.revision(id)]]);
233
+ const [info, messages, children] = await Promise.all([this.client.inspect(id), this.client.messages(id), this.childTree(id)]);
213
234
  const parent = info.fork ? null : info.parentID ?? null, existing = this.sessions.get(id);
214
235
  if (info.id !== id || !existing && (!parent || !this.sessions.has(parent) || info.fork)
215
236
  || existing && (existing.parent_provider_session_id ?? null) !== parent) throw new DaemonError("foreign_session", "OpenCode Session is not in the retained owned tree");
@@ -219,17 +240,31 @@ export class Runtime {
219
240
  if (child.fork || retained && retained.parent_provider_session_id !== child.parentID) throw new DaemonError("session_identity_mismatch", "OpenCode child ancestry changed");
220
241
  this.retainSession(child, record.root_provider_session_id);
221
242
  }
222
- const pending = new Map();
243
+ const pending = new Map(), histories = new Map([[id, messages]]);
244
+ for (const child of children) before.set(child.id, await this.revision(child.id));
245
+ const statuses = await this.client.statuses();
223
246
  for (const session of [id, ...children.map(child => child.id)]) {
247
+ if (session !== id) histories.set(session, await this.client.messages(session));
224
248
  const value = this.client.pending ? await this.client.pending(session) : { inbox: [], forms: [], permissions: [] };
225
249
  pending.set(session, { ...value, shells: this.client.shells ? (await this.client.shells(session)).filter(shell => shell.status === "running") : [] });
226
250
  }
227
- const busy = session => Boolean(statuses[session] && statuses[session].type !== "idle") || Object.values(pending.get(session)).some(items => items.length > 0);
251
+ const changed = new Set(), unavailable = new Set();
252
+ for (const [session, seq] of before) {
253
+ const after = await this.revision(session);
254
+ if (seq === null || after === null) { unavailable.add(session); changed.add(session); }
255
+ else if (after !== seq) changed.add(session);
256
+ }
257
+ // Re-read one raced snapshot inside this request, without a new clock or
258
+ // quiet sleep. An absent fence has no reconciliation authority.
259
+ if (changed.size && !unavailable.size && refreshes > 0) return this.describe(id, refreshes - 1);
260
+ const currentMessages = session => { const history = histories.get(session); return history.slice(history.findLastIndex(message => message.info.role === "user") + 1); };
261
+ const busy = session => changed.has(session) || Boolean(statuses[session] && statuses[session].type !== "idle") || Object.values(pending.get(session)).some(items => items.length > 0)
262
+ || currentMessages(session).some(message => (message.parts ?? []).some(part => part.type === "tool" && ["pending", "running", "streaming"].includes(part.state?.status)));
228
263
  for (const child of children) {
229
- const history = await this.client.messages(child.id);
264
+ const history = histories.get(child.id);
230
265
  const index = history.findLastIndex(message => message.info.role === "user" && message.info.sessionID === child.id);
231
266
  const terminal = index >= 0 && history.slice(index + 1).findLast(message => message.info.role === "idle" && message.info.sessionID === child.id);
232
- if (!terminal || busy(child.id)) continue;
267
+ if (!terminal || busy(child.id)) { Object.assign(child, { status: changed.has(child.id) ? "unknown" : busy(child.id) ? "running" : "idle", native_turn_items: { observed: Boolean(terminal) && !changed.has(child.id), possible_effects: true, pending: busy(child.id) } }); continue; }
233
268
  const outcome = terminal.info.outcome;
234
269
  if (!["succeeded", "failed", "interrupted"].includes(outcome)) continue;
235
270
  Object.assign(child, { status: outcome === "succeeded" ? "completed" : outcome === "failed" ? "failed" : "cancelled",
@@ -237,7 +272,7 @@ export class Runtime {
237
272
  native_turn_items: { observed: true, possible_effects: true, pending: false },
238
273
  ...(terminal.info.error ? { provider_error: safeProviderError(terminal.info.error) } : {}) });
239
274
  }
240
- const unsettledChildren = children.filter(child => busy(child.id)).map(child => child.id), status = busy(id) ? "busy" : "idle";
275
+ const unsettledChildren = children.filter(child => busy(child.id)).map(child => child.id), status = changed.has(id) ? "unknown" : busy(id) ? "busy" : "idle";
241
276
  const lastAssistant = messages.findLast(message => message.info?.role === "assistant");
242
277
  const inputIndex = messages.findLastIndex(message => message.info.role === "user");
243
278
  const idle = messages.slice(inputIndex + 1).findLast(message => message.info.role === "idle");
@@ -245,18 +280,20 @@ export class Runtime {
245
280
  await this.observeModels(id, messages, parent);
246
281
  this.journaledTools ??= new Map();
247
282
  if (this.state.config.journal) journalToolObservations(this.state.config.journal, messages.flatMap(message => (message.parts ?? []).map(part => openCodeToolObservation(part, id, message.info?.id))), this.journaledTools);
248
- return { ...record, observed_profile: { provider: lastAssistant?.info?.providerID ?? info.model?.providerID ?? null, model: lastAssistant?.info?.modelID ?? info.model?.id ?? null, reasoning: lastAssistant?.info?.variant ?? info.model?.variant ?? "default", mode: info.agent ?? null },
283
+ const receipt = { ...record, observed_profile: { provider: lastAssistant?.info?.providerID ?? info.model?.providerID ?? null, model: lastAssistant?.info?.modelID ?? info.model?.id ?? null, reasoning: lastAssistant?.info?.variant ?? info.model?.variant ?? "default", mode: info.agent ?? null },
249
284
  assistant_text: (lastAssistant?.parts ?? []).filter(part => part.type === "text").map(part => part.text ?? "").join(""), info, status,
250
285
  route_conformance: { matches: [info, ...children].every(session => session.directory && path.resolve(session.directory) === path.resolve(this.state.config.cwd)), expected: this.state.config.cwd, actual: info.directory ?? null, actual_provenance: "native_session_readback" },
251
286
  route_violations: [info, ...children].filter(session => !session.directory || path.resolve(session.directory) !== path.resolve(this.state.config.cwd)).map(session => ({ provider_session_id: session.id, expected: this.state.config.cwd, actual: session.directory ?? null, actual_provenance: "native_session_readback" })),
252
287
  settled: status === "idle" && !unsettledChildren.length, unsettled_children: unsettledChildren,
253
288
  tree_session_ids: treeIds, unsettled_sessions: treeIds.filter(busy),
254
- tree_observations: treeIds.map(session => ({ provider_session_id: session, status: busy(session) ? "running" : "idle", settled: !busy(session) })),
289
+ tree_observations: treeIds.map(session => ({ provider_session_id: session, status: changed.has(session) ? "unknown" : busy(session) ? "running" : "idle", settled: !busy(session) })),
290
+ ...(changed.size ? { native_settlement: { confirmed: false, state: "unproven", reason: "native_revision_fence_unavailable_or_changed" } } : {}),
255
291
  terminal_turn_id: idle && inputIndex >= 0 ? messages[inputIndex].info.id : null,
256
292
  terminal_status: idle?.info.outcome === "failed" ? "failed" : idle?.info.outcome === "succeeded" ? "completed" : idle?.info.outcome ?? null,
257
293
  native_turn_items: { observed: Boolean(idle), possible_effects: messages.slice(inputIndex + 1).some(message => message.parts.some(part => part.type === "tool")),
258
- pending: busy(id) || messages.slice(inputIndex + 1).some(message => message.parts.some(part => part.type === "tool" && ["running", "streaming"].includes(part.state?.status))) },
294
+ pending: busy(id) },
259
295
  children, pending: pending.get(id), usage: usageSnapshot(messages, id), history_sha256: historyDigest(messages), message_count: messages.length };
296
+ return nativeExecutionReceipt(receipt, { daemonId: this.state.daemon_id, revision: before.get(id) ?? 0 });
260
297
  }
261
298
  async observeModels(id, messages, parent = null) {
262
299
  this.modelMessages ??= new Set();
@@ -301,18 +338,24 @@ export class Runtime {
301
338
  async cancelTree(id, seen = new Set()) {
302
339
  if (!this.sessions.has(id)) throw new DaemonError("foreign_session", "cannot cancel a Session without daemon ownership");
303
340
  if (seen.has(id)) return [];
304
- const native = await this.client.inspect(id), retained = this.sessions.get(id);
305
- if (native.id !== id || (retained.parent_provider_session_id ?? null) !== (native.fork ? null : native.parentID ?? null)) throw new DaemonError("session_identity_mismatch", "OpenCode cancellation Session ancestry changed");
306
- this.retainSession(native, retained.root_provider_session_id ?? id);
341
+ const retained = this.sessions.get(id), errors = [];
342
+ let native;
343
+ try { native = await this.client.inspect(id); }
344
+ catch (error) { errors.push({ session_id: id, phase: "inspect", ...errorPayload(error) }); }
345
+ if (native) {
346
+ if (native.id !== id || (retained.parent_provider_session_id ?? null) !== (native.fork ? null : native.parentID ?? null)) throw new DaemonError("session_identity_mismatch", "OpenCode cancellation Session ancestry changed");
347
+ try { this.retainSession(native, retained.root_provider_session_id ?? id); }
348
+ catch (error) { errors.push({ session_id: id, phase: "retain", ...errorPayload(error) }); }
349
+ }
307
350
  seen.add(id);
308
- const errors = []; let children = [];
309
- try { children = await this.client.children(id); } catch (error) { errors.push({ session_id: id, phase: "children", ...errorPayload(error) }); }
351
+ let children = [];
352
+ try { children = await this.ownedChildren(id); } catch (error) { errors.push({ session_id: id, phase: "children", ...errorPayload(error) });
353
+ children = [...this.sessions.values()].filter(record => record.parent_provider_session_id === id).map(record => ({ id: record.provider_session_id, parentID: id, directory: record.cwd })); }
310
354
  for (const child of children) {
311
355
  if (child.parentID !== id || child.fork || this.sessions.has(child.id) && this.sessions.get(child.id).parent_provider_session_id !== id) {
312
356
  errors.push({ session_id: child.id, phase: "identity", code: "session_identity_mismatch" }); continue;
313
357
  }
314
- this.retainSession(child, this.sessions.get(id)?.root_provider_session_id ?? id);
315
- try { errors.push(...await this.cancelTree(child.id, seen)); }
358
+ try { this.retainSession(child, this.sessions.get(id)?.root_provider_session_id ?? id); errors.push(...await this.cancelTree(child.id, seen)); }
316
359
  catch (error) { errors.push({ session_id: child.id, phase: "cancel_child", ...errorPayload(error) }); }
317
360
  }
318
361
  try { await this.client.abort(id); } catch (error) { errors.push({ session_id: id, phase: "abort", ...errorPayload(error) }); }
@@ -467,7 +510,10 @@ export class Runtime {
467
510
  },
468
511
  ...(params.cancelTree ? { cancel: id => this.cancelTree(id) } : {})
469
512
  });
470
- return { clean: true };
513
+ // Fresh native observation, not a prior productive result, owns the idle proof.
514
+ commitDaemonShutdown(this.paths.dir);
515
+ await this.persist({ active_tree: false });
516
+ return { clean: true, settled: true, native_settled: true };
471
517
  }, [
472
518
  ["provider", () => stopProcessGroup(this.child)],
473
519
  ["provider_resource", () => finishDaemonProcess(this.state.config, this.providerProcess)],
@@ -14,6 +14,33 @@ const lost = (message, details) => new OpenCodeError("operation_observation_lost
14
14
  export const requestDigest = value => createHash("sha256").update(JSON.stringify(value)).digest("hex");
15
15
  export const nativeModel = ({ provider, model, variant }) => ({ providerID: provider, id: model, ...(variant && variant !== "default" ? { variant } : {}) });
16
16
 
17
+ /** Native finite session.log includes a control watermark, not a domain event. */
18
+ export function decodeFiniteLog(items, sessionId, after = 0) {
19
+ const fail = (reason, field, unproven = false) => { throw new OpenCodeError(unproven ? "opencode_observation_unproven" : "opencode_observation_corrupt", "OpenCode finite log has no trustworthy revision", false,
20
+ { native_outcome: "unknown", session_id: sessionId, cursor: after, reason, field }); };
21
+ if (!Number.isSafeInteger(after) || after < 0) fail("invalid_cursor", "after");
22
+ if (!Array.isArray(items)) fail("invalid_batch", "items");
23
+ const events = []; let watermark, cursor = after;
24
+ for (const item of items) {
25
+ if (watermark !== undefined) fail("frame_after_sync", "type");
26
+ const sync = item?.type === "log.synced", metadata = sync ? item : item?.durable;
27
+ if (!metadata) fail("missing_metadata", "durable");
28
+ if (metadata.aggregateID !== sessionId) fail("foreign_aggregate", sync ? "aggregateID" : "durable.aggregateID");
29
+ if (!Number.isSafeInteger(metadata.seq) || metadata.seq < 0) fail("invalid_sequence", sync ? "seq" : "durable.seq", sync && metadata.seq === undefined);
30
+ if (sync) {
31
+ if (metadata.seq < cursor) fail("watermark_behind_cursor", "seq");
32
+ watermark = metadata.seq;
33
+ } else {
34
+ if (typeof item.type !== "string" || !item.type.startsWith("session.") || !item.data || typeof item.data !== "object") fail("invalid_domain_event", "type/data");
35
+ if (item.data.sessionID !== undefined && item.data.sessionID !== sessionId) fail("foreign_session", "data.sessionID");
36
+ if (metadata.seq <= cursor) fail("non_increasing_sequence", "durable.seq");
37
+ cursor = metadata.seq; events.push(item);
38
+ }
39
+ }
40
+ if (watermark === undefined) fail("missing_sync", "log.synced", true);
41
+ return { events, watermark };
42
+ }
43
+
17
44
  /** info/parts is our normalized projection, not a second v1 native protocol. */
18
45
  export function normalizeMessage(message, sessionID) {
19
46
  if (!message?.id || !message.type) throw lost("OpenCode message has no native identity");
@@ -124,7 +151,7 @@ export class OpenCodeClient {
124
151
  try { for await (const event of api.session.log({ sessionID: id, after, follow: false }, { signal: controller.signal })) events.push(event); }
125
152
  catch (cause) { throw Object.assign(lost("OpenCode durable log observation failed", { session_id: id }), { cause }); }
126
153
  finally { clearInterval(timer); }
127
- return events;
154
+ return decodeFiniteLog(events, id, after);
128
155
  }
129
156
  async abort(id) {
130
157
  // Queued input must not become fresh work during interruption cleanup.
@@ -221,15 +248,15 @@ export class OpenCodeClient {
221
248
  for (;;) {
222
249
  try {
223
250
  await checkHookFailures?.();
224
- const events = await this.logs(id, state.cursor);
251
+ const { events, watermark } = await this.logs(id, state.cursor);
225
252
  let delivered = state.delivered === true, terminal = state.terminal;
226
253
  for (const event of events) {
227
- if (!Number.isInteger(event.durable?.seq) || event.durable.seq <= state.cursor) continue;
228
254
  if (event.type === "session.inbox.delivered" && event.data.inboxID === messageId) { delivered = true; terminal = null; }
229
255
  if (delivered && event.type === "session.execution.started") terminal = null;
230
256
  if (delivered && ["session.execution.succeeded", "session.execution.failed", "session.execution.interrupted"].includes(event.type)) terminal = { type: event.type, error: event.data.error };
231
257
  state = { ...state, cursor: event.durable.seq, delivered, terminal }; await save?.(state);
232
258
  }
259
+ if (state.cursor !== watermark) { state = { ...state, cursor: watermark }; await save?.(state); }
233
260
  let runningTools = false;
234
261
  const observe = async (session, visited = new Set()) => {
235
262
  if (visited.has(session)) throw lost("OpenCode topology contains a cycle"); visited.add(session); known.add(session);
@@ -30,7 +30,7 @@ import {
30
30
  } from "./dd-zcode.mjs";
31
31
 
32
32
  const REQUEST_SCHEMA = "dd-zcode/daemon-request@1";
33
- import { settledNativeTree } from "./native-children.mjs";
33
+ import { settledNativeTree, nativeExecutionReceipt } from "./native-children.mjs";
34
34
  const RESPONSE_SCHEMA = "dd-zcode/daemon-response@1";
35
35
  const STATE_SCHEMA = "dd-zcode/daemon-state@1";
36
36
  // ACP silence starts diagnostics; it is not a provider-failure deadline.
@@ -549,7 +549,7 @@ export class DaemonRuntime {
549
549
  for (const receipt of [previous, closed]) {
550
550
  if (receipt?.close?.closed === true && receipt.settled === true
551
551
  && receipt.topology?.complete === true && receipt.topology.root_session_id === params.sessionId
552
- && Array.isArray(receipt.topology.child_session_ids)) return receipt;
552
+ && Array.isArray(receipt.topology.child_session_ids)) return nativeExecutionReceipt(receipt, { daemonId: this.state.daemon_id, generation: this.controlGeneration ?? 0 });
553
553
  }
554
554
  const known = [...new Set([...topology.child_session_ids, ...(previous?.topology?.child_session_ids ?? [])])];
555
555
  const emptyRoot = this.neverPromptedRootProof(params.sessionId);
@@ -559,7 +559,7 @@ export class DaemonRuntime {
559
559
  this.state.settlement_observations ??= {};
560
560
  this.state.settlement_observations[params.sessionId] = observation;
561
561
  await this.persist();
562
- return observation;
562
+ return nativeExecutionReceipt(observation, { daemonId: this.state.daemon_id, generation: this.controlGeneration ?? 0 });
563
563
  }
564
564
  const adapter = params.adapterSessionId ?? params.sessionId;
565
565
  if (!this.liveAdapters.has(adapter) && this.activeProductive) {
@@ -584,8 +584,8 @@ export class DaemonRuntime {
584
584
  const childIds = [...new Set([...(this.sessions.get(result.provider_session_id)?.known_child_session_ids ?? []), ...zcodeChildSessionIds(nativeChildren)])];
585
585
  const childStates = new Map([...(nativeChildren?.ended?.items ?? nativeChildren?.completed ?? []), ...(nativeChildren?.running ?? []).map(child => ({ ...child, status: "running" }))].map(child => [child.childSessionId ?? child.sessionId, child.status]));
586
586
  const rootSettled = !this.activeProductive && failure?.provider_session_id === result.provider_session_id && (result.evidence ?? result).read?.projection?.status === "idle";
587
- return { ...result, settled, ...settledNativeTree(result.provider_session_id, childIds.map(childSessionId => ({ childSessionId, status: childStates.get(childSessionId) ?? "unknown" })), settled, rootSettled),
588
- ...(failure?.provider_session_id === result.provider_session_id ? { terminal_turn_id: String(failure.request_id) } : {}) };
587
+ return nativeExecutionReceipt({ ...result, settled, ...settledNativeTree(result.provider_session_id, childIds.map(childSessionId => ({ childSessionId, status: childStates.get(childSessionId) ?? "unknown" })), settled, rootSettled),
588
+ ...(failure?.provider_session_id === result.provider_session_id ? { terminal_turn_id: String(failure.request_id) } : {}) }, { daemonId: this.state.daemon_id, generation: this.controlGeneration ?? 0 });
589
589
  }
590
590
  if (operation === "session.cancel") {
591
591
  if (!this.sessions.has(params.sessionId)) throw new DaemonError("session_identity_mismatch", "cancellation Session is not owned by this daemon");
@@ -604,6 +604,7 @@ export class DaemonRuntime {
604
604
  return result;
605
605
  }
606
606
  if (operation === "daemon.stop") return await completeDaemonShutdown(this.paths.dir, this.state, patch => this.persist(patch), () => this.prepareStop(params.cancelTree === true), [
607
+ ["provider", () => this.bridge.close?.()],
607
608
  ["provider_resource", () => finishDaemonProcess(this.state.config, this.state.provider_process)],
608
609
  ["daemon_resource", () => finishDaemonProcess(this.state.config, this.state.resource_process)]
609
610
  ]);
@@ -691,7 +692,7 @@ export class DaemonRuntime {
691
692
  try { if (!bridgeClosed && typeof this.bridge.close === "function") await this.bridge.close(); }
692
693
  catch (error) { throw new DaemonError(error.code ?? "provider_process_stop_incomplete", error.message ?? String(error), false, error.details); }
693
694
  await this.persist({ active_tree: false, shutdown_state: "stopping", active_operation: null, active_request: null, physical_settlement: { confirmed_at: new Date().toISOString(), bridge_pid: this.bridge.child?.pid ?? null } });
694
- return { daemon_id: this.state.daemon_id, stopped: true, clean: true, physical_settlement: this.state.physical_settlement, _shutdown: true };
695
+ return { daemon_id: this.state.daemon_id, stopped: true, clean: true, settled: true, native_settled: true, physical_settlement: this.state.physical_settlement, _shutdown: true };
695
696
  }
696
697
 
697
698
  }
@@ -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
  }
@@ -60,7 +60,7 @@ export function delegationContract(harness) {
60
60
  return `dd-flow/delegation/${key}@${["codex", "opencode"].includes(key) ? 2 : 1}`;
61
61
  }
62
62
 
63
- export const lifecycleCommandCompletionInstruction = "Run runtime-issued lifecycle CLI calls in the foreground, never with a native background or detached-shell option. A shell tool may yield before the lifecycle process exits. Retain and poll its exact process/session handle to completion, including inside a tool wrapper; empty interim output is not a CLI result. If a wrapper returns 'Script running with cell ID …', use that wrapper's wait tool to finish the same cell before expecting an inner shell session handle; do not execute the command again. Read final stdout or response_file before deciding continuation, and never reissue the command while it is pending.";
63
+ export const lifecycleCommandCompletionInstruction = "Run runtime-issued lifecycle CLI calls in the foreground, never with a native background or detached-shell option. A shell tool may yield before the lifecycle process exits. Retain and poll its exact process/session handle to completion, including inside a tool wrapper; empty interim output is not a CLI result. If a wrapper returns 'Script running with cell ID …', use that wrapper's wait tool to finish the same cell before expecting an inner shell session handle; do not execute the command again. Read final stdout or response_file before deciding continuation, and never reissue the command while it is pending. When the result provides prompt_delivery, read its complete plain Markdown file at prompt_delivery.path before working; it contains the current instructions, unlike the historical prompt_path. If delivery publication is pending, stop for reconciliation without repeating the accepted command.";
64
64
 
65
65
  export const lifecycleRetryInstruction = `${lifecycleCommandCompletionInstruction} A failure is not permission to replay a lifecycle command. For proven effect=no_effect with recoverable=true and retry_command, correct the input and execute that exact issued successor. A check failure can retain receipts while leaving Work running: correct the evidenced cause first, then use the CLI's exact retry_command for an intentional fresh check attempt; never replay a check merely because Work is unsettled. If a successful CODE finish returns outcome=verification_required, inspect the retained receipts, update its verification file, and use only next.finish_command: the Stage is still running and the previous invocation ID is consumed. If continuation.kind=repair_required, or a successful result has outcome=repair_required and a repair Work ID, end this Turn for the registered repair child; use any retry_command only after that repair settles and verification is refreshed. Unknown effects require reconciliation, never replay. A committed effect without an explicit continuation is not retryable. A native shell launch refusal before the CLI starts permits correcting only that shell-form error and retrying the same exact command. Otherwise report the primary error and stop; never invent IDs, call hooks directly, or run status as a substitute.`;
66
66
 
@@ -6,22 +6,23 @@ import { writeJsonAtomic } from "./runner-events.mjs";
6
6
  import { reportedError } from "./operation-errors.mjs";
7
7
  import { ObservationClock } from "./observation-clock.mjs";
8
8
  import { runtimeProcess } from "./managed-daemon.mjs";
9
+ import { nativeShutdownConfirmed } from "./native-children.mjs";
9
10
 
10
- function cleanShutdownConfirmed(state) {
11
+ async function cleanShutdownConfirmed(root, state) {
11
12
  const proof = state.shutdown;
13
+ let fence;
14
+ try { fence = JSON.parse(await readFile(path.join(root, 'operation-fence.json'), 'utf8')); }
15
+ catch (error) { if (error.code !== 'ENOENT') throw error; }
12
16
  return state.shutdown_state === "clean" && state.active_tree === false
13
- && proof?.schema_id === "dd-flow/daemon-shutdown@1" && proof.daemon_id === state.daemon_id
14
- && proof.result?.clean === true && Array.isArray(proof.required_phases)
15
- && proof.required_phases.length >= 3 && proof.required_phases.includes("tree")
16
- && proof.required_phases.includes("daemon_resource")
17
- && proof.required_phases.every(phase => typeof phase === "string" && proof.phases?.[phase] === true);
17
+ && typeof state.daemon_id === 'string' && fence?.daemon_id === state.daemon_id && Number.isSafeInteger(fence.generation) && fence.generation >= 0
18
+ && nativeShutdownConfirmed(proof, { daemonId: state.daemon_id, generation: fence.generation });
18
19
  }
19
20
 
20
21
  /** Explicit same-Session restart retains native stores and the old daemon
21
22
  * receipt; it does not turn a terminal directory into a new execution. */
22
23
  export async function authorizeRetainedDaemonResume(root, previous, sessionId, config) {
23
24
  const sessions = [...(previous.sessions ?? []), ...(previous.root_session_id ? [previous.root_session_id] : [])];
24
- if (!cleanShutdownConfirmed(previous) || typeof sessionId !== "string" || !sessions.some(session => (typeof session === "string" ? session : session.provider_session_id) === sessionId)) throw Object.assign(new Error("terminal daemon restart requires a retained, settled Session identity and complete shutdown proof"), { code: "daemon_state_terminal" });
25
+ if (!await cleanShutdownConfirmed(root, previous) || typeof sessionId !== "string" || !sessions.some(session => (typeof session === "string" ? session : session.provider_session_id) === sessionId)) throw Object.assign(new Error("terminal daemon restart requires a retained, settled Session identity and complete shutdown proof"), { code: "daemon_state_terminal" });
25
26
  for (const key of ["cwd", "projectRoot", "ddFlowHome", "ddFlowBin", "provider", "model", "reasoning", "mode", "variant", "agent", "bin", "noFlow"]) {
26
27
  if ((previous.config?.[key] ?? null) !== (config[key] ?? null)) throw Object.assign(new Error(`retained daemon configuration changed: ${key}`), { code: "daemon_config_mismatch" });
27
28
  }
@@ -53,7 +54,7 @@ export async function recoverySettlement(driversRoot) {
53
54
  }
54
55
  else if (entry.name === "daemon.json") {
55
56
  const state = JSON.parse(await readFile(file, "utf8"));
56
- if (!cleanShutdownConfirmed(state)) throw Object.assign(new Error("provider tree has no complete clean shutdown receipt"), { code: "recovery_settlement_unconfirmed" });
57
+ if (!await cleanShutdownConfirmed(directory, state)) throw Object.assign(new Error("provider tree has no complete clean shutdown receipt"), { code: "recovery_settlement_unconfirmed" });
57
58
  await assertDaemonReplaceable(directory);
58
59
  await reconcileDriverReplies(directory);
59
60
  if (state.config?.resourceHome && state.resource_process?.id) {
@@ -0,0 +1,2 @@
1
+ export function shortEntityReference(id: string): string;
2
+ export function entityReferenceVariants(id: string): string[];
@@ -0,0 +1,9 @@
1
+ /** Stable model-facing reference for persisted project, RUN and Work IDs. */
2
+ export function shortEntityReference(id) {
3
+ return /^(?:PRJ|RUN|WRK)-\d{3,}(?:-|$)/.exec(id)?.[0]?.replace(/-$/, "") ?? id;
4
+ }
5
+
6
+ /** Accepted spellings of one already-resolved entity. Wrong full slugs stay distinct. */
7
+ export function entityReferenceVariants(id) {
8
+ return [...new Set([id, shortEntityReference(id)])];
9
+ }
@@ -10,7 +10,7 @@ export function heartbeatDaemonProcess(config: RuntimeConfig, record: ProcessBin
10
10
  export function finishDaemonProcess(config: RuntimeConfig, record: ProcessBinding | null | undefined, state?: string, reason?: string): Promise<void>;
11
11
  export function cleanupFailedDaemonStart(config: RuntimeConfig, record: ProcessBinding | null, child: ChildProcess | null, original: Error & { details?: Record<string, unknown> }, readState: () => Promise<Record<string, unknown>>, daemonId: string): Promise<void>;
12
12
  export function completeBudgetOperation(stateDir: string, request: { id: string; operation: string; params?: { sessionId?: string } }, result: unknown, observe?: () => Promise<Record<string, unknown>>): Promise<Record<string, unknown>>;
13
- export function stopProcessGroup(child: ChildProcess | null | undefined, graceMs?: number, options?: { detached?: boolean; settlementMs?: number }): Promise<void>;
13
+ export function stopProcessGroup(child: ChildProcess | { pid: number; started: string } | null | undefined, graceMs?: number, options?: { detached?: boolean; settlementMs?: number; started?: string }): Promise<void>;
14
14
  export function assertStartupChild(child: Pick<ChildProcess, "pid" | "exitCode" | "signalCode">, expected: Record<string, unknown>, observed: Record<string, unknown> | null, log: string): void;
15
15
  export function retainStartupFailure(input: { state: Record<string, unknown>; publish: (state: Record<string, unknown>) => Promise<unknown>; log: string; error: Error & { code?: string; details?: Record<string, unknown> } }): Promise<void>;
16
16
  export function startManagedBridge(input: { config: RuntimeConfig; state: Record<string, unknown>; bridge: { child: ChildProcess | null; options: { onSpawn?: (child: ChildProcess) => Promise<void> }; start: () => Promise<unknown> }; kind: string; publish: (state: Record<string, unknown>) => Promise<unknown>; log?: string }): Promise<unknown>;