@deksden-com/dd-flow-cli 0.9.0-beta.137 → 0.9.0-beta.139

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 (283) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +9 -0
  3. package/dist/build-info.json +6 -6
  4. package/dist/cli/argument-values.js +5 -5
  5. package/dist/cli/command-inputs.js +89 -20
  6. package/dist/cli/command-metadata.js +197 -0
  7. package/dist/cli/command-option-descriptions.js +191 -0
  8. package/dist/cli/completion-cli.js +70 -0
  9. package/dist/cli/completion.js +57 -0
  10. package/dist/cli/error-output.js +52 -0
  11. package/dist/cli/executor-onboarding.js +76 -0
  12. package/dist/cli/flows.js +29 -0
  13. package/dist/cli/harnesses.js +84 -0
  14. package/dist/cli/help.js +240 -6
  15. package/dist/cli/input-preparation.js +54 -5
  16. package/dist/cli/memory-flow-admission.js +44 -0
  17. package/dist/cli/memory-init-invocation.js +78 -0
  18. package/dist/cli/memory-init.js +240 -0
  19. package/dist/cli/memory-storage-admission.js +53 -0
  20. package/dist/cli/models.js +39 -0
  21. package/dist/cli/native-hook-ingress.js +1 -1
  22. package/dist/cli/operator-profiles.js +61 -0
  23. package/dist/cli/output-options.js +35 -0
  24. package/dist/cli/response-file.js +71 -0
  25. package/dist/cli/run-cli.js +283 -160
  26. package/dist/cli/runtime-process.js +1 -1
  27. package/dist/cli/setup.js +149 -0
  28. package/dist/cli.js +56 -2
  29. package/dist/domain/stage-handler-identities.js +20 -0
  30. package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
  31. package/dist/harness-runtime/lib/daemon-operations.mjs +4 -3
  32. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +53 -22
  33. package/dist/harness-runtime/lib/dd-agy.mjs +16 -5
  34. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +12 -6
  35. package/dist/harness-runtime/lib/dd-codex.mjs +24 -12
  36. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +2 -1
  37. package/dist/harness-runtime/lib/dd-droid.mjs +32 -9
  38. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +11 -3
  39. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +100 -15
  40. package/dist/harness-runtime/lib/dd-opencode.mjs +51 -20
  41. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +22 -10
  42. package/dist/harness-runtime/lib/dd-zcode.mjs +17 -4
  43. package/dist/harness-runtime/lib/delegation-instructions.d.mts +1 -0
  44. package/dist/harness-runtime/lib/delegation-instructions.mjs +16 -3
  45. package/dist/harness-runtime/lib/lease-renewal.mjs +2 -1
  46. package/dist/harness-runtime/lib/lifecycle-command.mjs +1 -0
  47. package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -0
  48. package/dist/harness-runtime/lib/managed-daemon.mjs +50 -15
  49. package/dist/harness-runtime/lib/managed-flow-env.mjs +15 -0
  50. package/dist/harness-runtime/lib/native-children.d.mts +2 -0
  51. package/dist/harness-runtime/lib/native-children.mjs +20 -1
  52. package/dist/harness-runtime/lib/native-hook-command.mjs +1 -1
  53. package/dist/harness-runtime/lib/native-request-proof.d.mts +2 -0
  54. package/dist/harness-runtime/lib/native-request-proof.mjs +65 -0
  55. package/dist/harness-runtime/lib/opencode-plugin.mjs +32 -9
  56. package/dist/harness-runtime/lib/operation-progress.mjs +3 -1
  57. package/dist/harness-runtime/lib/provider-overload-policy.d.mts +13 -0
  58. package/dist/harness-runtime/lib/provider-overload-policy.mjs +138 -0
  59. package/dist/runtime/context.js +1 -1
  60. package/dist/schemas/discovery-result.schema.json +184 -0
  61. package/dist/schemas/execution-bundle.schema.json +205 -0
  62. package/dist/schemas/execution-settings.schema.json +162 -0
  63. package/dist/schemas/flow-descriptor.schema.json +54 -0
  64. package/dist/schemas/flow-run.schema.json +26 -0
  65. package/dist/schemas/harness-catalog.schema.json +80 -0
  66. package/dist/schemas/mb-init-compose-result.schema.json +457 -0
  67. package/dist/schemas/mb-init-discover-result.schema.json +500 -0
  68. package/dist/schemas/mb-init-finish-result.schema.json +422 -0
  69. package/dist/schemas/mb-init-preflight-result.schema.json +367 -0
  70. package/dist/schemas/mb-init-review-result.schema.json +396 -0
  71. package/dist/schemas/mb-init-verify-result.schema.json +391 -0
  72. package/dist/schemas/memory-bootstrap-receipt-v2.schema.json +67 -0
  73. package/dist/schemas/memory-bootstrap-receipt.schema.json +354 -0
  74. package/dist/schemas/memory-composition-work-result.schema.json +38 -0
  75. package/dist/schemas/memory-repair-work-result.schema.json +49 -0
  76. package/dist/schemas/memory-review-correction-plan.schema.json +30 -0
  77. package/dist/schemas/memory-review-work-result.schema.json +43 -0
  78. package/dist/schemas/model-catalog.schema.json +153 -0
  79. package/dist/schemas/profile-settings.schema.json +122 -0
  80. package/dist/schemas/release-manifest.schema.json +291 -0
  81. package/dist/schemas/run-engine-binding.schema.json +1 -1
  82. package/dist/services/check-runner-evidence.js +51 -4
  83. package/dist/services/cli-operation-classifier.js +8 -47
  84. package/dist/services/cli-upgrade-allowlist.js +27 -0
  85. package/dist/services/code-checks.js +387 -34
  86. package/dist/services/controller-child-capacity.js +13 -0
  87. package/dist/services/controller-child-observer.js +18 -2
  88. package/dist/services/controller-execution.js +88 -0
  89. package/dist/services/controller-fanout.js +71 -12
  90. package/dist/services/dashboard.js +8 -2
  91. package/dist/services/descriptor-coordinator-session.js +35 -0
  92. package/dist/services/descriptor-root-work.js +53 -0
  93. package/dist/services/descriptor-run-settlement.js +105 -0
  94. package/dist/services/descriptor-run-start.js +449 -0
  95. package/dist/services/descriptor-stage-coordinator.js +197 -0
  96. package/dist/services/descriptor-stage-settlement.js +222 -0
  97. package/dist/services/descriptor-stage-start.js +139 -0
  98. package/dist/services/discovery/agy-discovery.js +2 -0
  99. package/dist/services/discovery/codex-discovery.js +2 -0
  100. package/dist/services/discovery/droid-discovery.js +2 -0
  101. package/dist/services/discovery/grok-discovery.js +2 -0
  102. package/dist/services/discovery/opencode-discovery.js +2 -0
  103. package/dist/services/discovery/static.js +31 -0
  104. package/dist/services/discovery/zcode-discovery.js +2 -0
  105. package/dist/services/engines.js +39 -22
  106. package/dist/services/eval-snapshots.js +83 -25
  107. package/dist/services/execution-bundles.js +263 -0
  108. package/dist/services/execution-policy.js +20 -16
  109. package/dist/services/executor-onboarding.js +56 -0
  110. package/dist/services/external-work-launch.js +93 -15
  111. package/dist/services/flow-catalog.js +21 -0
  112. package/dist/services/flow-descriptors.js +82 -0
  113. package/dist/services/frozen-engine.js +250 -0
  114. package/dist/services/generic-execution.js +164 -0
  115. package/dist/services/harness-adapter.js +28 -2
  116. package/dist/services/harness-discovery.js +29 -0
  117. package/dist/services/harness-doctor.js +75 -0
  118. package/dist/services/harness-probe-effects.js +51 -0
  119. package/dist/services/harness-version-probe.js +100 -0
  120. package/dist/services/hooks.js +24 -6
  121. package/dist/services/hybrid-contracts.js +483 -0
  122. package/dist/services/ids.js +29 -1
  123. package/dist/services/installation-lock.js +95 -0
  124. package/dist/services/installed-flow-source.js +183 -0
  125. package/dist/services/lifecycle-invocations.js +257 -78
  126. package/dist/services/managed-processes.js +31 -10
  127. package/dist/services/memory-accepted-composition.js +77 -0
  128. package/dist/services/memory-baseline-preservation.js +65 -0
  129. package/dist/services/memory-completed-correction-wave.js +320 -0
  130. package/dist/services/memory-compose-handler.js +258 -0
  131. package/dist/services/memory-composition-acceptance.js +199 -0
  132. package/dist/services/memory-composition-graph.js +134 -0
  133. package/dist/services/memory-composition-registration.js +146 -0
  134. package/dist/services/memory-composition-work-custody.js +151 -0
  135. package/dist/services/memory-composition-work-results.js +96 -0
  136. package/dist/services/memory-corrected-review-acceptance.js +129 -0
  137. package/dist/services/memory-corrected-review-batch.js +30 -0
  138. package/dist/services/memory-corrected-review-findings.js +106 -0
  139. package/dist/services/memory-corrected-review-inputs.js +210 -0
  140. package/dist/services/memory-corrected-review-projection.js +78 -0
  141. package/dist/services/memory-corrected-review-receipt.js +170 -0
  142. package/dist/services/memory-corrected-review-registration.js +191 -0
  143. package/dist/services/memory-correction-check-receipt.js +219 -0
  144. package/dist/services/memory-correction-input-owner.js +6 -0
  145. package/dist/services/memory-discover-handler.js +104 -0
  146. package/dist/services/memory-finish-evidence.js +435 -0
  147. package/dist/services/memory-finish-handler.js +215 -0
  148. package/dist/services/memory-finish-integration.js +280 -0
  149. package/dist/services/memory-finish-readiness.js +122 -0
  150. package/dist/services/memory-git-integration.js +151 -0
  151. package/dist/services/memory-historical-corrected-review.js +206 -0
  152. package/dist/services/memory-historical-original-review.js +130 -0
  153. package/dist/services/memory-historical-repair-registration.js +99 -0
  154. package/dist/services/memory-historical-second-review.js +128 -0
  155. package/dist/services/memory-init-bootstrap-readiness.js +210 -0
  156. package/dist/services/memory-init-contexts.js +89 -0
  157. package/dist/services/memory-init-controller-start.js +84 -0
  158. package/dist/services/memory-init-engine-release.js +223 -0
  159. package/dist/services/memory-init-git-policy.js +136 -0
  160. package/dist/services/memory-init-lint-capability.js +185 -0
  161. package/dist/services/memory-init-native-capacity.js +296 -0
  162. package/dist/services/memory-init-preflight.js +330 -0
  163. package/dist/services/memory-init-project-bootstrap.js +599 -0
  164. package/dist/services/memory-init-request-candidate.js +157 -0
  165. package/dist/services/memory-init-request-input.js +66 -0
  166. package/dist/services/memory-init-resource-storage.js +125 -0
  167. package/dist/services/memory-init-run-allocation.js +81 -0
  168. package/dist/services/memory-init-run-bundle.js +98 -0
  169. package/dist/services/memory-init-start.js +181 -0
  170. package/dist/services/memory-init-startup-custody.js +80 -0
  171. package/dist/services/memory-init-startup-input.js +240 -0
  172. package/dist/services/memory-init-storage.js +81 -0
  173. package/dist/services/memory-init-stores.js +359 -0
  174. package/dist/services/memory-init-worktrunk-capability.js +178 -0
  175. package/dist/services/memory-layer-publication.js +611 -0
  176. package/dist/services/memory-layer.js +156 -0
  177. package/dist/services/memory-lint-config.js +85 -0
  178. package/dist/services/memory-native-review-copy.js +165 -0
  179. package/dist/services/memory-normal-flow-entry.js +75 -0
  180. package/dist/services/memory-ordinary-defaults.js +112 -0
  181. package/dist/services/memory-preflight-bootstrap-custody.js +115 -0
  182. package/dist/services/memory-preflight-handler.js +259 -0
  183. package/dist/services/memory-repair-current-view.js +299 -0
  184. package/dist/services/memory-repair-document-delta.js +451 -0
  185. package/dist/services/memory-repair-history.js +491 -0
  186. package/dist/services/memory-repair-physical-binding.js +312 -0
  187. package/dist/services/memory-repair-publication-files.js +152 -0
  188. package/dist/services/memory-repair-publication-intent.js +27 -0
  189. package/dist/services/memory-repair-publication-plan.js +160 -0
  190. package/dist/services/memory-repair-publication-source.js +225 -0
  191. package/dist/services/memory-repair-publication.js +794 -0
  192. package/dist/services/memory-repair-registration.js +239 -0
  193. package/dist/services/memory-repair-work-baseline.js +413 -0
  194. package/dist/services/memory-repair-work-completion.js +97 -0
  195. package/dist/services/memory-repair-work-custody.js +73 -0
  196. package/dist/services/memory-repair-work-results.js +89 -0
  197. package/dist/services/memory-repair-workspace.js +252 -0
  198. package/dist/services/memory-repair-writer-copy.js +735 -0
  199. package/dist/services/memory-result-binding.js +173 -0
  200. package/dist/services/memory-retained-review-authority.js +129 -0
  201. package/dist/services/memory-retained-review-results.js +70 -0
  202. package/dist/services/memory-retained-second-baseline.js +102 -0
  203. package/dist/services/memory-retained-second-repairs.js +156 -0
  204. package/dist/services/memory-review-acceptance.js +214 -0
  205. package/dist/services/memory-review-correction-plan.js +171 -0
  206. package/dist/services/memory-review-handler.js +585 -0
  207. package/dist/services/memory-review-inputs.js +200 -0
  208. package/dist/services/memory-review-receipt.js +180 -0
  209. package/dist/services/memory-review-registration.js +108 -0
  210. package/dist/services/memory-review-work-custody.js +124 -0
  211. package/dist/services/memory-review-work-input.js +72 -0
  212. package/dist/services/memory-review-work-results.js +73 -0
  213. package/dist/services/memory-second-corrected-review-inputs.js +165 -0
  214. package/dist/services/memory-second-correction-plan.js +76 -0
  215. package/dist/services/memory-second-repair-baseline.js +91 -0
  216. package/dist/services/memory-second-repair-launch.js +614 -0
  217. package/dist/services/memory-second-repair-registration.js +88 -0
  218. package/dist/services/memory-second-repair-source.js +187 -0
  219. package/dist/services/memory-second-review-acceptance.js +115 -0
  220. package/dist/services/memory-source-observations.js +117 -0
  221. package/dist/services/memory-stage-dispatch.js +28 -0
  222. package/dist/services/memory-stage-results.js +199 -0
  223. package/dist/services/memory-verification-acceptance.js +135 -0
  224. package/dist/services/memory-verification-checks.js +252 -0
  225. package/dist/services/memory-verification-receipt.js +159 -0
  226. package/dist/services/memory-verify-handler.js +135 -0
  227. package/dist/services/memory-work-result-preparation.js +63 -0
  228. package/dist/services/merge-server.js +2 -4
  229. package/dist/services/model-catalog.js +109 -0
  230. package/dist/services/model-discovery.js +46 -0
  231. package/dist/services/operator-profiles.js +236 -0
  232. package/dist/services/projects.js +17 -2
  233. package/dist/services/{codex-capacity.js → provider-capacity.js} +34 -16
  234. package/dist/services/recovery-snapshot-database.js +417 -2
  235. package/dist/services/release-archive-producer.js +52 -0
  236. package/dist/services/release-archive.js +143 -0
  237. package/dist/services/release-cache.js +142 -0
  238. package/dist/services/release-compatibility.js +18 -0
  239. package/dist/services/release-download.js +67 -0
  240. package/dist/services/release-envelope.js +104 -0
  241. package/dist/services/release-manifest.js +45 -0
  242. package/dist/services/release-payload.js +114 -0
  243. package/dist/services/release-producer.js +63 -0
  244. package/dist/services/release-provenance.js +160 -0
  245. package/dist/services/release-registry.js +66 -0
  246. package/dist/services/release-source.js +194 -0
  247. package/dist/services/repair-intents.js +1 -1
  248. package/dist/services/review-copy.js +10 -2
  249. package/dist/services/run-control.js +80 -9
  250. package/dist/services/run-controller-adapter.js +1 -1
  251. package/dist/services/run-controller-capture.js +10 -3
  252. package/dist/services/run-controller-process.js +21 -0
  253. package/dist/services/run-controller-recovery.js +5 -5
  254. package/dist/services/run-controller-state.js +1 -1
  255. package/dist/services/run-controller.js +158 -64
  256. package/dist/services/run-recovery-guard.js +5 -3
  257. package/dist/services/run-recovery-runtime.js +3 -3
  258. package/dist/services/run-recovery.js +145 -5
  259. package/dist/services/run-start-custody.js +177 -0
  260. package/dist/services/runs.js +413 -30
  261. package/dist/services/setup.js +377 -0
  262. package/dist/services/stage-context.js +9 -1
  263. package/dist/services/stage-handlers.js +74 -0
  264. package/dist/services/stage-report-renderer.js +63 -0
  265. package/dist/services/stage-work-graph.js +17 -0
  266. package/dist/services/vnext-code-review.js +1 -1
  267. package/dist/services/vnext-code.js +6 -6
  268. package/dist/services/vnext-execution-profile.js +2 -2
  269. package/dist/services/vnext-fanout.js +73 -11
  270. package/dist/services/vnext-merge.js +8 -3
  271. package/dist/services/vnext-plan-review.js +7 -5
  272. package/dist/services/vnext-plan.js +3 -3
  273. package/dist/services/vnext-protocolize.js +20 -6
  274. package/dist/services/vnext-workspace-policy.js +7 -2
  275. package/dist/services/work-registry.js +616 -71
  276. package/dist/services/workspace-files.js +1 -1
  277. package/dist/storage/database.js +84 -6
  278. package/dist/storage/writer-contract.js +11 -7
  279. package/package.json +13 -3
  280. package/tools/create-canon-release.mjs +33 -0
  281. package/tools/produce-hybrid-release.mjs +41 -0
  282. package/dist/harness-runtime/lib/codex-capacity-policy.d.mts +0 -10
  283. package/dist/harness-runtime/lib/codex-capacity-policy.mjs +0 -102
@@ -13,7 +13,8 @@ import { readNativeLines, streamNativeDiagnostics, nativeObservationError } from
13
13
  import { operationProgressReporter } from "./operation-progress.mjs";
14
14
  import { publishSetup } from './setup-progress.mjs';
15
15
 
16
- const LATE_RESPONSE_GRACE_MS = 5_000;
16
+ export const CODEX_REQUEST_TIMEOUT_MS = 120_000;
17
+ export const LATE_RESPONSE_GRACE_MS = 5_000;
17
18
  const CODEX_SESSION_CONFIG = Object.freeze({
18
19
  bypass_hook_trust: true,
19
20
  "features.plugins": false,
@@ -280,7 +281,7 @@ export class CodexBridge {
280
281
  const correction = isProvenShellCorrection(error);
281
282
  error.details = { ...error.details, native_hook_binding: { provider_session_id: threadId, root_provider_session_id: root, turn_id: turnId,
282
283
  hook_run_id: run.id, source: "codex.hook/completed" }, provider_session_id: threadId, root_provider_session_id: root, turn_id: turnId,
283
- hook_run_id: run.id, effect: correction ? "no_effect" : "unknown", duration_ms: run.durationMs };
284
+ hook_run_id: run.id, effect: error.details?.effect ?? "unknown", native_turn_items: { observed: false, possible_effects: true, pending: true }, duration_ms: run.durationMs };
284
285
  if (correction) {
285
286
  try { void Promise.resolve(this.journal.write("native_hook_correction", errorRecord(error))).catch(storage => { error.cleanup_error ??= errorRecord(storage); }); }
286
287
  catch (storage) { error.cleanup_error ??= errorRecord(storage); }
@@ -409,7 +410,7 @@ export class CodexBridge {
409
410
  this.send({ jsonrpc: "2.0", id: message.id, error });
410
411
  }
411
412
 
412
- request(method, params = {}, timeoutMs = this.options.timeoutMs ?? 120_000) {
413
+ request(method, params = {}, timeoutMs = this.options.timeoutMs ?? CODEX_REQUEST_TIMEOUT_MS) {
413
414
  const turnFloor = method === "turn/start" ? new Set([this.currentTurns?.get(params.threadId), ...[...this.turns].filter(([, value]) => value.threadId === params.threadId).map(([id]) => id)].filter(Boolean)) : null;
414
415
  if (method === "turn/start" && params.threadId) {
415
416
  // Only one productive root Turn is admitted by the daemon fence. Drop
@@ -576,7 +577,8 @@ export async function promptSessionWithBridge(bridge, options) {
576
577
  const started = await bridge.request("turn/start", { threadId: sessionId, input: [{ type: "text", text: prompt }], cwd, model: options.model ?? null, effort: options.reasoning ?? null, approvalPolicy: "never", sandboxPolicy: { type: "dangerFullAccess" } }, 30_000);
577
578
  const turn = started?.turn ?? started; const turnId = turn?.id ?? null;
578
579
  let completed;
579
- try { completed = await waitForTurn(bridge, sessionId, turnId, options.livenessTimeoutMs ?? options.timeoutMs ?? 1_800_000, options, cwd, clock); }
580
+ try { completed = isTerminalTurnStatus(turn?.status) ? terminalTurn({ turn }, sessionId, turnId, bridge)
581
+ : await waitForTurn(bridge, sessionId, turnId, options.livenessTimeoutMs ?? options.timeoutMs ?? 1_800_000, options, cwd, clock, turn?.status); }
580
582
  catch (error) {
581
583
  const retained = bridge.turns?.get(turnId);
582
584
  if (retained?.value?.threadId === sessionId && retained.value.turn?.id === turnId && retained.value.turn.status === "completed") {
@@ -624,11 +626,20 @@ async function ensureThreadLoaded(bridge, sessionId, options, cwd) {
624
626
  }, 30_000);
625
627
  }
626
628
 
627
- async function waitForTurn(bridge, sessionId, turnId, timeoutMs, options, cwd, clock = new ObservationClock({ timeoutMs, onGap: gap => bridge.journal?.write("observation_gap", { ...gap, session_id: sessionId, turn_id: turnId }) })) {
629
+ async function waitForTurn(bridge, sessionId, turnId, timeoutMs, options, cwd, clock = new ObservationClock({ timeoutMs, onGap: gap => bridge.journal?.write("observation_gap", { ...gap, session_id: sessionId, turn_id: turnId }) }), startStatus) {
628
630
  // app-server can briefly expose the new Turn as `interrupted` between the
629
631
  // turn/start response and its turn/started notification. Give that snapshot
630
632
  // one event-loop grace window before treating failure as authoritative.
631
633
  const failureNotBefore = Date.now() + 1_000;
634
+ const reconcileSnapshot = async (status, threadStatus) => {
635
+ if (!isAuthoritativeTerminal(bridge, turnId, status, threadStatus, failureNotBefore)) return false;
636
+ if (status === "completed" || startStatus !== "inProgress" || bridge.turns.get(turnId)?.status === "completed") return true;
637
+ // A provisional failure can contradict the accepted start before its
638
+ // delayed notification arrives. Recheck this Turn, never start another.
639
+ const current = await readTurn(bridge, sessionId, turnId, true);
640
+ return bridge.turns.get(turnId)?.status === "completed" || current?.turn?.status === status
641
+ && isAuthoritativeTerminal(bridge, turnId, status, current.threadStatus, failureNotBefore);
642
+ };
632
643
  let nextStoredReadAt = Date.now();
633
644
  while (true) {
634
645
  bridge.assertHookHealthy?.(sessionId);
@@ -636,20 +647,20 @@ async function waitForTurn(bridge, sessionId, turnId, timeoutMs, options, cwd, c
636
647
  const expired = clock.sample(bridge.activityObservations?.get(sessionId) ?? bridge.activity?.get(sessionId));
637
648
  if (expired || Date.now() >= nextStoredReadAt) {
638
649
  const stored = await readTurn(bridge, sessionId, turnId, false);
639
- if (stored?.turn && isAuthoritativeTerminal(bridge, turnId, stored.turn.status, stored.threadStatus, failureNotBefore)) return terminalTurn({ turn: stored.turn }, sessionId, turnId, bridge);
650
+ if (stored?.turn && await reconcileSnapshot(stored.turn.status, stored.threadStatus)) return terminalTurn({ turn: stored.turn }, sessionId, turnId, bridge);
640
651
  // A loaded app-server can retain a stale `active` Thread state after a
641
652
  // provider-side interruption. The persisted turn page is independent of
642
653
  // that live subscription and is the compact authority for its terminal
643
654
  // status; unlike `includeTurns`, it never hydrates the whole history.
644
655
  const persisted = await readPersistedTurn(bridge, sessionId, turnId);
645
- if (persisted && isAuthoritativeTerminal(bridge, turnId, persisted.status, stored?.threadStatus, failureNotBefore)) return terminalTurn({ turn: persisted }, sessionId, turnId, bridge);
656
+ if (persisted && await reconcileSnapshot(persisted.status, stored?.threadStatus)) return terminalTurn({ turn: persisted }, sessionId, turnId, bridge);
646
657
  // Desktop may unload an idle Thread immediately after an interrupted
647
658
  // Turn. Reload it once before polling again: otherwise the compact
648
659
  // status remains `notLoaded` forever and hides the terminal Turn.
649
660
  if (stored?.threadStatus === "notLoaded") {
650
661
  await ensureThreadLoaded(bridge, sessionId, options, cwd);
651
662
  const hydrated = await readTurn(bridge, sessionId, turnId, true);
652
- if (hydrated?.turn && isAuthoritativeTerminal(bridge, turnId, hydrated.turn.status, hydrated.threadStatus, failureNotBefore)) return terminalTurn({ turn: hydrated.turn }, sessionId, turnId, bridge);
663
+ if (hydrated?.turn && await reconcileSnapshot(hydrated.turn.status, hydrated.threadStatus)) return terminalTurn({ turn: hydrated.turn }, sessionId, turnId, bridge);
653
664
  }
654
665
  // Some app-server versions correctly omit turns for a compact read.
655
666
  // Once the thread is idle, hydrate exactly once to recover the terminal
@@ -660,7 +671,7 @@ async function waitForTurn(bridge, sessionId, turnId, timeoutMs, options, cwd, c
660
671
  // an inProgress Turn after emitting the final agent message; treating
661
672
  // that stale Turn as terminal incorrectly aborts an otherwise usable
662
673
  // worker session.
663
- if (hydrated?.turn && isAuthoritativeTerminal(bridge, turnId, hydrated.turn.status, hydrated.threadStatus, failureNotBefore)) return terminalTurn({ turn: hydrated.turn }, sessionId, turnId, bridge);
674
+ if (hydrated?.turn && await reconcileSnapshot(hydrated.turn.status, hydrated.threadStatus)) return terminalTurn({ turn: hydrated.turn }, sessionId, turnId, bridge);
664
675
  }
665
676
  // Some app-server builds emit a final agent message but omit both the
666
677
  // terminal event and the compact turn record. Wait briefly, hydrate once,
@@ -671,8 +682,9 @@ async function waitForTurn(bridge, sessionId, turnId, timeoutMs, options, cwd, c
671
682
  // app-server reports the Thread idle.
672
683
  if (final?.turnId === turnId && stored?.threadStatus === "idle" && Date.now() - final.completedAt >= 1_000) {
673
684
  const hydrated = await readTurn(bridge, sessionId, turnId, true);
674
- if (hydrated?.turn && isAuthoritativeTerminal(bridge, turnId, hydrated.turn.status, hydrated.threadStatus, failureNotBefore)) return terminalTurn({ turn: hydrated.turn }, sessionId, turnId, bridge);
675
- return terminalTurn({ turn: { id: turnId, status: "completed", synthetic: true, final_message: final.item } }, sessionId, turnId, bridge);
685
+ if (hydrated?.turn && await reconcileSnapshot(hydrated.turn.status, hydrated.threadStatus)) return terminalTurn({ turn: hydrated.turn }, sessionId, turnId, bridge);
686
+ if (hydrated?.threadStatus === "idle" && !isTerminalTurnStatus(hydrated.turn?.status))
687
+ return terminalTurn({ turn: { id: turnId, status: "completed", synthetic: true, final_message: final.item } }, sessionId, turnId, bridge);
676
688
  }
677
689
  nextStoredReadAt = Date.now() < failureNotBefore ? failureNotBefore : Date.now() + 15_000;
678
690
  }
@@ -722,7 +734,7 @@ export function turnItemEvidence(turn, bridge, sessionId) {
722
734
  possible_effects: items.some(item => classifyCodexItem(item?.type) !== "non_tool") } : { observed: false };
723
735
  }
724
736
 
725
- export { terminalCodexOverload } from "./codex-capacity-policy.mjs";
737
+ export { terminalProviderOverload as terminalCodexOverload } from "./provider-overload-policy.mjs";
726
738
 
727
739
  function isTerminalTurnStatus(status) { return ["completed", "failed", "interrupted", "cancelled"].includes(status); }
728
740
  function isAuthoritativeTerminal(bridge, turnId, status, threadStatus, failureNotBefore) {
@@ -1,6 +1,7 @@
1
1
  import { SETUP_STARTUP_MS, startupObserver, observationDetails } from './setup-progress.mjs';
2
2
  import { callDaemonObserved } from './daemon-observation.mjs';
3
3
  import { admitNativeRequestBuffer, nativeObservationError } from "./native-streams.mjs";
4
+ import { prepareDaemonRequest } from "./native-request-proof.mjs";
4
5
  import { createHash, randomUUID } from "node:crypto";
5
6
  import { chmod, lstat, mkdir, open, unlink, realpath } from "node:fs/promises";
6
7
  import { spawn } from "node:child_process";
@@ -16,7 +17,7 @@ import { prepareRuntimeOwner, cleanupFailedStart, confirmDaemonProcess, confirmD
16
17
  const REQUEST = "dd-droid/daemon-request@1", RESPONSE = "dd-droid/daemon-response@1";
17
18
  async function removeSocket(file) { try { if (!(await lstat(file)).isSocket()) throw new DroidError("unsafe_socket", "Refusing to replace non-socket daemon path"); await unlink(file); } catch (error) { if (error.code !== "ENOENT") throw error; } }
18
19
  export async function callDaemon(stateDir, operation, params = {}, timeoutMs = 30_000, operationId = process.env.DD_EVAL_OPERATION_ID ?? randomUUID()) {
19
- const request = { schema_id: REQUEST, id: operationId, operation, params };
20
+ const request = prepareDaemonRequest({ schema_id: REQUEST, id: operationId, operation, params });
20
21
  return await callDaemonObserved({ stateDir, socket: droidPaths(stateDir).socket, request, responseSchema: RESPONSE, harness: 'droid', timeoutMs });
21
22
  }
22
23
  function sameConfig(a, b) { return ["cwd", "bin", "model", "reasoning", "mode", "provider", "ddFlowBin", "ddFlowHome", "resourceHome", "projectRoot", "noFlow","inactivityMs"].every(key => a?.[key] === b[key]); }
@@ -1,3 +1,4 @@
1
+ import { managedFlowEnv } from './managed-flow-env.mjs';
1
2
  import { processSnapshot } from "./process-snapshot.mjs";
2
3
  import { ToolObservations, droidToolObservations, journalToolObservations } from "./tool-observations.mjs";
3
4
  export { processSnapshot } from "./process-snapshot.mjs";
@@ -10,7 +11,7 @@ import path from "node:path";
10
11
  import { ObservationClock, observedTimeout, validateObservationDuration } from "./observation-clock.mjs";
11
12
  import { operationProgressReporter } from "./operation-progress.mjs";
12
13
  import { writeJsonAtomic } from "./runner-events.mjs";
13
- import { errorRecord, reportedError, isObservationLoss } from "./operation-errors.mjs";
14
+ import { errorRecord, reportedError, isObservationLoss, isProvenShellCorrection } from "./operation-errors.mjs";
14
15
  import { hookBudget, invokeNativeHook } from "./native-hook-command.mjs";
15
16
  import { checkObservedProfile, observeModel } from "./model-observations.mjs";
16
17
  import { readNativeLines, streamNativeDiagnostics, nativeObservationError } from "./native-streams.mjs";
@@ -20,6 +21,8 @@ const metadataReaders = new Map();
20
21
  function metadataReader(factory) { if (!metadataReaders.has(factory)) metadataReaders.set(factory, new DroidMetadataReader(factory)); return metadataReaders.get(factory); }
21
22
  import { confirmDaemonProcess, finishDaemonProcess, scheduleDaemonProcessHeartbeat, registerDaemonProcess, stopProcessGroup } from "./managed-daemon.mjs";
22
23
  import { renderAdapterPolicy, lifecycleRetryInstruction } from "./delegation-instructions.mjs";
24
+ import { classifyProviderError, safeProviderError } from "./provider-overload-policy.mjs";
25
+ import { settledNativeTree } from "./native-children.mjs";
23
26
 
24
27
  export const DROID_CONTRACT = "dd-droid-harness@1";
25
28
  export const DROID_PROTOCOL = "1.201.0";
@@ -44,7 +47,7 @@ export function droidEnvironment(config) {
44
47
  // This adapter deliberately uses the operator's existing Factory login.
45
48
  // Undefined is a tombstone: downstream env overlays must not resurrect keys.
46
49
  for (const key of Object.keys(env)) if (key.endsWith("API_KEY")) env[key] = undefined;
47
- return env;
50
+ return { ...env, ...managedFlowEnv(config) };
48
51
  }
49
52
  export async function prepareDroidHome(config) {
50
53
  const paths = droidPaths(config.stateDir);
@@ -95,6 +98,19 @@ export async function readDroidSession(factory, id) {
95
98
  const terminal = value => ["completed", "cancelled", "failed", "process_exit", "error"].includes(value);
96
99
  const status = value => value === "completed" ? "completed" : ["cancelled", "process_exit"].includes(value) ? "cancelled" : ["failed", "error"].includes(value) ? "failed" : "unknown";
97
100
 
101
+ export function droidTerminalFailure(outcome, sessionId, settled) {
102
+ if (!["failed", "error"].includes(outcome?.reason) || !outcome.error || typeof outcome.error !== "object") return null;
103
+ const code = classifyProviderError(outcome.error);
104
+ if (code === "provider_failed") return null;
105
+ const turnId = outcome.turnId;
106
+ return Object.assign(new DroidError(code, "Droid native provider request failed", {
107
+ provider_session_id: sessionId, session_id: sessionId, turn_id: turnId, native_turn_id: turnId,
108
+ terminal_status: "failed", native_source: "droid.agent_turn_outcome", observed_at: new Date().toISOString(),
109
+ provider_error: safeProviderError(outcome.error), will_retry: outcome.error.willRetry === true, native_retry_active: outcome.error.willRetry === true,
110
+ native_turn_items: { observed: settled === true, possible_effects: true, pending: settled !== true }
111
+ }), { retryable: code === "provider_capacity_transient" || code === "provider_rate_limited" });
112
+ }
113
+
98
114
 
99
115
  function descendants(rows, rootPid) { const ids = new Set([rootPid]); for (;;) { const size = ids.size; for (const row of rows) if (ids.has(row.ppid)) ids.add(row.pid); if (size === ids.size) return rows.filter(row => ids.has(row.pid)); } }
100
116
 
@@ -360,15 +376,18 @@ export class DroidRuntime {
360
376
  });
361
377
  }
362
378
  const outcome = await completed; await this.draining;
379
+ let receipt;
363
380
  try {
364
- const receipt = await this.inspect(id, { ignoreActive: true });
365
- return { ...receipt, result: { ...outcome, status: status(outcome.reason) }, assistant_text: current.text, turn_id: turnId };
381
+ receipt = await this.inspect(id, { ignoreActive: true });
366
382
  } catch (error) {
367
383
  // A terminal native outcome is durable independently of a later
368
384
  // filesystem/topology observation. Preserve it and let settlement
369
385
  // reconcile; do not convert it into a failed prompt.
370
386
  return { provider_session_id: id, adapter_session_id: id, result: { ...outcome, status: status(outcome.reason) }, assistant_text: current.text, turn_id: turnId, settled: false, settlement_observation_error: errorRecord(error) };
371
387
  }
388
+ const providerFailure = droidTerminalFailure(outcome, id, receipt.settled);
389
+ if (providerFailure) throw providerFailure;
390
+ return { ...receipt, result: { ...outcome, status: status(outcome.reason) }, assistant_text: current.text, turn_id: turnId };
372
391
  } catch (error) {
373
392
  if (current.outcome && isObservationLoss(error)) error = Object.assign(new Error("Droid completed natively but its dispatch acknowledgement was lost", { cause: error }), {
374
393
  code: "native_outcome_observation_failed", details: { native_outcome: { provider_session_id: id, turn_id: turnId, result: current.outcome }, settled: false, observation_error: errorRecord(error) }
@@ -399,6 +418,8 @@ export class DroidRuntime {
399
418
  catch (error) {
400
419
  receipt = { provider_session_id: binding.provider_session_id, adapter_session_id: binding.provider_session_id, settled: false, settlement_observation_error: errorRecord(error) };
401
420
  }
421
+ const failure = droidTerminalFailure(outcome, binding.provider_session_id, receipt.settled);
422
+ if (failure) throw failure;
402
423
  // Native outcome and tree settlement are independent. Retain the former
403
424
  // even while children are running; settled:false keeps capacity reserved.
404
425
  const current = this.active;
@@ -448,7 +469,9 @@ export class DroidRuntime {
448
469
  const ordered = tasks.toSorted((a, b) => (a.createdAt ?? 0) - (b.createdAt ?? 0));
449
470
  const latest = ordered.at(-1); const running = tasks.some(task => !terminal(task.status));
450
471
  const closed = running && prior?.source === "adapter.tree_closed";
451
- this.topology.set(id, { provider_session_id: id, parent_provider_session_id: this.rootId, tool_use_id: creationTool, latest_tool_use_id: latest.parentToolUseId, task_id: latest.taskInvocationId, task_invocation_ids: ordered.map(task => task.taskInvocationId), status: closed ? prior.status : running ? "running" : status(latest.status), source: closed ? prior.source : "droid.task_invocations", transcript_path: native.transcript_path });
472
+ const outcome = native.outcomes.at(-1), nativeTurn = outcome && terminal(outcome.reason) && typeof outcome.turnId === "string" ? outcome.turnId : null;
473
+ const evidence = !running && nativeTurn ? { terminal_turn_id: nativeTurn, native_source: "droid.agent_turn_outcome", native_turn_items: { observed: native.complete && native.tool_calls.completeness === "complete", possible_effects: native.tool_calls.total > 0, pending: native.tool_calls.pending > 0 }, ...(outcome.error && typeof outcome.error === "object" ? { provider_error: safeProviderError(outcome.error) } : {}) } : {};
474
+ this.topology.set(id, { provider_session_id: id, parent_provider_session_id: this.rootId, tool_use_id: creationTool, latest_tool_use_id: latest.parentToolUseId, task_id: latest.taskInvocationId, task_invocation_ids: ordered.map(task => task.taskInvocationId), status: closed ? prior.status : running ? "running" : status(latest.status), source: closed ? prior.source : "droid.task_invocations", transcript_path: native.transcript_path, ...evidence });
452
475
  }
453
476
  return [...this.topology.values()];
454
477
  }
@@ -469,7 +492,8 @@ export class DroidRuntime {
469
492
  this.requireIdentity(id); await this.captureProcesses(); const children = await this.refreshTopology(); const source = await readDroidSession(this.paths.factory, id);
470
493
  const usage = []; for (const sessionId of [id, ...children.map(child => child.provider_session_id)]) usage.push(await this.snapshotUsage(sessionId, { ingest: ingestUsage }));
471
494
  const settled = !this.topologyIncomplete && (!this.active || ignoreActive) && (this.closed || this.workingState === "idle") && children.every(child => terminal(child.status));
472
- return { harness: "droid-cli", runtime_family: "droid", session: { harness_id: "droid-cli", session_id: id }, provider_session_id: id, adapter_session_id: id, cwd: this.config.cwd, observed_profile: source?.settings ? await this.observeSettings(id, source.settings, { path: source.transcript_path, sha256: source.source_sha256 }) : this.observedProfile, observed_runtime: { droid: this.config.version, factory_protocol: this.protocol ?? DROID_PROTOCOL, dd_harness_contract: DROID_CONTRACT }, status: settled ? "idle" : "running", settled, descendants: children, transcript_path: source?.transcript_path ?? null, usage, result: this.lastResult, provider_pid: this.closed ? null : this.child?.pid ?? null };
495
+ const rootSettled = (!this.active || ignoreActive) && (this.closed || this.workingState === "idle") && terminal((source?.outcomes?.at(-1) ?? this.lastResult)?.reason);
496
+ return { harness: "droid-cli", runtime_family: "droid", session: { harness_id: "droid-cli", session_id: id }, provider_session_id: id, adapter_session_id: id, cwd: this.config.cwd, observed_profile: source?.settings ? await this.observeSettings(id, source.settings, { path: source.transcript_path, sha256: source.source_sha256 }) : this.observedProfile, observed_runtime: { droid: this.config.version, factory_protocol: this.protocol ?? DROID_PROTOCOL, dd_harness_contract: DROID_CONTRACT }, status: settled ? "idle" : "running", settled, ...settledNativeTree(id, children, settled, rootSettled && !this.topologyIncomplete), terminal_turn_id: source?.outcomes?.at(-1)?.turnId ?? this.lastResult?.turnId, descendants: children, transcript_path: source?.transcript_path ?? null, usage, result: this.lastResult, provider_pid: this.closed ? null : this.child?.pid ?? null };
473
497
  }
474
498
  async observeHook(payload, eventId, deadlineAt = null) {
475
499
  try { return await this.observeHookChecked(payload, eventId, hookBudget(23_000, deadlineAt)); }
@@ -499,15 +523,14 @@ export class DroidRuntime {
499
523
  try {
500
524
  return await this.flow("event", "handle", { schema_id: "dd-flow/droid-tool-event@1", phase: event === "PreToolUse" ? "before" : "after", event_id: eventId, daemon_id: this.config.daemonId, session: { provider_session_id: id, parent_provider_session_id: parent, directory: this.config.cwd }, tool: payload.tool_name, input: payload.tool_input, profile, transcript_path: native.transcript_path, outcome: { status: payload.tool_response?.is_error ? "error" : "completed" } }, budget.remaining("admission", 15_000));
501
525
  } catch (error) {
502
- const details = error.details;
503
- if (event === "PreToolUse" && error.code === "lifecycle_shell_syntax_invalid" && details?.phase === "prepare" && details.effect === "no_effect" && details.recoverable === true && details.disposition === "pre_cli_shell_refusal" && (details.refusal_event_key || details.event_key)) {
526
+ if (event === "PreToolUse" && isProvenShellCorrection(error)) {
504
527
  const refusal = errorRecord(error);
505
528
  await this.journal("hook_rejected", { error: refusal, event_id: eventId, session_id: id });
506
529
  await this.save();
507
530
  return { continue: false, stopReason: error.message };
508
531
  }
509
532
  // Identity is proven above; only this owned admission can fail its turn.
510
- this.fatalError ??= Object.assign(error, { details: { ...error.details, provider_session_id: id, event_id: eventId, effect: "unknown" } });
533
+ this.fatalError ??= Object.assign(error, { details: { ...error.details, provider_session_id: id, event_id: eventId, effect: error.details?.effect ?? "unknown", native_turn_items: { observed: false, possible_effects: true, pending: true } } });
511
534
  this.rejectPending(this.fatalError);
512
535
  try { await budget.run(async () => { await this.save({ fatal_error: errorRecord(this.fatalError) }); await this.journal("hook_rejected", { error: errorRecord(this.fatalError) }); }, "failure_persistence"); }
513
536
  catch (storage) { this.fatalError.details.receipt_error = errorRecord(storage); }
@@ -1,7 +1,9 @@
1
+ import { managedFlowEnv } from './managed-flow-env.mjs';
1
2
  import { SETUP_STARTUP_MS, startupObserver, observationDetails } from './setup-progress.mjs';
2
3
  import { callDaemonObserved } from './daemon-observation.mjs';
3
4
  import { canonicalNativeOperation } from './adapter-timeouts.mjs';
4
5
  import { admitNativeRequestBuffer } from "./native-streams.mjs";
6
+ import { prepareDaemonRequest } from "./native-request-proof.mjs";
5
7
  import { captureDispatchGuard, cancelPendingDispatch } from "./dispatch-fence.mjs";
6
8
  import { errorRecord, isProvenShellCorrection } from "./operation-errors.mjs";
7
9
  import { ToolObservations, acpToolObservation } from "./tool-observations.mjs";
@@ -18,6 +20,8 @@ import os from "node:os";
18
20
  import path from "node:path";
19
21
 
20
22
  import { AcpBridge } from "./dd-zcode.mjs";
23
+ import { safeProviderError } from "./provider-overload-policy.mjs";
24
+ import { settledNativeTree } from "./native-children.mjs";
21
25
  import { observeModel, readModelObservations } from "./model-observations.mjs";
22
26
  import { cancelSessionWithBridge, createSessionWithBridge, doctor, forkSessionWithBridge, inspectSessionWithBridge, promptSessionWithBridge } from "./dd-grok.mjs";
23
27
  import { prepareRuntimeOwner, cleanupFailedDaemonStart, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, scheduleDaemonProcessHeartbeat, registerDaemonProcess, startManagedBridge, stopProcessGroup } from "./managed-daemon.mjs";
@@ -39,7 +43,7 @@ const errorPayload = errorRecord;
39
43
  function shellQuote(value) { return `'${String(value).replace(/'/g, "'\\''")}'`; }
40
44
 
41
45
  function executable(command, args = []) { const resolved = command ?? process.env.DD_GROK_BIN ?? "grok"; return /\.[cm]?js$/.test(resolved) ? { command: process.execPath, args: [resolved, ...args] } : { command: resolved, args }; }
42
- export function isolatedEnv(config) { const env = { ...process.env, HOME: config.grokHome, GROK_HOME: config.grokHome, ...(config.ddFlowHome ? { DD_FLOW_HOME: config.ddFlowHome } : {}), ...(config.resourceHome ? { DD_FLOW_RESOURCE_HOME: config.resourceHome } : {}), ...(config.authPath ? { GROK_AUTH_PATH: config.authPath } : {}) }; env.DD_FLOW_DAEMON_ID = undefined; if (config.ddFlowHome && config.daemonId) env.DD_FLOW_DAEMON_ID = config.daemonId; return env; }
46
+ export function isolatedEnv(config) { return { ...process.env, HOME: config.grokHome, GROK_HOME: config.grokHome, ...(config.authPath ? { GROK_AUTH_PATH: config.authPath } : {}), ...managedFlowEnv(config) }; }
43
47
  function within(candidate, root) { const resolved = path.resolve(candidate); const parent = path.resolve(root); return resolved === parent || resolved.startsWith(`${parent}${path.sep}`); }
44
48
 
45
49
  async function writeIsolatedConfig(paths) {
@@ -151,7 +155,7 @@ async function materializeSessionArchive(paths, archiveDir) {
151
155
 
152
156
  export async function callDaemon(stateDir, operation, params = {}, timeoutMs = 30_000) {
153
157
  operation = canonicalNativeOperation('grok', operation);
154
- const request = { schema_id: REQUEST_SCHEMA, id: process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation, params };
158
+ const request = prepareDaemonRequest({ schema_id: REQUEST_SCHEMA, id: process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation, params });
155
159
  return await callDaemonObserved({ stateDir, socket: locations(stateDir).socket, request, responseSchema: RESPONSE_SCHEMA, harness: 'grok', timeoutMs });
156
160
  }
157
161
 
@@ -239,6 +243,10 @@ export class Runtime {
239
243
  parent_provider_session_id: parent,
240
244
  status,
241
245
  source,
246
+ ...(terminal(status) && typeof attempt === "string" && attempt ? { terminal_turn_id: attempt,
247
+ ...(typeof value.observed_at === "string" ? { terminal_observed_at: value.observed_at } : {}),
248
+ ...(value.provider_error && typeof value.provider_error === "object" ? { provider_error: safeProviderError(value.provider_error), native_source: "grok.subagent.event" } : {}),
249
+ ...(value.native_turn_items && typeof value.native_turn_items === "object" ? { native_turn_items: value.native_turn_items } : {}) } : {}),
242
250
  ...(attempt || descendant?.attempt_id ? { attempt_id: attempt ?? descendant.attempt_id } : {}),
243
251
  ...(newAttempt && descendant?.attempt_id ? { retired_attempt_ids: [...retired, descendant.attempt_id] } : retired.length ? { retired_attempt_ids: retired } : {}),
244
252
  ...(!newAttempt && !terminal(status) && descendant?.outcome_error ? { outcome_error: descendant.outcome_error } : {})
@@ -408,7 +416,7 @@ export class Runtime {
408
416
  if (operation === "session.create") return await this.productive(operation, async () => { const result = await createSessionWithBridge(this.bridge, { ...this.options(params), onSessionCreated: async (session) => { this.track(session); this.loadedSessionId = session.provider_session_id; await this.persist(); } }, this.initialized); this.loadedSessionId = result.provider_session_id; return result; }, null, operationId);
409
417
  if (operation === "session.prompt") { await this.requireSettled(); return await this.productive(operation, async () => { const result = await promptSessionWithBridge(this.bridge, { ...this.options(params), assertDispatch, operationId }); this.loadedSessionId = params.sessionId; return result; }, params.sessionId, operationId); }
410
418
  if (operation === "session.fork") { await this.requireSettled(); return await this.productive(operation, async () => { const result = await forkSessionWithBridge(this.bridge, this.options(params)); this.loadedSessionId = params.sessionId; return result; }); }
411
- if (operation === "session.inspect" || operation === "session.resume") { const before = new Map(this.descendants); const result = await inspectSessionWithBridge(this.bridge, this.options({ ...params, liveSession: true })); this.track(result, before); await this.persist(); return { ...result, settled: !this.active && !this.treeRunning(result), descendants: this.topology(result.provider_session_id) }; }
419
+ if (operation === "session.inspect" || operation === "session.resume") { const before = new Map(this.descendants); const result = await inspectSessionWithBridge(this.bridge, this.options({ ...params, liveSession: true })); this.track(result, before); await this.persist(); const settled = !this.active && !this.treeRunning(result); const descendants = this.topology(result.provider_session_id); const terminal = this.sessions.get(result.provider_session_id)?.native_root_receipt; return { ...result, settled, descendants, ...settledNativeTree(result.provider_session_id, descendants, settled, !this.active && !this.rootRunning(result)), ...(terminal?.kind === "failed" && terminal.request_id !== undefined ? { terminal_turn_id: String(terminal.request_id) } : {}) }; }
412
420
  if (operation === "session.cancel") { if (!this.sessions.has(params.sessionId)) throw new DaemonError("session_identity_mismatch", "cancellation Session is not owned by this daemon"); cancelPendingDispatch(this); const result = await cancelSessionWithBridge(this.bridge, this.options({ ...params, liveSession: true })); await this.persist({ active_tree: Boolean(this.active) || !result.settled }); return result; }
413
421
  if (operation === "session.archive") { await this.requireSettled(); return await this.productive(operation, async () => { let session = this.sessions.get(params.sessionId); if (!session?.cwd) { const result = await inspectSessionWithBridge(this.bridge, this.options({ ...params, liveSession: true })); this.track(result); session = this.sessions.get(result.provider_session_id); } return await exportSessionArchive(this.paths, session.provider_session_id, absolute(session.cwd, "Grok Build session cwd"), params.archiveDir, this.state.versions); }); }
414
422
  if (operation === "daemon.stop") {
@@ -1,6 +1,8 @@
1
+ import { managedFlowEnv } from './managed-flow-env.mjs';
1
2
  import { SETUP_STARTUP_MS, startupObserver, observationDetails } from './setup-progress.mjs';
2
3
  import { callDaemonObserved } from './daemon-observation.mjs';
3
4
  import { admitNativeRequestBuffer } from "./native-streams.mjs";
5
+ import { prepareDaemonRequest } from "./native-request-proof.mjs";
4
6
  import { captureDispatchGuard, cancelPendingDispatch } from "./dispatch-fence.mjs";
5
7
  import { openCodeToolObservation, journalToolObservations } from "./tool-observations.mjs";
6
8
  import { ObservationClock } from "./observation-clock.mjs";
@@ -10,13 +12,15 @@ import { spawn } from "node:child_process";
10
12
  import net from "node:net";
11
13
  import { durableDaemonDispatch, completeDaemonShutdown, trackDaemonConnections, retireDaemonEndpoint, retireFailedDaemon, assertDaemonDispatch, assertDaemonRestartable } from "./daemon-operations.mjs";
12
14
  import { waitForSettlement } from "./session-settlement.mjs";
13
- import { errorRecord } from "./operation-errors.mjs";
15
+ import { errorRecord, isProvenShellCorrection, reportedError } from "./operation-errors.mjs";
14
16
  import { checkObservedProfile, observeModel } from "./model-observations.mjs";
15
17
  import os from "node:os";
16
18
  import path from "node:path";
17
19
  import { parse as parseJsonc } from "jsonc-parser";
18
20
  import { writeJsonAtomic } from "./runner-events.mjs";
19
21
  import { OpenCodeClient, OpenCodeError, historyDigest, usageSnapshot, nativeModel } from "./dd-opencode.mjs";
22
+ import { safeProviderError } from "./provider-overload-policy.mjs";
23
+ import { acceptNativeHookRequest, hookBudget, readNativeHookFailures } from "./native-hook-command.mjs";
20
24
  import { prepareRuntimeOwner, cleanupFailedStart, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, scheduleDaemonProcessHeartbeat, registerDaemonProcess, stopProcessGroup } from "./managed-daemon.mjs";
21
25
 
22
26
  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";
@@ -40,13 +44,13 @@ const writeJson = writeJsonAtomic;
40
44
  async function readState(dir) { try { return JSON.parse(await readFile(locations(dir).state, "utf8")); } catch (error) { if (error.code === "ENOENT") return null; throw error; } }
41
45
  async function removeSocket(paths) { try { if (!(await lstat(paths.socket)).isSocket()) throw new DaemonError("unsafe_socket", "refusing to replace non-socket daemon path"); await unlink(paths.socket); } catch (error) { if (error.code !== "ENOENT") throw error; } }
42
46
  function executable(bin, args = []) { return /\.[cm]?js$/.test(bin) ? { command: process.execPath, args: [bin, ...args] } : { command: bin, args }; }
43
- function isolatedEnv(config) {
47
+ export function isolatedEnv(config) {
44
48
  const env = { ...process.env };
45
49
  for (const name of ["OPENCODE_CONFIG", "OPENCODE_CONFIG_CONTENT", "OPENCODE_PASSWORD", "OPENCODE_PTY_HANDOFF", "OPENCODE_SERVER", "OPENCODE_SIMULATE"]) delete env[name];
46
50
  return { ...env, XDG_DATA_HOME: config.dataHome, XDG_CONFIG_HOME: config.configHome, XDG_CACHE_HOME: config.cacheHome,
47
51
  XDG_STATE_HOME: config.stateHome, OPENCODE_DB: config.database, OPENCODE_CONFIG_DIR: config.adapterDir,
48
52
  OPENCODE_CONFIG_PROJECT_DISABLE: "1", OPENCODE_SERVER_PASSWORD: config.password,
49
- ...(config.ddFlowHome ? { DD_FLOW_HOME: config.ddFlowHome } : {}), ...(config.resourceHome ? { DD_FLOW_RESOURCE_HOME: config.resourceHome } : {}),
53
+ ...managedFlowEnv(config),
50
54
  DD_OPENCODE_PLUGIN_SHA256: config.pluginSha256 ?? "none" };
51
55
  }
52
56
  export async function run(bin, args, config, options = {}) {
@@ -63,7 +67,7 @@ const errorPayload = errorRecord;
63
67
 
64
68
  async function writePlugin(paths, config) {
65
69
  const directory = path.join(paths.config, "opencode", "plugins"); await mkdir(directory, { recursive: true, mode: 0o700 });
66
- const source = `import { makeOpenCodePlugin } from ${JSON.stringify(new URL("./opencode-plugin.mjs", import.meta.url).href)};\nexport default makeOpenCodePlugin(${JSON.stringify({ daemonId: config.daemonId, version: config.version, ddFlowBin: config.ddFlowBin, nodeBin: process.execPath, ddFlowHome: config.ddFlowHome, projectRoot: config.projectRoot })});\n`;
70
+ 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`;
67
71
  const checksum = createHash("sha256").update(source).digest("hex"); await writeFile(path.join(directory, "dd-flow.js"), source, { mode: 0o600 }); return checksum;
68
72
  }
69
73
  async function prepare(paths, config, authSource) {
@@ -136,7 +140,7 @@ async function importArchive(config, client) {
136
140
  export async function callDaemon(stateDir, operation, params = {}, timeoutMs = 30_000) {
137
141
  const state = await readState(stateDir);
138
142
  if (state && state.schema_id !== STATE_SCHEMA) throw new DaemonError("legacy_opencode_state", "Use archive/opencode-v1-final-beta134 for v1 states; the v2 adapter cannot dispatch to a legacy daemon", false, { effect: "no_effect" });
139
- const request = { schema_id: REQUEST_SCHEMA, id: process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation, params };
143
+ const request = prepareDaemonRequest({ schema_id: REQUEST_SCHEMA, id: process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation, params });
140
144
  return await callDaemonObserved({ stateDir, socket: locations(stateDir).socket, request, responseSchema: RESPONSE_SCHEMA, harness: 'opencode', timeoutMs });
141
145
  }
142
146
  function sameConfig(state, config) { return ["cwd","journal","bin","provider","model","variant","agent","projectRoot","ddFlowBin","ddFlowHome","sessionArchive","resourceHome","noFlow","configSource","authSource"].every(key => (state.config?.[key] ?? null) === (config[key] ?? null)); }
@@ -187,13 +191,15 @@ export class Runtime {
187
191
  }
188
192
  async describe(id) {
189
193
  const [info, messages, children, statuses] = await Promise.all([this.client.inspect(id), this.client.messages(id), this.childTree(id), this.client.statuses()]);
190
- if (!info.directory || path.resolve(info.directory) !== path.resolve(this.state.config.cwd)) throw new DaemonError("foreign_session", "OpenCode Session directory is outside the controlled workspace");
191
194
  const parent = info.fork ? null : info.parentID ?? null, existing = this.sessions.get(id);
195
+ if (info.id !== id || !existing && (!parent || !this.sessions.has(parent) || info.fork)
196
+ || existing && (existing.parent_provider_session_id ?? null) !== parent) throw new DaemonError("foreign_session", "OpenCode Session is not in the retained owned tree");
192
197
  const record = { provider_session_id: id, adapter_session_id: id, parent_provider_session_id: parent,
193
198
  root_provider_session_id: parent ? this.sessions.get(parent)?.root_provider_session_id ?? parent : existing?.root_provider_session_id ?? id, cwd: info.directory };
194
199
  this.sessions.set(id, record);
195
200
  for (const child of children) {
196
- if (!child.directory || path.resolve(child.directory) !== path.resolve(this.state.config.cwd)) throw new DaemonError("foreign_session", "OpenCode child has another workspace");
201
+ const retained = this.sessions.get(child.id);
202
+ if (child.fork || retained && retained.parent_provider_session_id !== child.parentID) throw new DaemonError("session_identity_mismatch", "OpenCode child ancestry changed");
197
203
  this.sessions.set(child.id, { provider_session_id: child.id, parent_provider_session_id: child.parentID, root_provider_session_id: record.root_provider_session_id, cwd: child.directory });
198
204
  }
199
205
  const pending = new Map();
@@ -202,6 +208,18 @@ export class Runtime {
202
208
  pending.set(session, { ...value, shells: this.client.shells ? (await this.client.shells(session)).filter(shell => shell.status === "running") : [] });
203
209
  }
204
210
  const busy = session => Boolean(statuses[session] && statuses[session].type !== "idle") || Object.values(pending.get(session)).some(items => items.length > 0);
211
+ for (const child of children) {
212
+ const history = await this.client.messages(child.id);
213
+ const index = history.findLastIndex(message => message.info.role === "user" && message.info.sessionID === child.id);
214
+ const terminal = index >= 0 && history.slice(index + 1).findLast(message => message.info.role === "idle" && message.info.sessionID === child.id);
215
+ if (!terminal || busy(child.id)) continue;
216
+ const outcome = terminal.info.outcome;
217
+ if (!["succeeded", "failed", "interrupted"].includes(outcome)) continue;
218
+ Object.assign(child, { status: outcome === "succeeded" ? "completed" : outcome === "failed" ? "failed" : "cancelled",
219
+ terminal_turn_id: history[index].info.id, native_source: "opencode.v2.session.execution",
220
+ native_turn_items: { observed: true, possible_effects: true, pending: false },
221
+ ...(terminal.info.error ? { provider_error: safeProviderError(terminal.info.error) } : {}) });
222
+ }
205
223
  const unsettledChildren = children.filter(child => busy(child.id)).map(child => child.id), status = busy(id) ? "busy" : "idle";
206
224
  const lastAssistant = messages.findLast(message => message.info?.role === "assistant");
207
225
  const inputIndex = messages.findLastIndex(message => message.info.role === "user");
@@ -212,6 +230,8 @@ export class Runtime {
212
230
  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);
213
231
  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 },
214
232
  assistant_text: (lastAssistant?.parts ?? []).filter(part => part.type === "text").map(part => part.text ?? "").join(""), info, status,
233
+ 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" },
234
+ 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" })),
215
235
  settled: status === "idle" && !unsettledChildren.length, unsettled_children: unsettledChildren,
216
236
  tree_session_ids: treeIds, unsettled_sessions: treeIds.filter(busy),
217
237
  tree_observations: treeIds.map(session => ({ provider_session_id: session, status: busy(session) ? "running" : "idle", settled: !busy(session) })),
@@ -235,6 +255,7 @@ export class Runtime {
235
255
  }
236
256
  async requireSettled(id) {
237
257
  const value = await this.describe(id);
258
+ if (!value.route_conformance.matches) throw new DaemonError("invocation_directory_mismatch", "Owned OpenCode Session has an unexpected workspace", false, { effect: "no_effect", ...value.route_violations[0], native_outcome: value.terminal_status, settled: value.settled });
238
259
  if (!value.settled) throw new DaemonError("tree_not_settled", "OpenCode Session is not idle", false, { session_id: id, status: value.status });
239
260
  const config = this.state.config;
240
261
  if (config.provider && config.model) checkObservedProfile({ ...config, reasoning: config.variant, mode: value.parent_provider_session_id ? "dd-flow-worker" : config.agent }, value.observed_profile);
@@ -254,30 +275,87 @@ export class Runtime {
254
275
  } catch (cause) {
255
276
  const error = completed ? new DaemonError("operation_observation_lost", "OpenCode returned but its durable observation could not be saved; do not resend", false, { result, cause: errorPayload(cause) }) : cause;
256
277
  const failures = [];
257
- try { await this.persist({ active_tree: error.details?.effect === "no_effect" ? priorActive : true, active_operation: null }); } catch (secondary) { failures.push(errorPayload(secondary)); }
278
+ try { await this.persist({ active_tree: error.details?.effect === "no_effect" && error.details?.native_turn_items?.possible_effects !== true ? priorActive : true, active_operation: null }); } catch (secondary) { failures.push(errorPayload(secondary)); }
258
279
  try { await this.journal(`${name}.failed`, null); } catch (secondary) { failures.push(errorPayload(secondary)); }
259
280
  if (failures.length) error.details = { ...error.details, observation_errors: failures };
260
281
  throw error;
261
282
  } finally { this.active = null; }
262
283
  }
263
284
  async cancelTree(id, seen = new Set()) {
285
+ if (!this.sessions.has(id)) throw new DaemonError("foreign_session", "cannot cancel a Session without daemon ownership");
264
286
  if (seen.has(id)) return [];
287
+ const native = await this.client.inspect(id), retained = this.sessions.get(id);
288
+ 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");
265
289
  seen.add(id);
266
290
  const errors = []; let children = [];
267
291
  try { children = await this.client.children(id); } catch (error) { errors.push({ session_id: id, phase: "children", ...errorPayload(error) }); }
268
292
  for (const child of children) {
269
- if (child.parentID !== id || !child.directory || path.resolve(child.directory) !== path.resolve(this.state.config.cwd)) {
293
+ if (child.parentID !== id || child.fork || this.sessions.has(child.id) && this.sessions.get(child.id).parent_provider_session_id !== id) {
270
294
  errors.push({ session_id: child.id, phase: "identity", code: "session_identity_mismatch" }); continue;
271
295
  }
272
- errors.push(...await this.cancelTree(child.id, seen));
296
+ this.sessions.set(child.id, { provider_session_id: child.id, parent_provider_session_id: id, root_provider_session_id: this.sessions.get(id)?.root_provider_session_id ?? id, cwd: child.directory });
297
+ try { errors.push(...await this.cancelTree(child.id, seen)); }
298
+ catch (error) { errors.push({ session_id: child.id, phase: "cancel_child", ...errorPayload(error) }); }
273
299
  }
274
300
  try { await this.client.abort(id); } catch (error) { errors.push({ session_id: id, phase: "abort", ...errorPayload(error) }); }
275
301
  return errors;
276
302
  }
303
+ async observeHookFailures(current = this.nativeHookOperation) {
304
+ if (!current || this.nativeHookOperation !== current) return;
305
+ if (current.reading) return await current.reading;
306
+ current.reading = readNativeHookFailures(this.paths.dir, current.requests, current, error => this.retainHookFailure(current, error));
307
+ try { await current.reading; } finally { current.reading = null; }
308
+ }
309
+ retainHookFailure(current, error) {
310
+ if (this.nativeHookOperation !== current || isProvenShellCorrection(error)) return;
311
+ error.details = { ...error.details, provider_session_id: current.sessionId, native_outcome: "unknown", native_turn_items: { observed: true, possible_effects: true, pending: true } };
312
+ current.failure ??= error;
313
+ }
277
314
  async dispatch(operation, params, operationId = null) {
278
315
  const assertDispatch = captureDispatchGuard(this);
279
316
  if (["session.create", "session.prompt", "session.start", "session.fork", "session.resume", "session.archive"].includes(operation)) assertDaemonDispatch(this.paths.dir);
280
317
  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 }; }
318
+ if (operation === "hook.resolve") {
319
+ const current = this.nativeHookOperation, sessionId = params.sessionId, payload = params.payload;
320
+ 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))
321
+ throw new DaemonError("native_hook_unproven", "OpenCode hook has no exact active operation");
322
+ let owned = sessionId === current.sessionId || this.sessions.get(sessionId)?.root_provider_session_id === current.sessionId;
323
+ try {
324
+ const tree = await this.childTree(current.sessionId);
325
+ 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");
326
+ owned = true;
327
+ const native = await this.client.inspect(sessionId), retained = this.sessions.get(sessionId), child = tree.find(item => item.id === sessionId);
328
+ if (native.id !== sessionId || child && (native.parentID !== child.parentID || native.fork) || retained && (retained.parent_provider_session_id ?? null) !== (native.fork ? null : native.parentID ?? null)) throw new DaemonError("native_hook_identity_conflict", "OpenCode hook Session ancestry changed");
329
+ if (!native.directory) throw new DaemonError("native_hook_unproven", "OpenCode hook Session directory is unavailable");
330
+ if (payload.input.workdir !== undefined && typeof payload.input.workdir !== "string") throw new DaemonError("native_hook_unproven", "OpenCode hook workdir must be a string");
331
+ if (this.nativeHookOperation !== current) throw new DaemonError("native_hook_unproven", "OpenCode operation changed during hook identity observation");
332
+ const accepted = await acceptNativeHookRequest(this.paths.dir, { daemon_id: this.state.daemon_id, operation_id: current.operationId, turn_generation: current.generation,
333
+ 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",
334
+ command_sha256: createHash("sha256").update(payload.input.command).digest("hex"), native_cwd: path.resolve(native.directory, payload.input.workdir ?? ".") }, current);
335
+ if (this.nativeHookOperation !== current) throw new DaemonError("native_hook_unproven", "OpenCode operation changed during hook acceptance");
336
+ current.requests.set(accepted.request_id, { current, binding: accepted }); return { nativeHookBinding: accepted, nativeSession: {
337
+ id: native.id, directory: native.directory, parentID: native.parentID, projectID: native.projectID,
338
+ model: { providerID: native.model?.providerID, id: native.model?.id, variant: native.model?.variant }
339
+ } };
340
+ } catch (error) {
341
+ if (owned) this.retainHookFailure(current, error);
342
+ throw error;
343
+ }
344
+ }
345
+ if (operation === "hook.verify") {
346
+ const current = this.nativeHookOperation, binding = params.binding;
347
+ if (!current || binding?.operation_id !== current.operationId || binding?.turn_generation !== current.generation
348
+ || 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");
349
+ return { current: true };
350
+ }
351
+ if (operation === "hook.failed") {
352
+ if (params.failure) {
353
+ await this.dispatch("hook.verify", { binding: params.binding });
354
+ 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");
355
+ this.retainHookFailure(this.nativeHookOperation, reportedError(params.failure, "Native hook failed"));
356
+ }
357
+ await this.observeHookFailures(); return { observed: true };
358
+ }
281
359
  if (operation === "daemon.doctor") {
282
360
  const info = await this.client.health();
283
361
  await this.client.call("model.list", { location: { directory: this.state.config.cwd } });
@@ -309,26 +387,33 @@ export class Runtime {
309
387
  if (operation === "session.inspect" || operation === "session.resume") { const result = await this.describe(params.sessionId); await this.persist(); return result; }
310
388
  if (operation === "session.prompt") { await this.requireSettled(params.sessionId); return this.productive(operation, async () => {
311
389
  this.state.operations ??= {}; const key = operationId ?? randomUUID(), retained = this.state.operations[key];
312
- assertDispatch(); const reply = await this.client.prompt(params.sessionId, { prompt: params.prompt, messageId: retained?.message_id ?? `msg_${randomUUID().replaceAll("-", "")}`, retained,
390
+ const current = { operationId: key, sessionId: params.sessionId, generation: (this.state.hook_generation ?? 0) + 1, requests: new Map(), failure: null };
391
+ this.state.hook_generation = current.generation; this.nativeHookOperation = current;
392
+ 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; };
393
+ let reply;
394
+ try { assertDispatch(); reply = await this.client.prompt(params.sessionId, { prompt: params.prompt, messageId: retained?.message_id ?? `msg_${randomUUID().replaceAll("-", "")}`, retained,
313
395
  save: async receipt => { this.state.operations[key] = receipt; await this.persist(); },
314
- assertDispatch, livenessTimeoutMs: params.livenessTimeoutMs ?? this.client.timeoutMs,
396
+ assertDispatch, checkHookFailures, livenessTimeoutMs: params.livenessTimeoutMs ?? this.client.timeoutMs,
315
397
  onDispatched: () => this.journal("native_dispatch", { operation_id: operationId, provider_session_id: params.sessionId }),
316
398
  onModelMessages: (id, messages, parent) => this.observeModels(id, messages, parent),
317
399
  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" })
318
- }); const assistant_text = reply.parts.filter(part => part.type === "text").map(part => part.text ?? "").join("");
400
+ }); await checkHookFailures(); }
401
+ finally { current.requests.clear(); if (this.nativeHookOperation === current) this.nativeHookOperation = null; }
402
+ const assistant_text = reply.parts.filter(part => part.type === "text").map(part => part.text ?? "").join("");
319
403
  const expectedAgent = this.sessions.get(params.sessionId)?.parent_provider_session_id ? "dd-flow-worker" : this.state.config.agent;
320
404
  const reply_profile = { provider: reply.info.providerID ?? null, model: reply.info.modelID ?? null, reasoning: reply.info.variant ?? null, mode: reply.info.mode ?? null };
321
405
  checkObservedProfile({ ...this.state.config, reasoning: this.state.config.variant, mode: expectedAgent }, reply_profile);
322
406
  let result;
323
407
  try { result = await this.describe(params.sessionId); }
324
- catch (error) { return { provider_session_id: params.sessionId, adapter_session_id: params.sessionId, assistant_text, response_message_id: reply.info.id, observed_profile: reply_profile, settled: false, settlement_observation_error: errorPayload(error) }; }
408
+ catch (error) { return { provider_session_id: params.sessionId, adapter_session_id: params.sessionId, assistant_text, response_message_id: reply.info.id, terminal_turn_id: reply.admission_message_id, terminal_status: "completed", native_outcome: "succeeded", observed_profile: reply_profile, settled: false, settlement_observation_error: errorPayload(error) }; }
325
409
  result.assistant_text = assistant_text;
410
+ 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 });
326
411
  result.response_message_id = reply.info.id;
327
412
  result.observed_profile = { ...reply_profile, mode: result.info.agent ?? reply_profile.mode };
328
413
  checkObservedProfile({ ...this.state.config, reasoning: this.state.config.variant, mode: expectedAgent }, result.observed_profile); result.usage_ingest_errors = await this.reconcileUsage(result); return result; }); }
329
414
  if (operation === "session.fork") { const source = await this.requireSettled(params.sessionId); return this.productive(operation, async () => {
330
415
  const info = await this.client.fork(params.sessionId, params.messageId);
331
- const created = { provider_session_id: info.id, adapter_session_id: info.id, parent_provider_session_id: info.parentID ?? params.sessionId, root_provider_session_id: this.sessions.get(params.sessionId)?.root_provider_session_id ?? params.sessionId, cwd: info.directory };
416
+ const created = { provider_session_id: info.id, adapter_session_id: info.id, parent_provider_session_id: info.fork ? null : info.parentID ?? params.sessionId, root_provider_session_id: info.fork ? info.id : this.sessions.get(params.sessionId)?.root_provider_session_id ?? params.sessionId, cwd: info.directory };
332
417
  this.sessions.set(info.id, created);
333
418
  try {
334
419
  const fork = await this.describe(info.id);