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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (227) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/build-info.json +8 -8
  3. package/dist/cli/command-inputs.js +33 -3
  4. package/dist/cli/command-metadata.js +73 -2
  5. package/dist/cli/delivery.js +108 -0
  6. package/dist/cli/diagnostic-invocation.js +23 -7
  7. package/dist/cli/diagnostic.js +14 -3
  8. package/dist/cli/help.js +117 -1
  9. package/dist/cli/hook-ingress.js +36 -49
  10. package/dist/cli/interactive-invocation.js +33 -0
  11. package/dist/cli/interactive.js +114 -0
  12. package/dist/cli/memory-fix-invocation.js +52 -0
  13. package/dist/cli/memory-fix.js +100 -0
  14. package/dist/cli/memory-flow-admission.js +7 -3
  15. package/dist/cli/memory-init.js +5 -4
  16. package/dist/cli/native-hook-ingress.js +5 -1
  17. package/dist/cli/native-hook-preparation.js +31 -4
  18. package/dist/cli/run-cli.js +273 -19
  19. package/dist/cli.js +17 -3
  20. package/dist/domain/delivery-flows.js +8 -0
  21. package/dist/domain/distill-flow.js +20 -0
  22. package/dist/domain/interactive-flow.js +1 -0
  23. package/dist/domain/memory-fix-flow.js +2 -0
  24. package/dist/domain/review-fix-flow.js +2 -0
  25. package/dist/domain/source-snapshot-flows.js +4 -0
  26. package/dist/harness-runtime/bin/dd-agy.mjs +18 -3
  27. package/dist/harness-runtime/bin/dd-grok.mjs +15 -5
  28. package/dist/harness-runtime/bin/dd-zcode.mjs +15 -7
  29. package/dist/harness-runtime/lib/agy-error-boundary.mjs +18 -10
  30. package/dist/harness-runtime/lib/agy-native-inspection.mjs +12 -3
  31. package/dist/harness-runtime/lib/agy-native-inventory.mjs +19 -0
  32. package/dist/harness-runtime/lib/agy-terminal-envelope.mjs +13 -6
  33. package/dist/harness-runtime/lib/agy-transcript-stream.mjs +54 -0
  34. package/dist/harness-runtime/lib/daemon-observation.d.mts +1 -0
  35. package/dist/harness-runtime/lib/daemon-observation.mjs +12 -4
  36. package/dist/harness-runtime/lib/daemon-operations.mjs +20 -5
  37. package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +1 -0
  38. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +371 -114
  39. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +43 -3
  40. package/dist/harness-runtime/lib/dd-codex.mjs +36 -4
  41. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +25 -11
  42. package/dist/harness-runtime/lib/dd-droid.mjs +124 -11
  43. package/dist/harness-runtime/lib/dd-grok-daemon.d.mts +1 -0
  44. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +137 -25
  45. package/dist/harness-runtime/lib/dd-grok.mjs +4 -2
  46. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +170 -25
  47. package/dist/harness-runtime/lib/dd-opencode.mjs +40 -19
  48. package/dist/harness-runtime/lib/dd-zcode-daemon.d.mts +1 -0
  49. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +115 -22
  50. package/dist/harness-runtime/lib/dd-zcode.mjs +75 -36
  51. package/dist/harness-runtime/lib/delegation-instructions.d.mts +1 -0
  52. package/dist/harness-runtime/lib/delegation-instructions.mjs +10 -3
  53. package/dist/harness-runtime/lib/delivery-command.mjs +84 -0
  54. package/dist/harness-runtime/lib/driver-recovery.mjs +0 -19
  55. package/dist/harness-runtime/lib/droid-observation.mjs +9 -5
  56. package/dist/harness-runtime/lib/lifecycle-contract.d.mts +1 -0
  57. package/dist/harness-runtime/lib/lifecycle-contract.mjs +2 -1
  58. package/dist/harness-runtime/lib/native-hook-command.d.mts +3 -2
  59. package/dist/harness-runtime/lib/native-hook-command.mjs +63 -8
  60. package/dist/harness-runtime/lib/native-streams.mjs +3 -10
  61. package/dist/harness-runtime/lib/opencode-plugin.mjs +36 -9
  62. package/dist/harness-runtime/lib/operation-errors.mjs +13 -0
  63. package/dist/harness-runtime/lib/operation-progress.mjs +6 -2
  64. package/dist/harness-runtime/lib/process-json.mjs +4 -5
  65. package/dist/harness-runtime/lib/tool-observations.mjs +20 -10
  66. package/dist/harness-runtime/lib/zcode-native-hook-ownership.mjs +109 -0
  67. package/dist/schemas/code-work-result.schema.json +2 -1
  68. package/dist/schemas/delivery-stage-result.schema.json +286 -0
  69. package/dist/schemas/distill-stage-result.schema.json +667 -0
  70. package/dist/schemas/distill-work-result.schema.json +428 -0
  71. package/dist/schemas/execution-bundle.schema.json +6 -2
  72. package/dist/schemas/interactive-stage-result.schema.json +47 -0
  73. package/dist/schemas/interactive-work-result.schema.json +63 -0
  74. package/dist/schemas/mb-fix-stage-result.schema.json +93 -0
  75. package/dist/schemas/project-flow-pack-manifest.schema.json +2 -2
  76. package/dist/schemas/review-fix-stage-result.schema.json +210 -0
  77. package/dist/schemas/run-control-receipt.schema.json +1 -0
  78. package/dist/services/assigned-lifecycle.js +382 -0
  79. package/dist/services/check-runner-evidence.js +77 -10
  80. package/dist/services/check-runner-proof.js +69 -1
  81. package/dist/services/cli-operation-classifier.js +6 -1
  82. package/dist/services/code-checks.js +56 -23
  83. package/dist/services/codex-hook-delivery.js +11 -2
  84. package/dist/services/controller-execution.js +4 -1
  85. package/dist/services/controller-fanout.js +9 -4
  86. package/dist/services/controller-memory-worker.js +6 -4
  87. package/dist/services/controller-result-authority.js +122 -0
  88. package/dist/services/dashboard.js +4 -2
  89. package/dist/services/delivery-access.js +123 -0
  90. package/dist/services/delivery-approval.js +68 -0
  91. package/dist/services/delivery-artifact.js +86 -0
  92. package/dist/services/delivery-checks.js +178 -0
  93. package/dist/services/delivery-execution.js +181 -0
  94. package/dist/services/delivery-intents.js +212 -0
  95. package/dist/services/delivery-operation.js +81 -0
  96. package/dist/services/delivery-policy.js +91 -0
  97. package/dist/services/delivery-procedures.js +43 -0
  98. package/dist/services/delivery-request.js +99 -0
  99. package/dist/services/delivery-result.js +144 -0
  100. package/dist/services/delivery-stage-commands.js +26 -0
  101. package/dist/services/delivery-stage-entry.js +56 -0
  102. package/dist/services/delivery-stage-handlers.js +135 -0
  103. package/dist/services/delivery-stage-results.js +94 -0
  104. package/dist/services/descriptor-coordinator-session.js +3 -1
  105. package/dist/services/descriptor-run-start.js +24 -4
  106. package/dist/services/descriptor-stage-coordinator.js +78 -10
  107. package/dist/services/descriptor-stage-settlement.js +59 -14
  108. package/dist/services/diagnostic-repair-intake.js +123 -0
  109. package/dist/services/diagnostic-repair-source-intake.js +102 -0
  110. package/dist/services/diagnostic-report.js +30 -0
  111. package/dist/services/diagnostic-run-bundle.js +48 -9
  112. package/dist/services/diagnostic-snapshot.js +2 -2
  113. package/dist/services/diagnostic-stage-handlers.js +3 -2
  114. package/dist/services/diagnostic-stage-results.js +26 -0
  115. package/dist/services/diagnostic-start.js +56 -6
  116. package/dist/services/diagnostic-work-collection.js +3 -3
  117. package/dist/services/distill-document-publication.js +280 -0
  118. package/dist/services/distill-promotion.js +197 -0
  119. package/dist/services/distill-publication-custody.js +162 -0
  120. package/dist/services/distill-stage-handlers.js +249 -0
  121. package/dist/services/distill-stage-results.js +105 -0
  122. package/dist/services/distill-user-inputs.js +4 -0
  123. package/dist/services/distill-work-batch.js +42 -0
  124. package/dist/services/distill-work-collection.js +74 -0
  125. package/dist/services/distill-work-registration.js +93 -0
  126. package/dist/services/distill-work-results.js +98 -0
  127. package/dist/services/eval-snapshots.js +17 -7
  128. package/dist/services/external-work-launch.js +8 -2
  129. package/dist/services/flow-catalog.js +24 -0
  130. package/dist/services/frozen-engine.js +19 -2
  131. package/dist/services/generic-execution.js +4 -3
  132. package/dist/services/harness-adapter.js +2 -14
  133. package/dist/services/hook-memory-routing.js +1 -1
  134. package/dist/services/hooks.js +31 -6
  135. package/dist/services/hybrid-contracts.js +6 -1
  136. package/dist/services/installed-flow-source.js +37 -2
  137. package/dist/services/interactive-attached-session.js +215 -0
  138. package/dist/services/interactive-baseline-custody.js +54 -0
  139. package/dist/services/interactive-before-versions.js +148 -0
  140. package/dist/services/interactive-bundle-data.js +85 -0
  141. package/dist/services/interactive-checks.js +179 -0
  142. package/dist/services/interactive-execution.js +33 -0
  143. package/dist/services/interactive-finish-integration.js +236 -0
  144. package/dist/services/interactive-git-baseline.js +230 -0
  145. package/dist/services/interactive-git-dispatch.js +278 -0
  146. package/dist/services/interactive-input-delivery.js +82 -0
  147. package/dist/services/interactive-mutation-scope.js +231 -0
  148. package/dist/services/interactive-request.js +287 -0
  149. package/dist/services/interactive-run-bundle.js +109 -0
  150. package/dist/services/interactive-stage-entry.js +41 -0
  151. package/dist/services/interactive-stage-handlers.js +289 -0
  152. package/dist/services/interactive-stage-observation.js +136 -0
  153. package/dist/services/interactive-stage-results.js +111 -0
  154. package/dist/services/interactive-user-inputs.js +142 -0
  155. package/dist/services/interactive-work-batch.js +43 -0
  156. package/dist/services/interactive-work-collection.js +119 -0
  157. package/dist/services/interactive-work-completion.js +180 -0
  158. package/dist/services/interactive-work-context.js +249 -0
  159. package/dist/services/interactive-work-history.js +112 -0
  160. package/dist/services/interactive-work-registration.js +124 -0
  161. package/dist/services/interactive-work-results.js +115 -0
  162. package/dist/services/interactive-work-start.js +155 -0
  163. package/dist/services/lifecycle-invocations.js +214 -24
  164. package/dist/services/managed-daemon-binding.js +52 -1
  165. package/dist/services/memory-compose-handler.js +2 -22
  166. package/dist/services/memory-finish-integration.js +58 -17
  167. package/dist/services/memory-fix-finish-integration.js +129 -0
  168. package/dist/services/memory-fix-git-dispatch.js +139 -0
  169. package/dist/services/memory-fix-intake-custody.js +11 -0
  170. package/dist/services/memory-fix-intake-settlement.js +127 -0
  171. package/dist/services/memory-fix-publication.js +196 -0
  172. package/dist/services/memory-fix-repair-admission.js +116 -0
  173. package/dist/services/memory-fix-request.js +141 -0
  174. package/dist/services/memory-fix-run-bundle.js +83 -0
  175. package/dist/services/memory-fix-scope.js +78 -0
  176. package/dist/services/memory-fix-sources.js +85 -0
  177. package/dist/services/memory-fix-stage-entry.js +24 -0
  178. package/dist/services/memory-fix-stage-handlers.js +282 -0
  179. package/dist/services/memory-fix-stage-results.js +106 -0
  180. package/dist/services/memory-fix-workspace.js +127 -0
  181. package/dist/services/memory-flow-user-inputs.js +88 -0
  182. package/dist/services/memory-init-contexts.js +55 -5
  183. package/dist/services/memory-init-controller-start.js +20 -4
  184. package/dist/services/memory-layer.js +7 -3
  185. package/dist/services/memory-native-review-copy.js +27 -3
  186. package/dist/services/memory-repair-document-delta.js +1 -0
  187. package/dist/services/project-flow-source.js +195 -0
  188. package/dist/services/project-git-integration.js +92 -0
  189. package/dist/services/repair-intents.js +2 -4
  190. package/dist/services/retained-native-hook.js +245 -0
  191. package/dist/services/review-fix-handoff.js +82 -0
  192. package/dist/services/review-fix-intake-custody.js +17 -0
  193. package/dist/services/review-fix-stage-entry.js +43 -0
  194. package/dist/services/review-fix-stage-handlers.js +176 -0
  195. package/dist/services/review-fix-stage-results.js +86 -0
  196. package/dist/services/run-control-worker.js +42 -1
  197. package/dist/services/run-control.js +40 -5
  198. package/dist/services/run-controller-adapter.js +37 -3
  199. package/dist/services/run-controller-process.js +12 -4
  200. package/dist/services/run-controller-recovery.js +46 -6
  201. package/dist/services/run-controller-state.js +3 -2
  202. package/dist/services/run-controller.js +209 -43
  203. package/dist/services/run-recovery.js +31 -2
  204. package/dist/services/run-timeline.js +59 -0
  205. package/dist/services/runs.js +18 -31
  206. package/dist/services/sessions.js +2 -1
  207. package/dist/services/stage-handlers.js +55 -3
  208. package/dist/services/stage-lifecycle.js +4 -3
  209. package/dist/services/stage-pause.js +26 -9
  210. package/dist/services/stage-report-renderer.js +2 -6
  211. package/dist/services/technical-notes.js +31 -0
  212. package/dist/services/verification-matrix.js +1 -1
  213. package/dist/services/version-status.js +1 -1
  214. package/dist/services/vnext-code-review.js +74 -5
  215. package/dist/services/vnext-code.js +9 -32
  216. package/dist/services/vnext-fanout.js +53 -8
  217. package/dist/services/vnext-merge.js +10 -5
  218. package/dist/services/vnext-plan-review.js +1 -1
  219. package/dist/services/vnext-plan.js +1 -1
  220. package/dist/services/vnext-protocolize.js +1 -1
  221. package/dist/services/vnext-specify.js +10 -5
  222. package/dist/services/work-registry.js +252 -66
  223. package/dist/services/worktrees.js +7 -4
  224. package/dist/storage/database.js +2 -1
  225. package/dist/verification-components.json +82 -0
  226. package/package.json +4 -3
  227. package/tools/verification-jobs.mjs +342 -0
@@ -1,6 +1,7 @@
1
1
  import { managedFlowEnv } from './managed-flow-env.mjs';
2
2
  import { SETUP_STARTUP_MS, startupObserver, observationDetails } from './setup-progress.mjs';
3
3
  import { callDaemonObserved } from './daemon-observation.mjs';
4
+ import { operationProgressReporter } from './operation-progress.mjs';
4
5
  import { admitNativeRequestBuffer } from "./native-streams.mjs";
5
6
  import { prepareDaemonRequest } from "./native-request-proof.mjs";
6
7
  import { captureDispatchGuard, cancelPendingDispatch } from "./dispatch-fence.mjs";
@@ -13,15 +14,17 @@ import net from "node:net";
13
14
  import { durableDaemonDispatch, completeDaemonShutdown, commitDaemonShutdown, trackDaemonConnections, retireDaemonEndpoint, retireFailedDaemon, assertDaemonDispatch, assertDaemonRestartable } from "./daemon-operations.mjs";
14
15
  import { waitForSettlement } from "./session-settlement.mjs";
15
16
  import { nativeExecutionReceipt } from "./native-children.mjs";
16
- import { errorRecord, isProvenShellCorrection, reportedError } from "./operation-errors.mjs";
17
- import { checkObservedProfile, observeModel } from "./model-observations.mjs";
17
+ import { diagnosticJson, errorRecord, isNativeHookObservationLoss, isProvenShellCorrection, reportedError } from "./operation-errors.mjs";
18
+ import { checkObservedProfile, observeModel, observedFields, readModelObservations } from "./model-observations.mjs";
18
19
  import os from "node:os";
19
20
  import path from "node:path";
20
21
  import { parse as parseJsonc } from "jsonc-parser";
21
22
  import { writeJsonAtomic } from "./runner-events.mjs";
22
23
  import { OpenCodeClient, OpenCodeError, historyDigest, usageSnapshot, nativeModel } from "./dd-opencode.mjs";
23
24
  import { safeProviderError } from "./provider-overload-policy.mjs";
24
- import { acceptNativeHookRequest, hookBudget, readNativeHookFailures } from "./native-hook-command.mjs";
25
+ import { acceptNativeHookRequest, hookBudget, readNativeHookFailures, writeNativeHookFailure, retainNativeWorkStart } from "./native-hook-command.mjs";
26
+ import { isDeepStrictEqual } from "node:util";
27
+ import { parseLifecycleCommand } from "./lifecycle-command.mjs";
25
28
  import { assertStartupChild, retainStartupFailure, prepareRuntimeOwner, cleanupFailedStart, cleanupFailedDaemonStart, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, scheduleDaemonProcessHeartbeat, registerDaemonProcess, stopProcessGroup } from "./managed-daemon.mjs";
26
29
 
27
30
  const REQUEST_SCHEMA = "dd-opencode/daemon-request@1", RESPONSE_SCHEMA = "dd-opencode/daemon-response@1", STATE_SCHEMA = "dd-opencode/daemon-state@2", ARCHIVE_SCHEMA = "dd-opencode/session-archive@2";
@@ -68,7 +71,7 @@ const errorPayload = errorRecord;
68
71
 
69
72
  async function writePlugin(paths, config) {
70
73
  const directory = path.join(paths.config, "opencode", "plugins"); await mkdir(directory, { recursive: true, mode: 0o700 });
71
- const source = `import { makeOpenCodePlugin } from ${JSON.stringify(new URL("./opencode-plugin.mjs", import.meta.url).href)};\nexport default makeOpenCodePlugin(${JSON.stringify({ stateDir: paths.dir, daemonId: config.daemonId, version: config.version, ddFlowBin: config.ddFlowBin, nodeBin: process.execPath, ddFlowHome: config.ddFlowHome, projectRoot: config.projectRoot, resourceHome: config.resourceHome })});\n`;
74
+ const source = `import { makeOpenCodePlugin } from ${JSON.stringify(new URL("./opencode-plugin.mjs", import.meta.url).href)};\nexport default makeOpenCodePlugin(${JSON.stringify({ stateDir: paths.dir, daemonId: config.daemonId, version: config.version, ddFlowBin: config.ddFlowBin, nodeBin: process.execPath, ddFlowHome: config.ddFlowHome, projectRoot: config.projectRoot, resourceHome: config.resourceHome, nativeWorkStartAdmission: true })});\n`;
72
75
  const checksum = createHash("sha256").update(source).digest("hex"); await writeFile(path.join(directory, "dd-flow.js"), source, { mode: 0o600 }); return checksum;
73
76
  }
74
77
  async function prepare(paths, config, authSource) {
@@ -188,7 +191,9 @@ export class Runtime {
188
191
  throw new DaemonError("session_identity_mismatch", "OpenCode retained Session identity is invalid or duplicated");
189
192
  this.sessions.set(record.provider_session_id, record);
190
193
  }
191
- this.active = null; this.persisting = Promise.resolve(); }
194
+ // ponytail: retained participating hooks live until daemon retirement;
195
+ // add durable per-tool retirement only if long-lived Sessions need it.
196
+ this.active = null; this.persisting = Promise.resolve(); this.retainedHookRequests = new Map(); }
192
197
  persist(patch = {}) { Object.assign(this.state, patch, { pid: process.pid, sessions: [...this.sessions.values()] }); const snapshot = structuredClone(this.state); this.persisting = this.persisting.catch(() => {}).then(() => writeJson(this.paths.state, snapshot)); return this.persisting; }
193
198
  retainSession(info, root) {
194
199
  const id = info.id, parent = info.fork ? null : info.parentID ?? null, retained = this.sessions.get(id);
@@ -202,13 +207,23 @@ export class Runtime {
202
207
  const record = { provider_session_id: id, adapter_session_id: id, parent_provider_session_id: parent, root_provider_session_id: root, cwd: info.directory };
203
208
  this.sessions.set(id, record); return record;
204
209
  }
205
- 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 }); } }
206
- async childTree(id, seen = new Set()) {
210
+ registerObservedChild(info, root) {
211
+ const owner = this.sessions.get(root);
212
+ if (!owner || owner.parent_provider_session_id || owner.root_provider_session_id !== root
213
+ || info.id === root || info.parentID !== root || info.fork)
214
+ throw new DaemonError("session_identity_mismatch", "OpenCode child is not a direct member of the owned root");
215
+ if (typeof info.directory !== "string" || !path.isAbsolute(info.directory))
216
+ throw new OpenCodeError("opencode_unavailable", "OpenCode child directory binding is not yet observable");
217
+ return this.retainSession(info, root);
218
+ }
219
+ 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, ...(result?.native_hook_diagnostic ? { native_hook_diagnostic: JSON.parse(diagnosticJson(result.native_hook_diagnostic)) } : {}) })}\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 }); } }
220
+ async childTree(id, seen = new Set(), onProgress = async () => {}) {
207
221
  if (seen.has(id)) throw new DaemonError("session_identity_mismatch", "OpenCode native topology contains a cycle");
208
222
  seen.add(id); const direct = await this.ownedChildren(id), nested = [];
223
+ await onProgress(`children:${id}:${createHash('sha256').update(JSON.stringify(direct)).digest('hex')}`);
209
224
  for (const child of direct) {
210
225
  if (!child.id || child.parentID !== id) throw new DaemonError("session_identity_mismatch", "OpenCode child has a foreign native parent");
211
- nested.push(child, ...await this.childTree(child.id, seen));
226
+ nested.push(child, ...await this.childTree(child.id, seen, onProgress));
212
227
  }
213
228
  return nested;
214
229
  }
@@ -299,17 +314,52 @@ export class Runtime {
299
314
  return nativeExecutionReceipt(receipt, { daemonId: this.state.daemon_id, revision: before.get(id) ?? 0 });
300
315
  }
301
316
  async observeModels(id, messages, parent = null) {
302
- this.modelMessages ??= new Set();
317
+ const journal = this.state.config.journal ?? this.paths.journal;
318
+ if (!this.modelMessages) {
319
+ this.modelMessages = new Map();
320
+ for (const event of journal ? await readModelObservations(journal) : []) {
321
+ if (event.harness !== 'opencode-server' || event.source?.method !== 'session/message' || !event.source.message_id) continue;
322
+ const key = `${event.session_id}:${event.source.message_id}`;
323
+ if (event.evidence_conflict) { const prior = this.modelMessages.get(key); if (prior) prior.conflicted = true; continue; }
324
+ if (!this.modelMessages.has(key)) this.modelMessages.set(key, { source: event.source,
325
+ known: { parent: event.parent_session_id, root: event.native_agent_role?.launch?.root_session_id,
326
+ role: event.native_agent_role ?? null, requested: observedFields(event.requested), observed: observedFields(event.observed) } });
327
+ }
328
+ }
303
329
  for (const message of messages ?? []) {
304
330
  const info = message.info;
305
- if (info?.role !== "assistant" || !info.id || this.modelMessages.has(info.id)) continue;
331
+ if (info?.role !== "assistant" || !info.id) continue;
306
332
  if (info.sessionID && info.sessionID !== id) throw new DaemonError("session_identity_mismatch", "native message belongs to another Session");
307
- await observeModel({ journal: this.state.config.journal ?? this.paths.journal, harness: "opencode-server", sessionId: id, parentSessionId: parent,
308
- requested: { ...this.state.config, reasoning: this.state.config.variant, mode: parent ? "dd-flow-worker" : this.state.config.agent },
309
- nativeAgentRole: this.state.config.workerLaunchConfig ? { name: info.mode ?? null, kind: parent ? 'subagent' : 'primary', parent_session_id: parent,
310
- launch: { ...this.state.config.workerLaunchConfig, root_session_id: this.sessions.get(id)?.root_provider_session_id ?? null } } : null,
311
- observed: { provider: info.providerID, model: info.modelID, reasoning: info.variant, mode: info.mode }, source: { method: "session/message", message_id: info.id, event_sha256: createHash("sha256").update(`${id}:${info.id}:${info.modelID}`).digest("hex") }, evidence: "response" });
312
- this.modelMessages.add(info.id);
333
+ const record = this.sessions.get(id);
334
+ if (record && (record.parent_provider_session_id ?? null) !== parent)
335
+ throw new DaemonError("session_identity_mismatch", "OpenCode observed message parent changed");
336
+ if (!record?.root_provider_session_id || !info.mode) {
337
+ await observeModel({ journal, harness: "opencode-server", sessionId: id, parentSessionId: parent,
338
+ source: { channel: "native.message_observer", scope: "response", non_asserting: true }, evidence: "unavailable", reason: "native_role_binding_unavailable" });
339
+ continue;
340
+ }
341
+ const requested = { ...this.state.config, reasoning: this.state.config.variant, mode: parent ? "dd-flow-worker" : this.state.config.agent };
342
+ const nativeAgentRole = this.state.config.workerLaunchConfig ? { name: info.mode, kind: parent ? 'subagent' : 'primary', parent_session_id: parent,
343
+ launch: { ...this.state.config.workerLaunchConfig, root_session_id: record.root_provider_session_id } } : null;
344
+ const observed = observedFields({ provider: info.providerID, model: info.modelID, reasoning: info.variant, mode: info.mode });
345
+ const tuple = { parent, root: record.root_provider_session_id, role: nativeAgentRole, requested: observedFields(requested), observed };
346
+ const key = `${id}:${info.id}`, previous = this.modelMessages.get(key);
347
+ if (previous?.conflicted) throw new DaemonError("model_observation_identity_conflict", "OpenCode native message has a retained identity/profile conflict", false, { session_id: id, message_id: info.id });
348
+ const conflicts = (before, after) => before != null && after != null && (typeof before === "object" && typeof after === "object"
349
+ ? [...new Set([...Object.keys(before), ...Object.keys(after)])].some(field => conflicts(before[field], after[field])) : before !== after);
350
+ if (previous && !conflicts(previous.known, tuple)) {
351
+ // Optional disclosure can arrive late. Do not rewrite the old native fact;
352
+ // remember known values only to detect a later positive contradiction.
353
+ for (const [field, value] of Object.entries(observed)) if (value != null) previous.known.observed[field] = value;
354
+ continue;
355
+ }
356
+ const source = previous?.source ?? { method: "session/message", message_id: info.id, event_sha256: createHash("sha256").update(`${id}:${info.id}`).digest("hex") };
357
+ const published = await observeModel({ journal, harness: "opencode-server", sessionId: id, parentSessionId: parent, requested, nativeAgentRole, observed, source, evidence: "response" });
358
+ if (previous || published?.evidence_conflict) {
359
+ if (previous) previous.conflicted = true;
360
+ throw new DaemonError("model_observation_identity_conflict", "OpenCode native message changed its published identity/profile", false, { session_id: id, message_id: info.id });
361
+ }
362
+ this.modelMessages.set(key, { known: structuredClone(tuple), source });
313
363
  }
314
364
  }
315
365
  async requireSettled(id) {
@@ -372,8 +422,27 @@ export class Runtime {
372
422
  current.reading = readNativeHookFailures(this.paths.dir, current.requests, current, error => this.retainHookFailure(current, error));
373
423
  try { await current.reading; } finally { current.reading = null; }
374
424
  }
375
- retainHookFailure(current, error) {
376
- if (this.nativeHookOperation !== current || isProvenShellCorrection(error)) return;
425
+ async retainHookFailure(current, error, unconfirmed = false) {
426
+ if (this.nativeHookOperation !== current) return;
427
+ if (unconfirmed || error.details?.native_hook_binding?.native_creator === 'unconfirmed') {
428
+ // A Session/tree association is not the native creator Turn. Preserve
429
+ // the real refusal, but do not abort the prompt merely being observed.
430
+ const diagnostic = { code: 'native_hook_failed', native_creator: 'unconfirmed', observer_operation_id: current.operationId, observer_turn_generation: current.generation, primary_error: errorRecord(error) };
431
+ (current.hookDiagnostics ??= []).push(diagnostic);
432
+ try { await this.journal('native_hook_observation_unavailable', { provider_session_id: current.sessionId, native_hook_diagnostic: diagnostic }); }
433
+ catch (storage) { process.stderr.write(`${diagnosticJson({ native_hook_diagnostic: diagnostic, journal_error: errorRecord(storage) })}\n`); }
434
+ return;
435
+ }
436
+ if (isProvenShellCorrection(error)) return;
437
+ if (isNativeHookObservationLoss(error)) {
438
+ // A timed-out observer has not established whether the owned CLI ran.
439
+ // The CLI still owns admission and the native turn owns its outcome.
440
+ const diagnostic = errorRecord(error);
441
+ (current.hookDiagnostics ??= []).push(diagnostic);
442
+ try { await this.journal("native_hook_observation_unavailable", { provider_session_id: current.sessionId, native_hook_diagnostic: diagnostic }); }
443
+ catch (storage) { process.stderr.write(`${diagnosticJson({ native_hook_diagnostic: diagnostic, journal_error: errorRecord(storage) })}\n`); }
444
+ return;
445
+ }
377
446
  error.details = { ...error.details, provider_session_id: current.sessionId, native_outcome: "unknown", native_turn_items: { observed: true, possible_effects: true, pending: true } };
378
447
  current.failure ??= error;
379
448
  }
@@ -381,12 +450,46 @@ export class Runtime {
381
450
  const assertDispatch = captureDispatchGuard(this);
382
451
  if (["session.create", "session.prompt", "session.start", "session.fork", "session.resume", "session.archive"].includes(operation)) assertDaemonDispatch(this.paths.dir);
383
452
  if (operation === "daemon.status") { const { password, ...config } = this.state.config; return { daemon_id: this.state.daemon_id, pid: process.pid, socket: this.paths.socket, versions: this.state.versions, auth_status: this.state.auth_status, shutdown_state: this.state.shutdown_state, active_tree: this.state.active_tree, active_operation: this.active, sessions: [...this.sessions.values()], config }; }
453
+ if (operation === "hook.register") {
454
+ if (!this.state.config.runtime_owner || !this.state.config.ddFlowBin) return {};
455
+ assertDaemonDispatch(this.paths.dir);
456
+ const payload = params.payload, observer = this.nativeHookOperation ?? this.lastNativeHookOperation;
457
+ if (typeof params.sessionId !== "string" || !params.sessionId || typeof payload?.message_id !== "string" || !payload.message_id
458
+ || typeof payload?.tool_call_id !== "string" || !payload.tool_call_id || !observer?.sessionId) throw new DaemonError("native_hook_unproven", "Native Work start lacks physical callback identity");
459
+ this.retainedWorkStarts ??= new Map();
460
+ const tool = `${payload.message_id}:${payload.tool_call_id}`;
461
+ const accepted = await retainNativeWorkStart(this.paths.dir, { daemon_id: this.state.daemon_id, source_harness: "opencode-server",
462
+ root_provider_session_id: observer.sessionId, provider_session_id: params.sessionId, parent_provider_session_id: null,
463
+ tool_call_id: tool, event: "PreToolUse", native_command: payload.input?.command, native_cwd: null,
464
+ native_payload: { schema_id: "dd-flow/opencode-tool-event@1", phase: "before", event_id: [this.state.daemon_id, params.sessionId, payload.message_id, payload.tool_call_id].join(":"),
465
+ daemon_id: this.state.daemon_id, message_id: payload.message_id, tool_call_id: tool, tool: "shell", input: payload.input,
466
+ session: { provider_session_id: params.sessionId } } }, observer, this.retainedWorkStarts);
467
+ return accepted ? { fastAck: true, nativeHookBinding: accepted } : {};
468
+ }
384
469
  if (operation === "hook.resolve") {
385
- const current = this.nativeHookOperation, sessionId = params.sessionId, payload = params.payload;
470
+ const payload = params.payload;
471
+ if (params.deferWorkStart === true && !this.state.config.noFlow && this.state.config.runtime_owner && this.state.config.ddFlowBin
472
+ && ["before", "after"].includes(payload?.phase) && typeof params.sessionId === "string" && params.sessionId
473
+ && typeof payload.message_id === "string" && payload.message_id && typeof payload.tool_call_id === "string" && payload.tool_call_id
474
+ && typeof payload.input?.command === "string") {
475
+ const analysis = parseLifecycleCommand(payload.input.command);
476
+ if (analysis.participation === "none" && !analysis.unresolved) {
477
+ // No lifecycle authority is involved. Native SSE/history still own
478
+ // source-bound progress; an unknown child callback is only a hint.
479
+ return { observationOnly: true, daemonId: this.state.daemon_id, sessionId: params.sessionId,
480
+ messageId: payload.message_id, toolCallId: payload.tool_call_id, command: payload.input.command };
481
+ }
482
+ }
483
+ if (params.deferWorkStart === true && params.payload?.phase === "before") {
484
+ const pending = await this.dispatch("hook.register", params);
485
+ if (pending.fastAck === true) return pending;
486
+ }
487
+ const current = this.nativeHookOperation, sessionId = params.sessionId;
386
488
  if (!current || !sessionId || typeof payload?.input?.command !== "string" || typeof payload.tool_call_id !== "string" || !payload.tool_call_id || typeof payload.message_id !== "string" || !payload.message_id || !["before", "after"].includes(payload.phase))
387
489
  throw new DaemonError("native_hook_unproven", "OpenCode hook has no exact active operation");
388
490
  let owned = sessionId === current.sessionId || this.sessions.get(sessionId)?.root_provider_session_id === current.sessionId;
389
491
  try {
492
+ const guard = current.guard ?? captureDispatchGuard(this); guard();
390
493
  const tree = await this.childTree(current.sessionId);
391
494
  if (sessionId !== current.sessionId && !tree.some(child => child.id === sessionId && !child.fork)) throw new DaemonError("foreign_session", "OpenCode hook Session is outside the owned native tree");
392
495
  owned = true;
@@ -396,29 +499,66 @@ export class Runtime {
396
499
  if (payload.input.workdir !== undefined && typeof payload.input.workdir !== "string") throw new DaemonError("native_hook_unproven", "OpenCode hook workdir must be a string");
397
500
  if (this.nativeHookOperation !== current) throw new DaemonError("native_hook_unproven", "OpenCode operation changed during hook identity observation");
398
501
  const accepted = await acceptNativeHookRequest(this.paths.dir, { daemon_id: this.state.daemon_id, operation_id: current.operationId, turn_generation: current.generation,
399
- provider_session_id: sessionId, root_provider_session_id: current.sessionId, tool_call_id: `${payload.message_id}:${payload.tool_call_id}`, event: payload.phase === "after" ? "PostToolUse" : "PreToolUse",
502
+ provider_session_id: sessionId, root_provider_session_id: current.sessionId, native_creator: 'unconfirmed', tool_call_id: `${payload.message_id}:${payload.tool_call_id}`, event: payload.phase === "after" ? "PostToolUse" : "PreToolUse",
400
503
  command_sha256: createHash("sha256").update(payload.input.command).digest("hex"), native_cwd: path.resolve(native.directory, payload.input.workdir ?? ".") }, current);
401
504
  if (this.nativeHookOperation !== current) throw new DaemonError("native_hook_unproven", "OpenCode operation changed during hook acceptance");
402
- current.requests.set(accepted.request_id, { current, binding: accepted }); return { nativeHookBinding: accepted, nativeSession: {
505
+ guard(); current.requests.set(accepted.request_id, { current, binding: accepted, guard }); return { nativeHookBinding: accepted, nativeSession: {
403
506
  id: native.id, directory: native.directory, parentID: native.parentID, projectID: native.projectID,
404
507
  model: { providerID: native.model?.providerID, id: native.model?.id, variant: native.model?.variant }
405
508
  } };
406
509
  } catch (error) {
407
- if (owned) this.retainHookFailure(current, error);
510
+ if (owned) await this.retainHookFailure(current, error, true);
408
511
  throw error;
409
512
  }
410
513
  }
411
514
  if (operation === "hook.verify") {
412
515
  const current = this.nativeHookOperation, binding = params.binding;
516
+ if (binding?.native_admission === "assigned_work_start") {
517
+ const retained = this.retainedWorkStarts?.get(params.requestId);
518
+ if (!retained || !isDeepStrictEqual(retained.binding, binding)) throw new DaemonError("native_hook_unproven", "Unknown native Work start callback");
519
+ retained.guard(); assertDaemonDispatch(this.paths.dir);
520
+ const root = binding.root_provider_session_id, id = binding.provider_session_id;
521
+ const report = operationProgressReporter();
522
+ const checkpoint = cursor => report('verification_progress', { native_request_id: cursor, session_id: id });
523
+ const tree = await this.childTree(root, new Set(), checkpoint), child = tree.find(node => node.id === id);
524
+ if (id !== root && (!child || child.fork)) throw new DaemonError("foreign_session", "Native Work start is outside its controlled tree");
525
+ const native = await this.client.inspect(id);
526
+ await checkpoint(`session:${id}:${createHash('sha256').update(JSON.stringify(native)).digest('hex')}`);
527
+ if (native.id !== id || native.fork || !path.isAbsolute(native.directory ?? "") || child && native.parentID !== child.parentID
528
+ || id === root && native.parentID) throw new DaemonError("native_hook_identity_conflict", "Native Work start ancestry is contradictory");
529
+ const input = binding.native_payload.input;
530
+ if (input.workdir !== undefined && typeof input.workdir !== "string") throw new DaemonError("native_hook_unproven", "Native Work start has invalid workdir");
531
+ retained.guard();
532
+ const cwd = path.resolve(native.directory, input.workdir ?? "."), parent = native.parentID ?? null;
533
+ const verified = { ...binding, parent_provider_session_id: parent, native_cwd: cwd, native_ancestry: "confirmed",
534
+ native_payload: { ...binding.native_payload, input: { ...input, workdir: cwd }, session: { provider_session_id: id, parent_provider_session_id: parent, directory: cwd },
535
+ profile: { provider: native.model?.providerID, model: native.model?.id, variant: native.model?.variant ?? "default" } } };
536
+ return { verified: true, registeredHookBinding: binding, nativeHookBinding: verified };
537
+ }
538
+ if (params.phase === "after") {
539
+ const retained = this.retainedHookRequests.get(binding?.request_id);
540
+ if (!retained || JSON.stringify(retained.binding) !== JSON.stringify(binding)) throw new DaemonError("native_hook_unproven", "OpenCode after hook has no retained original delivery");
541
+ assertDaemonDispatch(this.paths.dir); retained.guard();
542
+ return { current: true, retained: true };
543
+ }
413
544
  if (!current || binding?.operation_id !== current.operationId || binding?.turn_generation !== current.generation
414
545
  || JSON.stringify(current.requests.get(binding?.request_id)?.binding) !== JSON.stringify(binding)) throw new DaemonError("native_hook_unproven", "OpenCode hook generation is no longer current");
546
+ const accepted = current.requests.get(binding.request_id); assertDaemonDispatch(this.paths.dir); accepted.guard();
547
+ if (params.retain_after === true && binding.event === "PreToolUse") this.retainedHookRequests.set(binding.request_id, { binding, guard: accepted.guard });
415
548
  return { current: true };
416
549
  }
417
550
  if (operation === "hook.failed") {
418
551
  if (params.failure) {
419
- await this.dispatch("hook.verify", { binding: params.binding });
420
552
  if (JSON.stringify(params.failure.details?.native_hook_binding) !== JSON.stringify(params.binding) || !params.failure.details?.receipt_error || typeof params.failure.code !== "string" || typeof params.failure.message !== "string") throw new DaemonError("native_hook_receipt_invalid", "OpenCode fallback hook error is not bound");
421
- this.retainHookFailure(this.nativeHookOperation, reportedError(params.failure, "Native hook failed"));
553
+ const current = this.nativeHookOperation, binding = params.binding;
554
+ const isCurrent = current && binding?.operation_id === current.operationId && binding?.turn_generation === current.generation
555
+ && JSON.stringify(current.requests.get(binding?.request_id)?.binding) === JSON.stringify(binding);
556
+ await this.dispatch("hook.verify", isCurrent ? { binding } : { binding, phase: "after" });
557
+ // The caller's write failed. Do not acknowledge the fallback until the
558
+ // same owner-bound failure is durable; a late after never poisons the
559
+ // currently active, different prompt.
560
+ await writeNativeHookFailure(this.paths.dir, binding, reportedError(params.failure, "Native hook failed"));
561
+ if (!isCurrent) return { observed: true, retained: true };
422
562
  }
423
563
  await this.observeHookFailures(); return { observed: true };
424
564
  }
@@ -453,8 +593,9 @@ export class Runtime {
453
593
  if (operation === "session.inspect" || operation === "session.resume") { const result = await this.describe(params.sessionId); await this.persist(); return result; }
454
594
  if (operation === "session.prompt") { await this.requireSettled(params.sessionId); return this.productive(operation, async () => {
455
595
  this.state.operations ??= {}; const key = operationId ?? randomUUID(), retained = this.state.operations[key];
456
- const current = { operationId: key, sessionId: params.sessionId, generation: (this.state.hook_generation ?? 0) + 1, requests: new Map(), failure: null };
596
+ const current = { operationId: key, sessionId: params.sessionId, generation: (this.state.hook_generation ?? 0) + 1, requests: new Map(), failure: null, guard: assertDispatch };
457
597
  this.state.hook_generation = current.generation; this.nativeHookOperation = current;
598
+ this.lastNativeHookOperation = current;
458
599
  const checkHookFailures = async () => { await this.observeHookFailures(current); if (this.nativeHookOperation !== current) throw new DaemonError("native_hook_unproven", "OpenCode prompt generation changed"); if (current.failure) throw current.failure; };
459
600
  let reply;
460
601
  try { assertDispatch(); reply = await this.client.prompt(params.sessionId, { prompt: params.prompt, messageId: retained?.message_id ?? `msg_${randomUUID().replaceAll("-", "")}`, retained,
@@ -462,6 +603,9 @@ export class Runtime {
462
603
  assertDispatch, checkHookFailures, livenessTimeoutMs: params.livenessTimeoutMs ?? this.client.timeoutMs,
463
604
  onDispatched: () => this.journal("native_dispatch", { operation_id: operationId, provider_session_id: params.sessionId }),
464
605
  onModelMessages: (id, messages, parent) => this.observeModels(id, messages, parent),
606
+ onNativeSession: (info, root) => this.registerObservedChild(info, root),
607
+ rootSessionId: this.sessions.get(params.sessionId)?.root_provider_session_id ?? params.sessionId,
608
+ parentSessionId: this.sessions.get(params.sessionId)?.parent_provider_session_id ?? null,
465
609
  onObservationError: error => observeModel({ journal: this.state.config.journal ?? this.paths.journal, harness: "opencode-server", sessionId: params.sessionId, requested: this.state.config, source: "native.message_observer", evidence: "unavailable", reason: error.code ?? "observation_unavailable" })
466
610
  }); await checkHookFailures(); }
467
611
  finally { current.requests.clear(); if (this.nativeHookOperation === current) this.nativeHookOperation = null; }
@@ -476,6 +620,7 @@ export class Runtime {
476
620
  if (!result.route_conformance.matches) throw new DaemonError("invocation_directory_mismatch", "Owned OpenCode Session moved during execution", false, { ...result.route_violations[0], native_outcome: result.terminal_status, settled: result.settled, result });
477
621
  result.response_message_id = reply.info.id;
478
622
  result.observed_profile = { ...reply_profile, mode: result.info.agent ?? reply_profile.mode };
623
+ if (current.hookDiagnostics?.length) result.native_hook_diagnostics = current.hookDiagnostics;
479
624
  checkObservedProfile({ ...this.state.config, reasoning: this.state.config.variant, mode: expectedAgent }, result.observed_profile); result.usage_ingest_errors = await this.reconcileUsage(result); return result; }); }
480
625
  if (operation === "session.fork") { const source = await this.requireSettled(params.sessionId); return this.productive(operation, async () => {
481
626
  const info = await this.client.fork(params.sessionId, params.messageId);
@@ -2,7 +2,6 @@ import { createHash, randomUUID } from "node:crypto";
2
2
  import { setTimeout as delay } from "node:timers/promises";
3
3
  import { ToolObservations, openCodeToolObservation } from "./tool-observations.mjs";
4
4
  import { ObservationClock, observedTimeout, validateObservationDuration } from "./observation-clock.mjs";
5
- import { assertNativeLine } from "./native-streams.mjs";
6
5
  import { operationProgressReporter } from "./operation-progress.mjs";
7
6
  import { classifyRecoveryError as classifyProviderError, safeProviderError } from "./provider-overload-policy.mjs";
8
7
 
@@ -98,7 +97,7 @@ export class OpenCodeClient {
98
97
  const options = { signal: controller.signal };
99
98
  const pending = input === undefined ? domain[name](options) : domain[name](input, options);
100
99
  void pending.catch(() => {}); await onDispatched?.(); const result = await pending;
101
- assertNativeLine(JSON.stringify(result ?? null)); return result;
100
+ return result;
102
101
  } catch (cause) {
103
102
  if (cause instanceof OpenCodeError || cause.code === "operation_observation_lost") throw cause;
104
103
  if (cause._tag === "SessionNotFoundError" || cause._tag === "NotFoundError") throw new OpenCodeError("session_not_found", "OpenCode native Session is unavailable");
@@ -169,13 +168,32 @@ export class OpenCodeClient {
169
168
  import(data) { return this.call("session.import", data, { productive: true }); }
170
169
 
171
170
  async prompt(id, { prompt, messageId = `msg_${randomUUID().replaceAll("-", "")}`, retained,
172
- save, onModelMessages, onObservationError, assertDispatch, onDispatched, checkHookFailures, livenessTimeoutMs = this.timeoutMs }) {
171
+ save, onModelMessages, onNativeSession, rootSessionId = id, parentSessionId = null,
172
+ onObservationError, assertDispatch, onDispatched, checkHookFailures, livenessTimeoutMs = this.timeoutMs }) {
173
173
  validateObservationDuration(livenessTimeoutMs);
174
174
  const request = { sessionID: id, id: messageId, text: prompt, delivery: "queue", resume: true }, digest = requestDigest(request);
175
175
  if (retained && (retained.request_sha256 !== digest || retained.message_id !== messageId)) throw new OpenCodeError("operation_request_mismatch", "OpenCode operation already binds another prompt", false, { effect: "no_effect" });
176
176
  let state = retained ?? { message_id: messageId, request_sha256: digest, phase: "prepared", cursor: 0 };
177
- const previous = await this.messages(id), baseline = new Map([[id, new Set(previous.map(item => item.info.id))]]), parents = new Map();
178
- const capture = async session => { for (const child of await this.children(session)) { parents.set(child.id, child.parentID); baseline.set(child.id, new Set((await this.messages(child.id)).map(item => item.info.id))); await capture(child.id); } };
177
+ const previous = await this.messages(id), baseline = new Map([[id, new Set(previous.map(item => item.info.id))]]), parents = new Map([[id, parentSessionId]]);
178
+ // The same native lineage guard applies before dispatch and while polling.
179
+ // Registration precedes message publication; a list ID alone is not ownership.
180
+ const childrenOf = async (session, visited) => {
181
+ const children = await this.children(session);
182
+ for (const child of children) {
183
+ if (!child?.id || child.parentID !== session || child.fork || session !== rootSessionId || visited.has(child.id)
184
+ || parents.has(child.id) && parents.get(child.id) !== session || visited.size >= 1024)
185
+ throw new OpenCodeError("session_identity_mismatch", "OpenCode native child lineage is invalid", false, { session_id: child?.id, parent_session_id: session });
186
+ visited.add(child.id); parents.set(child.id, session);
187
+ await onNativeSession?.(child, rootSessionId);
188
+ }
189
+ return children;
190
+ };
191
+ const capture = async (session, visited = new Set([id])) => {
192
+ for (const child of await childrenOf(session, visited)) {
193
+ baseline.set(child.id, new Set((await this.messages(child.id)).map(item => item.info.id)));
194
+ await capture(child.id, visited);
195
+ }
196
+ };
179
197
  await capture(id);
180
198
  if (retained) {
181
199
  const index = previous.findIndex(item => item.info.id === messageId);
@@ -258,8 +276,8 @@ export class OpenCodeClient {
258
276
  }
259
277
  if (state.cursor !== watermark) { state = { ...state, cursor: watermark }; await save?.(state); }
260
278
  let runningTools = false;
261
- const observe = async (session, visited = new Set()) => {
262
- if (visited.has(session)) throw lost("OpenCode topology contains a cycle"); visited.add(session); known.add(session);
279
+ const observe = async (session, visited = new Set([id])) => {
280
+ known.add(session);
263
281
  const messages = await this.messages(session);
264
282
  await onModelMessages?.(session, messages, parents.get(session) ?? null);
265
283
  const floor = baseline.get(session) ?? new Set(), current = messages.filter(item => item.info.role === "assistant" && !floor.has(item.info.id));
@@ -291,10 +309,7 @@ export class OpenCodeClient {
291
309
  }
292
310
  }
293
311
  }
294
- for (const child of await this.children(session)) {
295
- if (child.parentID !== session) throw lost("OpenCode child has a foreign parent");
296
- parents.set(child.id, session); await observe(child.id, visited);
297
- }
312
+ for (const child of await childrenOf(session, visited)) await observe(child.id, visited);
298
313
  return current;
299
314
  };
300
315
  const current = await observe(id), rootMessages = await this.messages(id);
@@ -309,12 +324,17 @@ export class OpenCodeClient {
309
324
  // Native messages and topology are separate reads: a child can
310
325
  // finish between them. Reconcile its ancestry before rejecting
311
326
  // the notification, never trust the notification alone.
312
- const child = (await this.children(id)).find(child => child.id === item.info.metadata.childID && child.parentID === id);
313
- if (child) { parents.set(child.id, id); await observe(child.id); }
327
+ const visited = new Set([id]);
328
+ const child = (await childrenOf(id, visited)).find(child => child.id === item.info.metadata.childID);
329
+ if (child) await observe(child.id, visited);
314
330
  }
315
- if (item.info.role === "user" || item.info.role === "synthetic" && !(
316
- item.info.metadata?.source === "subagent" && parents.get(item.info.metadata.childID) === id
317
- || item.info.metadata?.source === "shell" && shellCursors.has(item.info.metadata.shellID)))
331
+ // Native continuation messages are synthetic, including retries
332
+ // without metadata. Only actual user input crosses the owned
333
+ // operation boundary. Claimed child/shell delivery is validated
334
+ // separately before it can settle a background task.
335
+ if (item.info.role === "user" || item.info.role === "synthetic" && (
336
+ item.info.metadata?.source === "subagent" && parents.get(item.info.metadata.childID) !== id
337
+ || item.info.metadata?.source === "shell" && !shellCursors.has(item.info.metadata.shellID)))
318
338
  throw new OpenCodeError("opencode_foreign_input", "OpenCode received input outside the owned operation", false, { session_id: id, message_id: item.info.id, role: item.info.role, source: item.info.metadata?.source });
319
339
  }
320
340
  const terminal = rootMessages.slice(deliveredIndex + 1).findLast(item => item.info.role === "idle");
@@ -327,8 +347,8 @@ export class OpenCodeClient {
327
347
  // A background child's native completion notification is delivered
328
348
  // asynchronously. An idle parent before that delivery is not the
329
349
  // completion of the coordinating operation.
330
- const deliveredNotifications = new Set(rootMessages.filter(item => item.info.role === "synthetic" && item.info.metadata?.source === "subagent").map(item => item.info.metadata.childID));
331
- const shellNotifications = new Set(rootMessages.filter(item => item.info.role === "synthetic" && item.info.metadata?.source === "shell").map(item => item.info.metadata.shellID));
350
+ const deliveredNotifications = new Set(rootMessages.filter(item => item.info.role === "synthetic" && item.info.metadata?.source === "subagent" && parents.get(item.info.metadata.childID) === id).map(item => item.info.metadata.childID));
351
+ const shellNotifications = new Set(rootMessages.filter(item => item.info.role === "synthetic" && item.info.metadata?.source === "shell" && shellCursors.has(item.info.metadata.shellID)).map(item => item.info.metadata.shellID));
332
352
  unsettled ||= [...backgroundShells].some(shell => !shellNotifications.has(shell));
333
353
  for (const item of current) for (const part of item.parts) {
334
354
  if (part.tool === "subagent" && part.state?.input?.background === true) {
@@ -353,7 +373,8 @@ export class OpenCodeClient {
353
373
  }
354
374
  } catch (error) {
355
375
  await checkHookFailures?.();
356
- if (error instanceof OpenCodeError && !["opencode_unavailable", "operation_observation_lost"].includes(error.code)) throw error;
376
+ if (error instanceof OpenCodeError && !["opencode_unavailable", "operation_observation_lost"].includes(error.code)
377
+ || ["session_identity_mismatch", "model_observation_identity_conflict"].includes(error.code)) throw error;
357
378
  await onObservationError?.(error);
358
379
  }
359
380
  if (clock.sample(marker)) throw lost("OpenCode native progress stopped or observation could not recover", { session_id: id, message_id: messageId, observation: clock.state(), ...(observationErrors.count ? { observation_errors: observationErrors } : {}) });
@@ -0,0 +1 @@
1
+ export function callDaemon(stateDir: string, operation: string, params?: Record<string, unknown>, timeoutMs?: number): Promise<Record<string, unknown>>;