@deksden-com/dd-flow-cli 0.9.0-beta.136 → 0.9.0-beta.138

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 (230) hide show
  1. package/CHANGELOG.md +12 -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 +56 -3
  7. package/dist/cli/command-option-descriptions.js +5 -0
  8. package/dist/cli/executor-onboarding.js +8 -3
  9. package/dist/cli/flows.js +29 -0
  10. package/dist/cli/help.js +240 -6
  11. package/dist/cli/input-preparation.js +54 -5
  12. package/dist/cli/memory-flow-admission.js +44 -0
  13. package/dist/cli/memory-init-invocation.js +78 -0
  14. package/dist/cli/memory-init.js +240 -0
  15. package/dist/cli/memory-storage-admission.js +53 -0
  16. package/dist/cli/native-hook-ingress.js +1 -1
  17. package/dist/cli/response-file.js +2 -0
  18. package/dist/cli/run-cli.js +283 -160
  19. package/dist/cli/setup.js +9 -7
  20. package/dist/cli.js +54 -2
  21. package/dist/domain/stage-handler-identities.js +20 -0
  22. package/dist/harness-runtime/DD-ZCODE.md +13 -0
  23. package/dist/harness-runtime/bin/dd-opencode.mjs +3 -3
  24. package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
  25. package/dist/harness-runtime/lib/daemon-operations.mjs +4 -3
  26. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +4 -2
  27. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +11 -5
  28. package/dist/harness-runtime/lib/dd-codex.mjs +22 -10
  29. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +2 -1
  30. package/dist/harness-runtime/lib/dd-droid.mjs +2 -1
  31. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +4 -2
  32. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +20 -5
  33. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +45 -10
  34. package/dist/harness-runtime/lib/dd-zcode.mjs +7 -3
  35. package/dist/harness-runtime/lib/delegation-instructions.mjs +5 -3
  36. package/dist/harness-runtime/lib/lifecycle-command.mjs +1 -0
  37. package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -0
  38. package/dist/harness-runtime/lib/managed-daemon.mjs +50 -15
  39. package/dist/harness-runtime/lib/managed-flow-env.mjs +15 -0
  40. package/dist/harness-runtime/lib/native-hook-command.mjs +1 -1
  41. package/dist/harness-runtime/lib/native-request-proof.d.mts +2 -0
  42. package/dist/harness-runtime/lib/native-request-proof.mjs +65 -0
  43. package/dist/harness-runtime/lib/opencode-plugin.mjs +5 -1
  44. package/dist/harness-runtime/lib/operation-progress.mjs +3 -1
  45. package/dist/runtime/context.js +1 -1
  46. package/dist/schemas/discovery-result.schema.json +184 -0
  47. package/dist/schemas/execution-bundle.schema.json +205 -0
  48. package/dist/schemas/execution-settings.schema.json +162 -0
  49. package/dist/schemas/flow-descriptor.schema.json +54 -0
  50. package/dist/schemas/flow-run.schema.json +22 -0
  51. package/dist/schemas/harness-catalog.schema.json +80 -0
  52. package/dist/schemas/mb-init-compose-result.schema.json +457 -0
  53. package/dist/schemas/mb-init-discover-result.schema.json +500 -0
  54. package/dist/schemas/mb-init-finish-result.schema.json +422 -0
  55. package/dist/schemas/mb-init-preflight-result.schema.json +367 -0
  56. package/dist/schemas/mb-init-review-result.schema.json +396 -0
  57. package/dist/schemas/mb-init-verify-result.schema.json +391 -0
  58. package/dist/schemas/memory-bootstrap-receipt-v2.schema.json +67 -0
  59. package/dist/schemas/memory-bootstrap-receipt.schema.json +354 -0
  60. package/dist/schemas/memory-composition-work-result.schema.json +38 -0
  61. package/dist/schemas/memory-repair-work-result.schema.json +49 -0
  62. package/dist/schemas/memory-review-correction-plan.schema.json +30 -0
  63. package/dist/schemas/memory-review-work-result.schema.json +43 -0
  64. package/dist/schemas/model-catalog.schema.json +153 -0
  65. package/dist/schemas/profile-settings.schema.json +122 -0
  66. package/dist/schemas/release-manifest.schema.json +291 -0
  67. package/dist/schemas/run-engine-binding.schema.json +1 -1
  68. package/dist/services/check-runner-evidence.js +51 -4
  69. package/dist/services/cli-operation-classifier.js +8 -47
  70. package/dist/services/code-checks.js +387 -34
  71. package/dist/services/controller-child-observer.js +99 -0
  72. package/dist/services/controller-execution.js +88 -0
  73. package/dist/services/controller-fanout.js +79 -13
  74. package/dist/services/dashboard.js +8 -2
  75. package/dist/services/descriptor-coordinator-session.js +35 -0
  76. package/dist/services/descriptor-root-work.js +53 -0
  77. package/dist/services/descriptor-run-settlement.js +105 -0
  78. package/dist/services/descriptor-run-start.js +449 -0
  79. package/dist/services/descriptor-stage-coordinator.js +197 -0
  80. package/dist/services/descriptor-stage-settlement.js +222 -0
  81. package/dist/services/descriptor-stage-start.js +139 -0
  82. package/dist/services/engines.js +39 -22
  83. package/dist/services/eval-snapshots.js +83 -25
  84. package/dist/services/execution-bundles.js +263 -0
  85. package/dist/services/execution-policy.js +20 -16
  86. package/dist/services/external-work-launch.js +91 -11
  87. package/dist/services/flow-catalog.js +21 -0
  88. package/dist/services/flow-descriptors.js +82 -0
  89. package/dist/services/frozen-engine.js +242 -0
  90. package/dist/services/generic-execution.js +164 -0
  91. package/dist/services/harness-adapter.js +28 -2
  92. package/dist/services/hooks.js +24 -6
  93. package/dist/services/hybrid-contracts.js +31 -3
  94. package/dist/services/ids.js +29 -1
  95. package/dist/services/installation-lock.js +7 -4
  96. package/dist/services/installed-flow-source.js +183 -0
  97. package/dist/services/lifecycle-invocations.js +257 -78
  98. package/dist/services/memory-accepted-composition.js +77 -0
  99. package/dist/services/memory-baseline-preservation.js +65 -0
  100. package/dist/services/memory-completed-correction-wave.js +320 -0
  101. package/dist/services/memory-compose-handler.js +258 -0
  102. package/dist/services/memory-composition-acceptance.js +199 -0
  103. package/dist/services/memory-composition-graph.js +134 -0
  104. package/dist/services/memory-composition-registration.js +146 -0
  105. package/dist/services/memory-composition-work-custody.js +151 -0
  106. package/dist/services/memory-composition-work-results.js +96 -0
  107. package/dist/services/memory-corrected-review-acceptance.js +129 -0
  108. package/dist/services/memory-corrected-review-batch.js +30 -0
  109. package/dist/services/memory-corrected-review-findings.js +106 -0
  110. package/dist/services/memory-corrected-review-inputs.js +210 -0
  111. package/dist/services/memory-corrected-review-projection.js +78 -0
  112. package/dist/services/memory-corrected-review-receipt.js +170 -0
  113. package/dist/services/memory-corrected-review-registration.js +191 -0
  114. package/dist/services/memory-correction-check-receipt.js +219 -0
  115. package/dist/services/memory-correction-input-owner.js +6 -0
  116. package/dist/services/memory-discover-handler.js +104 -0
  117. package/dist/services/memory-finish-evidence.js +435 -0
  118. package/dist/services/memory-finish-handler.js +215 -0
  119. package/dist/services/memory-finish-integration.js +280 -0
  120. package/dist/services/memory-finish-readiness.js +122 -0
  121. package/dist/services/memory-git-integration.js +151 -0
  122. package/dist/services/memory-historical-corrected-review.js +206 -0
  123. package/dist/services/memory-historical-original-review.js +130 -0
  124. package/dist/services/memory-historical-repair-registration.js +99 -0
  125. package/dist/services/memory-historical-second-review.js +128 -0
  126. package/dist/services/memory-init-bootstrap-readiness.js +210 -0
  127. package/dist/services/memory-init-contexts.js +89 -0
  128. package/dist/services/memory-init-controller-start.js +84 -0
  129. package/dist/services/memory-init-engine-release.js +223 -0
  130. package/dist/services/memory-init-git-policy.js +136 -0
  131. package/dist/services/memory-init-lint-capability.js +185 -0
  132. package/dist/services/memory-init-native-capacity.js +241 -0
  133. package/dist/services/memory-init-preflight.js +312 -0
  134. package/dist/services/memory-init-project-bootstrap.js +599 -0
  135. package/dist/services/memory-init-request-candidate.js +157 -0
  136. package/dist/services/memory-init-request-input.js +66 -0
  137. package/dist/services/memory-init-resource-storage.js +125 -0
  138. package/dist/services/memory-init-run-allocation.js +81 -0
  139. package/dist/services/memory-init-run-bundle.js +98 -0
  140. package/dist/services/memory-init-start.js +175 -0
  141. package/dist/services/memory-init-startup-custody.js +80 -0
  142. package/dist/services/memory-init-startup-input.js +240 -0
  143. package/dist/services/memory-init-storage.js +81 -0
  144. package/dist/services/memory-init-stores.js +359 -0
  145. package/dist/services/memory-init-worktrunk-capability.js +172 -0
  146. package/dist/services/memory-layer-publication.js +611 -0
  147. package/dist/services/memory-layer.js +156 -0
  148. package/dist/services/memory-lint-config.js +85 -0
  149. package/dist/services/memory-native-review-copy.js +165 -0
  150. package/dist/services/memory-normal-flow-entry.js +75 -0
  151. package/dist/services/memory-ordinary-defaults.js +112 -0
  152. package/dist/services/memory-preflight-bootstrap-custody.js +115 -0
  153. package/dist/services/memory-preflight-handler.js +259 -0
  154. package/dist/services/memory-repair-current-view.js +299 -0
  155. package/dist/services/memory-repair-document-delta.js +451 -0
  156. package/dist/services/memory-repair-history.js +491 -0
  157. package/dist/services/memory-repair-physical-binding.js +312 -0
  158. package/dist/services/memory-repair-publication-files.js +152 -0
  159. package/dist/services/memory-repair-publication-intent.js +27 -0
  160. package/dist/services/memory-repair-publication-plan.js +160 -0
  161. package/dist/services/memory-repair-publication-source.js +225 -0
  162. package/dist/services/memory-repair-publication.js +794 -0
  163. package/dist/services/memory-repair-registration.js +239 -0
  164. package/dist/services/memory-repair-work-baseline.js +413 -0
  165. package/dist/services/memory-repair-work-completion.js +97 -0
  166. package/dist/services/memory-repair-work-custody.js +73 -0
  167. package/dist/services/memory-repair-work-results.js +89 -0
  168. package/dist/services/memory-repair-workspace.js +252 -0
  169. package/dist/services/memory-repair-writer-copy.js +735 -0
  170. package/dist/services/memory-result-binding.js +173 -0
  171. package/dist/services/memory-retained-review-authority.js +129 -0
  172. package/dist/services/memory-retained-review-results.js +70 -0
  173. package/dist/services/memory-retained-second-baseline.js +102 -0
  174. package/dist/services/memory-retained-second-repairs.js +156 -0
  175. package/dist/services/memory-review-acceptance.js +214 -0
  176. package/dist/services/memory-review-correction-plan.js +171 -0
  177. package/dist/services/memory-review-handler.js +585 -0
  178. package/dist/services/memory-review-inputs.js +200 -0
  179. package/dist/services/memory-review-receipt.js +180 -0
  180. package/dist/services/memory-review-registration.js +108 -0
  181. package/dist/services/memory-review-work-custody.js +124 -0
  182. package/dist/services/memory-review-work-input.js +72 -0
  183. package/dist/services/memory-review-work-results.js +73 -0
  184. package/dist/services/memory-second-corrected-review-inputs.js +165 -0
  185. package/dist/services/memory-second-correction-plan.js +76 -0
  186. package/dist/services/memory-second-repair-baseline.js +91 -0
  187. package/dist/services/memory-second-repair-launch.js +614 -0
  188. package/dist/services/memory-second-repair-registration.js +88 -0
  189. package/dist/services/memory-second-repair-source.js +187 -0
  190. package/dist/services/memory-second-review-acceptance.js +115 -0
  191. package/dist/services/memory-source-observations.js +117 -0
  192. package/dist/services/memory-stage-dispatch.js +28 -0
  193. package/dist/services/memory-stage-results.js +199 -0
  194. package/dist/services/memory-verification-acceptance.js +135 -0
  195. package/dist/services/memory-verification-checks.js +252 -0
  196. package/dist/services/memory-verification-receipt.js +159 -0
  197. package/dist/services/memory-verify-handler.js +135 -0
  198. package/dist/services/memory-work-result-preparation.js +63 -0
  199. package/dist/services/projects.js +17 -2
  200. package/dist/services/recovery-snapshot-database.js +416 -1
  201. package/dist/services/repair-intents.js +1 -1
  202. package/dist/services/review-copy.js +10 -2
  203. package/dist/services/run-control.js +80 -9
  204. package/dist/services/run-controller-capture.js +10 -3
  205. package/dist/services/run-controller-process.js +21 -0
  206. package/dist/services/run-controller-recovery.js +3 -1
  207. package/dist/services/run-controller-state.js +1 -1
  208. package/dist/services/run-controller.js +184 -48
  209. package/dist/services/run-recovery-guard.js +2 -0
  210. package/dist/services/run-recovery.js +145 -5
  211. package/dist/services/run-start-custody.js +177 -0
  212. package/dist/services/runs.js +319 -25
  213. package/dist/services/stage-context.js +9 -1
  214. package/dist/services/stage-handlers.js +74 -0
  215. package/dist/services/stage-lifecycle.js +2 -1
  216. package/dist/services/stage-report-renderer.js +76 -0
  217. package/dist/services/stage-work-graph.js +17 -0
  218. package/dist/services/vnext-code.js +3 -3
  219. package/dist/services/vnext-execution-profile.js +2 -2
  220. package/dist/services/vnext-fanout.js +136 -10
  221. package/dist/services/vnext-merge.js +8 -3
  222. package/dist/services/vnext-plan-review.js +6 -4
  223. package/dist/services/vnext-protocolize.js +60 -2
  224. package/dist/services/work-registry.js +616 -71
  225. package/dist/services/workspace-files.js +1 -1
  226. package/dist/storage/database.js +84 -6
  227. package/dist/storage/writer-contract.js +11 -7
  228. package/package.json +13 -3
  229. package/tools/create-canon-release.mjs +33 -0
  230. package/tools/produce-hybrid-release.mjs +41 -0
package/dist/cli/setup.js CHANGED
@@ -15,6 +15,7 @@ import { hybridDigest } from "../services/hybrid-contracts.js";
15
15
  import { prepareResponseDestination, writeResponseFile } from "./response-file.js";
16
16
  import { readRegistryRelease } from "../services/release-registry.js";
17
17
  import { writePreparationError } from "./error-output.js";
18
+ import { publishSetup } from "../harness-runtime/lib/setup-progress.mjs";
18
19
  /** Lightweight global diagnostic ingress; never creates a runtime context. */
19
20
  export async function runSetupCli(args, env, io = { stdout: process.stdout, stderr: process.stderr, stdin: process.stdin }) {
20
21
  let json = false;
@@ -96,13 +97,14 @@ export async function runSetupCli(args, env, io = { stdout: process.stdout, stde
96
97
  const installed = installAuthenticatedRelease(home, { requestId, action: route.key === "setup update" ? "update" : "install",
97
98
  envelope: bundle.envelope, trustedProducers: producers, compatibility, canonArchive: bundle.canonArchive, engineArchive: bundle.engineArchive,
98
99
  ...(deferred ? { deferredOnboarding: true } : { executor: selected.profile, ...(profileCreation ? { profileCreation } : {}) }),
99
- ...((output.progressJsonl || humanProgress) ? { onCheckpoint: phase => {
100
- if (output.progressJsonl)
101
- io.stdout.write(`${JSON.stringify({ type: "setup_progress", request_id: requestId, phase })}\n`);
102
- else
103
- reportHumanProgress({ prepared: "verified release request retained", canon_cached: "canon cache verified", engine_cached: "engine cache verified",
104
- profiles_saved: "profile settings saved", committed: "installation committed; runtime qualification remains unknown" }[phase]);
105
- } } : {}) });
100
+ onCheckpoint: phase => {
101
+ publishSetup("dependency_check_completed", `release-setup:${phase}`);
102
+ if (output.progressJsonl)
103
+ io.stdout.write(`${JSON.stringify({ type: "setup_progress", request_id: requestId, phase })}\n`);
104
+ else if (humanProgress)
105
+ reportHumanProgress({ prepared: "verified release request retained", canon_cached: "canon cache verified", engine_cached: "engine cache verified",
106
+ profiles_saved: "profile settings saved", committed: "installation committed; runtime qualification remains unknown" }[phase]);
107
+ } });
106
108
  if (humanProgress && installed.replayed)
107
109
  reportHumanProgress("exact request replay verified; retained selection was not reactivated");
108
110
  if (deferred) {
package/dist/cli.js CHANGED
@@ -1,14 +1,66 @@
1
1
  #!/usr/bin/env node
2
2
  import { isNativeHookIngress } from "./cli/native-hook-ingress.js";
3
3
  import { isRuntimeMaintenanceIngress, runRuntimeMaintenanceIngress } from "./cli/runtime-maintenance-ingress.js";
4
+ import { resolveHelp } from "./cli/help.js";
5
+ import { errorRecord } from "./shared/errors.js";
6
+ import { writePreparationError } from "./cli/error-output.js";
4
7
  import { publishSetup } from './harness-runtime/lib/setup-progress.mjs';
5
8
  const args = process.argv.slice(2);
6
- const managedHook = Boolean(process.env.DD_FLOW_CODEX_STATE_DIR) && args.slice(0, 3).join(" ") === "codex hook handle" && !args.includes("--help") && !args.includes("-h");
9
+ let help;
10
+ try {
11
+ help = resolveHelp(args);
12
+ }
13
+ catch (error) {
14
+ const record = errorRecord(error);
15
+ writePreparationError({ stderr: process.stderr }, args, record);
16
+ process.exit(record.exitCode);
17
+ }
18
+ if (help) {
19
+ process.stdout.write(`${help.json ? JSON.stringify(help.data) : help.text}\n`);
20
+ process.exit(0);
21
+ }
22
+ if (args[0] === "completion") {
23
+ const { runCompletionCli } = await import("./cli/completion-cli.js");
24
+ process.exit(runCompletionCli(args));
25
+ }
26
+ if (args[0] === "profiles") {
27
+ const { runOperatorProfilesCli } = await import("./cli/operator-profiles.js");
28
+ process.exit(runOperatorProfilesCli(args, process.env));
29
+ }
30
+ if (args[0] === "setup") {
31
+ const { runSetupCli } = await import("./cli/setup.js");
32
+ process.exit(await runSetupCli(args, process.env));
33
+ }
34
+ if (args[0] === "harnesses") {
35
+ const { runHarnessesCli } = await import("./cli/harnesses.js");
36
+ process.exit(await runHarnessesCli(args, process.env));
37
+ }
38
+ if (args[0] === "models") {
39
+ const { runModelsCli } = await import("./cli/models.js");
40
+ process.exit(runModelsCli(args));
41
+ }
42
+ if (args[0] === "flow" && ["list", "show"].includes(args[1] ?? "")) {
43
+ const { runFlowCatalogCli } = await import("./cli/flows.js");
44
+ process.exit(runFlowCatalogCli(args));
45
+ }
46
+ if (args[0] === "init" || args[0] === "flow" && args[1] === "mb-init") {
47
+ const { runMemoryInitCli } = await import("./cli/memory-init.js");
48
+ process.exit(await runMemoryInitCli(args, process.env));
49
+ }
50
+ if (args.slice(0, 3).join(" ") === "runtime storage admit") {
51
+ const { runMemoryStorageAdmissionCli } = await import("./cli/memory-storage-admission.js");
52
+ process.exit(runMemoryStorageAdmissionCli(args));
53
+ }
54
+ if (args.slice(0, 3).join(" ") === "runtime flow admit") {
55
+ const { runMemoryFlowAdmissionCli } = await import("./cli/memory-flow-admission.js");
56
+ process.exit(await runMemoryFlowAdmissionCli(args));
57
+ }
58
+ const managedHook = Boolean(process.env.DD_FLOW_CODEX_STATE_DIR) && args.slice(0, 3).join(" ") === "codex hook handle";
7
59
  const exitCode = isRuntimeMaintenanceIngress(args, process.env)
8
60
  ? await runRuntimeMaintenanceIngress(args)
9
61
  : managedHook
10
62
  ? await (await import("./cli/hook-ingress.js")).runHookIngress(args)
11
- : isNativeHookIngress(args) && !args.includes("--help") && !args.includes("-h")
63
+ : isNativeHookIngress(args)
12
64
  ? await (await import("./cli/native-hook-ingress.js")).runNativeHookIngress(args)
13
65
  : await (async () => {
14
66
  publishSetup('ingress_entered');
@@ -0,0 +1,20 @@
1
+ /** Engine-owned identities shared by inspection and executable registration.
2
+ * Keep this module dependency-free: catalog inspection must not load handlers.
3
+ */
4
+ export const sdlcHandlerIdentities = Object.freeze({
5
+ specify: Object.freeze({ stage: "specify", entryId: "sdlc-specify-entry", finishId: "sdlc-specify-finish" }),
6
+ protocolize: Object.freeze({ stage: "protocolize", entryId: "sdlc-protocolize-entry", finishId: "sdlc-protocolize-finish" }),
7
+ plan: Object.freeze({ stage: "plan", entryId: "sdlc-plan-entry", finishId: "sdlc-plan-finish" }),
8
+ "plan-review": Object.freeze({ stage: "plan-review", entryId: "sdlc-plan-review-entry", finishId: "sdlc-plan-review-finish" }),
9
+ code: Object.freeze({ stage: "code", entryId: "sdlc-code-entry", finishId: "sdlc-code-finish" }),
10
+ "code-review": Object.freeze({ stage: "code-review", entryId: "sdlc-code-review-entry", finishId: "sdlc-code-review-finish" }),
11
+ merge: Object.freeze({ stage: "merge", entryId: "sdlc-merge-entry", finishId: "sdlc-merge-finish" })
12
+ });
13
+ export const memoryInitHandlerIdentities = Object.freeze({
14
+ preflight: Object.freeze({ stage: "preflight", entryId: "memory-stage-entry", finishId: "memory-stage-finish" }),
15
+ discover: Object.freeze({ stage: "discover", entryId: "memory-stage-entry", finishId: "memory-stage-finish" }),
16
+ compose: Object.freeze({ stage: "compose", entryId: "memory-stage-entry", finishId: "memory-stage-finish" }),
17
+ verify: Object.freeze({ stage: "verify", entryId: "memory-stage-entry", finishId: "memory-stage-finish" }),
18
+ review: Object.freeze({ stage: "review", entryId: "memory-stage-entry", finishId: "memory-stage-finish" }),
19
+ finish: Object.freeze({ stage: "finish", entryId: "memory-stage-entry", finishId: "memory-stage-finish" })
20
+ });
@@ -18,6 +18,19 @@ closed transport is not proof that its native descendants stopped. The adapter
18
18
  must retain causal errors, missing observations and usage completeness rather
19
19
  than turn them into successful/zero-valued results.
20
20
 
21
+ ## Retained child observations
22
+
23
+ Daemon capability `retained_child_observation` enables `session observe-children`
24
+ for its registered root. It reads retained topology and exact terminal metadata,
25
+ never loads/resumes a child or performs semantic work. During fanout the controller
26
+ records failed child facts before dispatching anything else; healthy in-flight
27
+ siblings may settle normally. Observation polling is not productive model activity.
28
+ Ordinary `session inspect` is not a substitute for this read-only operation.
29
+
30
+ Native attribution is a reported fact, not proof that an HTTP request reached a
31
+ provider. If the native store discarded the causal exception, retain unknown;
32
+ do not scrape ambient logs or infer overload/retry authority from an error message.
33
+
21
34
  The bridge marks heuristic ACP completion with
22
35
  `_meta.zcodeCompletionEvidence: "inferred"` and exhausted failed turns with
23
36
  `"failed"`. dd-zcode rejects these before any post-turn materializing read:
@@ -2,19 +2,19 @@
2
2
  import { adapterTimeoutOptions } from "../lib/adapter-timeouts.mjs";
3
3
  import { durableDaemonStart, inspectDaemonOperation, interruptDaemonOperation, settleDaemonOperation } from "../lib/daemon-operations.mjs";
4
4
  import { readFile } from "node:fs/promises";
5
- import { callDaemon, serveDaemon, startDaemon, stopDaemon } from "../lib/dd-opencode-daemon.mjs";
5
+ import { callDaemon, doctor, openCodeProfileOptions, serveDaemon, startDaemon, stopDaemon } from "../lib/dd-opencode-daemon.mjs";
6
6
 
7
7
  function parse(argv) { const positional = [], options = {}; for (let i = 0; i < argv.length; i += 1) { const token = argv[i]; if (!token.startsWith("--")) { positional.push(token); continue; } const key = token.slice(2); if (["json","cancel-tree","no-flow"].includes(key)) options[key] = true; else { if (argv[i + 1] === undefined) throw new Error(`--${key} requires a value`); options[key] = argv[++i]; } } return { positional, options }; }
8
8
  const prompt = async options => options["prompt-file"] ? await readFile(options["prompt-file"], "utf8") : options.prompt;
9
9
  try {
10
- const { positional, options } = parse(process.argv.slice(2)), [family, command, modifier] = positional; const common = { journal: options.journal, stateDir: options["state-dir"], sessionId: options["session-id"], cwd: options.cwd, bin: options["opencode-bin"], provider: options.provider, model: options.model, variant: options.variant, agent: options.agent, authPath: options["auth-path"], configPath: options["config-path"], projectRoot: options["project-root"], ddFlowBin: options["dd-flow-bin"], ddFlowHome: options["dd-flow-home"], noFlow: options["no-flow"] === true, sessionArchive: options["session-archive"], ...adapterTimeoutOptions(options, family === "session" ? command : null) }; let result;
10
+ const { positional, options } = parse(process.argv.slice(2)), [family, command, modifier] = positional; const common = { journal: options.journal, stateDir: options["state-dir"], sessionId: options["session-id"], cwd: options.cwd, bin: options["opencode-bin"], provider: options.provider, model: options.model, ...openCodeProfileOptions(options), authPath: options["auth-path"], configPath: options["config-path"], projectRoot: options["project-root"], ddFlowBin: options["dd-flow-bin"], ddFlowHome: options["dd-flow-home"], noFlow: options["no-flow"] === true, sessionArchive: options["session-archive"], ...adapterTimeoutOptions(options, family === "session" ? command : null) }; let result;
11
11
  if (family === "daemon" && command === "start") result = await durableDaemonStart({ ...common, entryPath: process.argv[1] }, startDaemon);
12
12
  else if (family === "daemon" && command === "serve") { await serveDaemon(common.stateDir); process.exit(0); }
13
13
  else if (family === "daemon" && command === "status") result = await callDaemon(common.stateDir, "daemon.status");
14
14
  else if (family === "daemon" && command === "operation" && modifier === "interrupt") result = await interruptDaemonOperation(common.stateDir, options["operation-id"], { control_id: options["control-id"], stop_operation_id: options["stop-operation-id"], daemon_id: options["daemon-id"], session_id: options["session-id"], evidence: options.evidence ? JSON.parse(options.evidence) : null });
15
15
  else if (family === "daemon" && command === "operation" && modifier === "settle") result = await settleDaemonOperation(common.stateDir, options["operation-id"]);
16
16
  else if (family === "daemon" && command === "operation") result = await inspectDaemonOperation(common.stateDir, options["operation-id"]);
17
- else if (family === "doctor") result = await callDaemon(common.stateDir, "daemon.doctor");
17
+ else if (family === "doctor") result = await doctor(common);
18
18
  else if (family === "daemon" && command === "stop") result = await stopDaemon({ stateDir: common.stateDir, cancelTree: options["cancel-tree"], timeoutMs: common.timeoutMs });
19
19
  else if (family === "session") { const operation = command === "status" ? "inspect" : command; const params = { livenessTimeoutMs: common.livenessTimeoutMs, sessionId: options["session-id"], title: options.title, messageId: options["message-id"], archiveFile: options["archive-file"], ...(command === "prompt" ? { prompt: await prompt(options) } : {}) }; result = await callDaemon(common.stateDir, `session.${operation}`, params, common.timeoutMs ?? 1_800_000); }
20
20
  else throw new Error("usage: dd-opencode daemon start|status|stop | session create|prompt|inspect|fork|cancel|archive [options] --json");
@@ -0,0 +1 @@
1
+ export function inspectDaemonOperation(root: string, id: string): Promise<Record<string, unknown>>;
@@ -8,6 +8,7 @@ import { errorRecord, isObservationLoss, isProvenShellCorrection, reportedError
8
8
  import { assertDaemonOwnership, budgetObservationScope, releaseBudgetObservation, completeBudgetOperation } from "./managed-daemon.mjs";
9
9
  import { readModelObservations, modelAttribution } from "./model-observations.mjs";
10
10
  import { publishOperationProgress, publishOperationWaiting, withOperationProgress } from './operation-progress.mjs';
11
+ import { prepareDaemonRequest } from "./native-request-proof.mjs";
11
12
 
12
13
  const productive = new Set(["session.create", "session.prompt", "session.start", "session.fork", "session.resume"]);
13
14
  const controls = new Map();
@@ -301,7 +302,7 @@ export async function durableDaemonStart(options, action) {
301
302
  // scope can reuse this exact adapter launch after retiring the old daemon.
302
303
  const launch = { schema_id: "dd-flow/native-launch@1", executable: options.entryPath ?? null, args: process.argv.slice(2), cwd: process.cwd(), runtime_owner: process.env.DD_FLOW_RUNTIME_OWNER ?? null,
303
304
  env: Object.fromEntries(["CODEX_HOME", "DD_FLOW_CONFIG_HOME", "DD_FLOW_HOME", "DD_FLOW_BIN", "DD_FLOW_RESOURCE_HOME"].map(key => [key, process.env[key] ?? null])) };
304
- const request = { id: process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation: "daemon.start", params: { ...options, runtimeOwner: process.env.DD_FLOW_RUNTIME_OWNER ?? null, resourceHome: process.env.DD_FLOW_RESOURCE_HOME ?? null, launch } };
305
+ const request = prepareDaemonRequest({ id: process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation: "daemon.start", params: { ...options, runtimeOwner: process.env.DD_FLOW_RUNTIME_OWNER ?? null, resourceHome: process.env.DD_FLOW_RESOURCE_HOME ?? null, launch } });
305
306
  return await journalDispatch(root, request, () => action(options));
306
307
  }
307
308
 
@@ -314,13 +315,13 @@ async function journalDispatch(root, request, action) {
314
315
  const daemon = await read(path.join(root, "daemon.json"));
315
316
  const startup = request.operation === "daemon.start";
316
317
  const daemonId = daemon?.daemon_id ?? daemon?.config?.daemonId ?? null;
317
- await writeJsonAtomic(temporary, { schema_id: "dd-flow/daemon-operation@1", operation_id: request.id, operation: request.operation, session_id: request.params?.sessionId ?? null, daemon_id: startup ? null : daemonId, owner_id: daemon?.config?.runtime_owner?.owner_id ?? null, generation: dispatchGenerations.get(root) ?? 0, ...(startup ? { predecessor_daemon_id: daemonId, launch: request.params?.launch } : {}), params_sha256: fingerprint, requested_at: new Date().toISOString() });
318
+ await writeJsonAtomic(temporary, { schema_id: "dd-flow/daemon-operation@1", operation_id: request.id, operation: request.operation, session_id: request.params?.sessionId ?? null, daemon_id: startup ? null : daemonId, owner_id: daemon?.config?.runtime_owner?.owner_id ?? null, generation: dispatchGenerations.get(root) ?? 0, ...(startup ? { predecessor_daemon_id: daemonId, launch: request.params?.launch } : {}), params_sha256: fingerprint, ...(request.invocation_sha256 ? { invocation_sha256: request.invocation_sha256 } : {}), requested_at: new Date().toISOString() });
318
319
  try {
319
320
  await link(temporary, path.join(directory, "requested.json"));
320
321
  } catch (error) {
321
322
  if (error.code !== "EEXIST") throw error;
322
323
  const saved = await inspectDaemonOperation(root, request.id);
323
- if (saved.params_sha256 !== fingerprint) throw Object.assign(new Error("operation id was reused with different parameters"), { code: "operation_conflict", details: { operation_id: request.id } });
324
+ if (saved.params_sha256 !== fingerprint || (saved.invocation_sha256 ?? null) !== (request.invocation_sha256 ?? null)) throw Object.assign(new Error("operation id was reused with different parameters or invocation"), { code: "operation_conflict", details: { operation_id: request.id } });
324
325
  if (saved.state === "completed") return saved.result;
325
326
  if (["failed", "interrupted"].includes(saved.state)) throw reportedError(saved.error, "provider operation failed");
326
327
  throw Object.assign(new Error("provider operation has no confirmed terminal result; inspect the existing operation instead of resending it"), { code: "operation_observation_lost", details: { operation_id: request.id, receipt_directory: directory } });
@@ -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 { operationProgressReporter } from './operation-progress.mjs';
5
+ import { prepareDaemonRequest } from "./native-request-proof.mjs";
4
6
  import { captureDispatchGuard, cancelPendingDispatch } from "./dispatch-fence.mjs";
5
7
  import { errorRecord } from "./operation-errors.mjs";
6
8
  import { ToolObservations, agyToolObservation, agyTranscriptObservations } from "./tool-observations.mjs";
@@ -58,7 +60,7 @@ async function writeJson(file, value) { const temporary = `${file}.${process.pid
58
60
  async function readState(dir) { try { return JSON.parse(await readFile(locations(dir).state, "utf8")); } catch (error) { if (error.code === "ENOENT") return null; throw error; } }
59
61
  async function removeSocket(paths) { try { if (!(await lstat(paths.socket)).isSocket()) throw new DaemonError("unsafe_socket", "refusing to replace a non-socket daemon path"); await unlink(paths.socket); } catch (error) { if (error.code !== "ENOENT") throw error; } }
60
62
  const shell = value => `'${String(value).replaceAll("'", `'"'"'`)}'`;
61
- export function sanitizedEnv(config) { const env = { ...process.env, TMPDIR: config.temporary, DD_AGY_HOOK_NODE: process.execPath, DD_AGY_HOOK_ENTRY: config.entryPath, DD_AGY_STATE_DIR: config.stateDir, DD_AGY_DAEMON_ID: config.daemonId }; for (const key of ["GEMINI_API_KEY","GOOGLE_API_KEY","ANTHROPIC_API_KEY","OPENAI_API_KEY","DD_FLOW_DAEMON_ID"]) env[key] = undefined; if (config.ddFlowHome && config.daemonId) env.DD_FLOW_DAEMON_ID = config.daemonId; return env; }
63
+ export function sanitizedEnv(config) { const env = { ...process.env, TMPDIR: config.temporary, DD_AGY_HOOK_NODE: process.execPath, DD_AGY_HOOK_ENTRY: config.entryPath, DD_AGY_STATE_DIR: config.stateDir, DD_AGY_DAEMON_ID: config.daemonId }; for (const key of ["GEMINI_API_KEY","GOOGLE_API_KEY","ANTHROPIC_API_KEY","OPENAI_API_KEY"]) env[key] = undefined; return { ...env, ...managedFlowEnv(config) }; }
62
64
  const errorPayload = errorRecord;
63
65
  async function forwardUsage(config, sessionId, result, tools, descendants) {
64
66
  if (!config.ddFlowBin || !config.projectRoot) return;
@@ -164,7 +166,7 @@ export async function qualifyAgyHooks(projectRoot, { stateDir = null, install =
164
166
  }
165
167
 
166
168
  export async function callDaemon(stateDir, operation, params = {}, timeoutMs = 30_000, requestId = null) {
167
- const request = { schema_id: REQUEST_SCHEMA, id: requestId ?? process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation, params };
169
+ const request = prepareDaemonRequest({ schema_id: REQUEST_SCHEMA, id: requestId ?? process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation, params });
168
170
  return await callDaemonObserved({ stateDir, socket: locations(stateDir).socket, request, responseSchema: RESPONSE_SCHEMA, harness: 'agy', timeoutMs });
169
171
  }
170
172
 
@@ -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 { createHash, randomUUID } from "node:crypto";
6
8
  import { chmod, lstat, mkdir, open, readFile, rename, unlink, writeFile } from "node:fs/promises";
@@ -12,9 +14,11 @@ import path from "node:path";
12
14
  import { spawn } from "node:child_process";
13
15
  import { waitForSettlement } from "./session-settlement.mjs";
14
16
  import { errorRecord } from "./operation-errors.mjs";
15
- import { CodexBridge, cancelSessionWithBridge, createSessionWithBridge, inspectSessionWithBridge, promptSessionWithBridge, startSessionWithBridge } from "./dd-codex.mjs";
17
+ import { CODEX_REQUEST_TIMEOUT_MS, LATE_RESPONSE_GRACE_MS, CodexBridge, cancelSessionWithBridge, createSessionWithBridge, inspectSessionWithBridge, promptSessionWithBridge, startSessionWithBridge } from "./dd-codex.mjs";
16
18
  import { assertDaemonOwnership, prepareRuntimeOwner, cleanupFailedDaemonStart, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, scheduleDaemonProcessHeartbeat, registerDaemonProcess, startManagedBridge, stopProcessGroup } from "./managed-daemon.mjs";
17
19
 
20
+ export function codexBridgeEnv(config, daemonId) { return { ...config.env, ...managedFlowEnv(config, daemonId) }; }
21
+
18
22
  const requestSchema = "dd-codex/daemon-request@1";
19
23
  const responseSchema = "dd-codex/daemon-response@1";
20
24
  const stateSchema = "dd-codex/daemon-state@1";
@@ -72,13 +76,15 @@ export async function cancelCodexTree(bridge, options, { remember = () => {}, ac
72
76
  return { harness: "codex-desktop", provider_session_id: options.sessionId, adapter_session_id: options.sessionId, cancelled: true, settled: true, tree_session_ids: [...sessions], cleanup_errors: settlement.cleanup_errors, tree_observations: settlement.observations.map(item => ({ provider_session_id: item.provider_session_id, status: item.status, settled: item.settled })) };
73
77
  }
74
78
 
75
- export async function callDaemon(stateDir, operation, params = {}, timeoutMs = 30_000) {
76
- const request = { schema_id: requestSchema, id: process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation, params };
79
+ export async function callDaemon(stateDir, operation, params = {}, timeoutMs = operation === "session.create" ? params.timeoutMs ?? CODEX_REQUEST_TIMEOUT_MS + LATE_RESPONSE_GRACE_MS : 30_000) {
80
+ const request = prepareDaemonRequest({ schema_id: requestSchema, id: process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation, params });
77
81
  return await callDaemonObserved({ stateDir, socket: locations(stateDir).socket, request, responseSchema: responseSchema, harness: 'codex', timeoutMs });
78
82
  }
79
83
 
80
84
  export async function startDaemon(options) {
81
- const paths = locations(options.stateDir); const cwd = absolute(options.cwd, "--cwd"); const config = { cwd, journal: absolute(options.journal, "--journal"), bin: options.bin ?? null, ddFlowBin: options.ddFlowBin ? absolute(options.ddFlowBin, "--dd-flow-bin") : null, ddFlowHome: options.ddFlowHome ? absolute(options.ddFlowHome, "--dd-flow-home") : null, projectRoot: options.projectRoot ? absolute(options.projectRoot, "--project-root") : cwd, resourceHome: process.env.DD_FLOW_RESOURCE_HOME ? absolute(process.env.DD_FLOW_RESOURCE_HOME, "DD_FLOW_RESOURCE_HOME") : null, env: options.ddFlowHome ? { DD_FLOW_HOME: absolute(options.ddFlowHome, "--dd-flow-home"), ...(options.ddFlowBin ? { DD_FLOW_BIN: absolute(options.ddFlowBin, "--dd-flow-bin") } : {}) } : {} };
85
+ if (!options.noFlow && Boolean(options.ddFlowHome) !== Boolean(options.ddFlowBin)) throw Object.assign(new Error("--dd-flow-home and --dd-flow-bin must be provided together"), { code: "flow_context_incomplete" });
86
+ if (options.noFlow) options = { ...options, ddFlowHome: undefined, ddFlowBin: undefined };
87
+ const paths = locations(options.stateDir); const cwd = absolute(options.cwd, "--cwd"); const config = { cwd, journal: absolute(options.journal, "--journal"), bin: options.bin ?? null, ddFlowBin: options.ddFlowBin ? absolute(options.ddFlowBin, "--dd-flow-bin") : null, ddFlowHome: options.ddFlowHome ? absolute(options.ddFlowHome, "--dd-flow-home") : null, projectRoot: options.projectRoot ? absolute(options.projectRoot, "--project-root") : cwd, resourceHome: !options.noFlow && process.env.DD_FLOW_RESOURCE_HOME ? absolute(process.env.DD_FLOW_RESOURCE_HOME, "DD_FLOW_RESOURCE_HOME") : null, env: options.ddFlowHome ? { DD_FLOW_HOME: absolute(options.ddFlowHome, "--dd-flow-home"), ...(options.ddFlowBin ? { DD_FLOW_BIN: absolute(options.ddFlowBin, "--dd-flow-bin") } : {}) } : {} };
82
88
  await prepareRuntimeOwner(config, paths.dir);
83
89
  await mkdir(paths.dir, { recursive: true, mode: 0o700 }); await chmod(paths.dir, 0o700);
84
90
  let previous = await readState(paths.state); assertDaemonRestartable(previous);
@@ -95,7 +101,7 @@ export async function stopDaemon(options) { const timeoutMs = options.timeoutMs
95
101
 
96
102
  export async function serveDaemon(stateDir) {
97
103
  const paths = locations(stateDir); const state = await readState(paths.state); if (!state || state.schema_id !== stateSchema) throw Object.assign(new Error("daemon state missing"), { code: "daemon_state_missing" });
98
- const bridge = new CodexBridge({ ...state.config, env: { ...state.config.env, CODEX_HOME: process.env.CODEX_HOME, DD_FLOW_DAEMON_ID: state.daemon_id, DD_FLOW_CODEX_STATE_DIR: paths.dir } }); await startManagedBridge({ config: state.config, state, bridge, kind: "codex-provider", publish: snapshot => writeState(paths.state, snapshot) }); const sessions = new Set(state.sessions ?? []); let active = null; let server; let persist = Promise.resolve();
104
+ const bridge = new CodexBridge({ ...state.config, env: { ...codexBridgeEnv(state.config, state.daemon_id), CODEX_HOME: process.env.CODEX_HOME, DD_FLOW_CODEX_STATE_DIR: paths.dir } }); await startManagedBridge({ config: state.config, state, bridge, kind: "codex-provider", publish: snapshot => writeState(paths.state, snapshot) }); const sessions = new Set(state.sessions ?? []); let active = null; let server; let persist = Promise.resolve();
99
105
  scheduleDaemonProcessHeartbeat(state.config, state.resource_process);
100
106
  const save = (patch = {}) => { Object.assign(state, patch, { pid: process.pid, updated_at: new Date().toISOString(), sessions: [...sessions] }); const snapshot = structuredClone(state); persist = persist.catch(() => {}).then(() => writeState(paths.state, snapshot)); return persist; };
101
107
  const dispatch = async (operation, params, operationId) => {
@@ -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,
@@ -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
  }
@@ -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";
@@ -44,7 +45,7 @@ export function droidEnvironment(config) {
44
45
  // This adapter deliberately uses the operator's existing Factory login.
45
46
  // Undefined is a tombstone: downstream env overlays must not resurrect keys.
46
47
  for (const key of Object.keys(env)) if (key.endsWith("API_KEY")) env[key] = undefined;
47
- return env;
48
+ return { ...env, ...managedFlowEnv(config) };
48
49
  }
49
50
  export async function prepareDroidHome(config) {
50
51
  const paths = droidPaths(config.stateDir);
@@ -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";
@@ -39,7 +41,7 @@ const errorPayload = errorRecord;
39
41
  function shellQuote(value) { return `'${String(value).replace(/'/g, "'\\''")}'`; }
40
42
 
41
43
  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; }
44
+ export function isolatedEnv(config) { return { ...process.env, HOME: config.grokHome, GROK_HOME: config.grokHome, ...(config.authPath ? { GROK_AUTH_PATH: config.authPath } : {}), ...managedFlowEnv(config) }; }
43
45
  function within(candidate, root) { const resolved = path.resolve(candidate); const parent = path.resolve(root); return resolved === parent || resolved.startsWith(`${parent}${path.sep}`); }
44
46
 
45
47
  async function writeIsolatedConfig(paths) {
@@ -151,7 +153,7 @@ async function materializeSessionArchive(paths, archiveDir) {
151
153
 
152
154
  export async function callDaemon(stateDir, operation, params = {}, timeoutMs = 30_000) {
153
155
  operation = canonicalNativeOperation('grok', operation);
154
- const request = { schema_id: REQUEST_SCHEMA, id: process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation, params };
156
+ const request = prepareDaemonRequest({ schema_id: REQUEST_SCHEMA, id: process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation, params });
155
157
  return await callDaemonObserved({ stateDir, socket: locations(stateDir).socket, request, responseSchema: RESPONSE_SCHEMA, harness: 'grok', timeoutMs });
156
158
  }
157
159
 
@@ -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";
@@ -22,18 +24,31 @@ import { prepareRuntimeOwner, cleanupFailedStart, confirmDaemonProcess, confirmD
22
24
  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";
23
25
  class DaemonError extends Error { constructor(code, message, retryable = false, details) { super(message); this.code = code; this.retryable = retryable; this.details = details; } }
24
26
  const absolute = (value, label) => { if (!value || !path.isAbsolute(value)) throw new DaemonError("invalid_path", `${label} must be an absolute path`); return path.resolve(value); };
27
+ export function openCodeProfileOptions(options) {
28
+ for (const [native, shared] of [["variant", "reasoning"], ["agent", "mode"]]) {
29
+ if (options[native] !== undefined && options[shared] !== undefined && options[native] !== options[shared]) throw new DaemonError("profile_conflict", `--${native} and --${shared} disagree`);
30
+ }
31
+ return { variant: options.variant ?? options.reasoning, agent: options.agent ?? options.mode };
32
+ }
33
+ export async function doctor(options) {
34
+ if (options.stateDir) return await callDaemon(options.stateDir, "daemon.doctor");
35
+ const output = (await run(options.bin ?? process.env.DD_OPENCODE_BIN ?? "opencode", ["--version"], { cwd: options.cwd ?? process.cwd() })).trim();
36
+ const version = output.match(/(?:^|\s)v?(2\.\d+\.\d+(?:[-+][\w.-]+)?)(?:\s|$)/)?.[1];
37
+ if (!version) throw new DaemonError("unsupported_version", "dd-opencode supports OpenCode v2 only");
38
+ return { compatible: true, native_protocol: 2, observed_runtime: { opencode: version }, productive_hooks: false, profile_observation: "pending_native_session" };
39
+ }
25
40
  function locations(stateDir) { const dir = absolute(stateDir, "--state-dir"), local = path.join(dir, "daemon.sock"); return { dir, socket: Buffer.byteLength(local) < 100 ? local : `/tmp/dd-opencode-${createHash("sha256").update(dir).digest("hex").slice(0, 20)}.sock`, state: path.join(dir, "daemon.json"), journal: path.join(dir, "events.jsonl"), log: path.join(dir, "daemon.log"), data: path.join(dir, "xdg-data"), config: path.join(dir, "xdg-config"), cache: path.join(dir, "xdg-cache"), archive: path.join(dir, "import.json"), db: path.join(dir, "native.db"), xdgState: path.join(dir, "xdg-state") }; }
26
41
  const writeJson = writeJsonAtomic;
27
42
  async function readState(dir) { try { return JSON.parse(await readFile(locations(dir).state, "utf8")); } catch (error) { if (error.code === "ENOENT") return null; throw error; } }
28
43
  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; } }
29
44
  function executable(bin, args = []) { return /\.[cm]?js$/.test(bin) ? { command: process.execPath, args: [bin, ...args] } : { command: bin, args }; }
30
- function isolatedEnv(config) {
45
+ export function isolatedEnv(config) {
31
46
  const env = { ...process.env };
32
47
  for (const name of ["OPENCODE_CONFIG", "OPENCODE_CONFIG_CONTENT", "OPENCODE_PASSWORD", "OPENCODE_PTY_HANDOFF", "OPENCODE_SERVER", "OPENCODE_SIMULATE"]) delete env[name];
33
48
  return { ...env, XDG_DATA_HOME: config.dataHome, XDG_CONFIG_HOME: config.configHome, XDG_CACHE_HOME: config.cacheHome,
34
49
  XDG_STATE_HOME: config.stateHome, OPENCODE_DB: config.database, OPENCODE_CONFIG_DIR: config.adapterDir,
35
50
  OPENCODE_CONFIG_PROJECT_DISABLE: "1", OPENCODE_SERVER_PASSWORD: config.password,
36
- ...(config.ddFlowHome ? { DD_FLOW_HOME: config.ddFlowHome } : {}), ...(config.resourceHome ? { DD_FLOW_RESOURCE_HOME: config.resourceHome } : {}),
51
+ ...managedFlowEnv(config),
37
52
  DD_OPENCODE_PLUGIN_SHA256: config.pluginSha256 ?? "none" };
38
53
  }
39
54
  export async function run(bin, args, config, options = {}) {
@@ -50,7 +65,7 @@ const errorPayload = errorRecord;
50
65
 
51
66
  async function writePlugin(paths, config) {
52
67
  const directory = path.join(paths.config, "opencode", "plugins"); await mkdir(directory, { recursive: true, mode: 0o700 });
53
- 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`;
68
+ 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, resourceHome: config.resourceHome })});\n`;
54
69
  const checksum = createHash("sha256").update(source).digest("hex"); await writeFile(path.join(directory, "dd-flow.js"), source, { mode: 0o600 }); return checksum;
55
70
  }
56
71
  async function prepare(paths, config, authSource) {
@@ -123,7 +138,7 @@ async function importArchive(config, client) {
123
138
  export async function callDaemon(stateDir, operation, params = {}, timeoutMs = 30_000) {
124
139
  const state = await readState(stateDir);
125
140
  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" });
126
- const request = { schema_id: REQUEST_SCHEMA, id: process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation, params };
141
+ const request = prepareDaemonRequest({ schema_id: REQUEST_SCHEMA, id: process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation, params });
127
142
  return await callDaemonObserved({ stateDir, socket: locations(stateDir).socket, request, responseSchema: RESPONSE_SCHEMA, harness: 'opencode', timeoutMs });
128
143
  }
129
144
  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)); }
@@ -272,7 +287,7 @@ export class Runtime {
272
287
  if (info.pid !== this.child?.pid || info.version !== this.state.versions.opencode) throw new DaemonError("foreign_server", "OpenCode endpoint is not the owned process");
273
288
  const pluginID = this.state.config.noFlow ? "dd-flow-security" : "dd-flow";
274
289
  if (!plugins.data.some(plugin => plugin.id === pluginID && plugin.state?.status === "active")) throw new DaemonError("opencode_plugin_missing", "Owned v2 plugin is not active");
275
- return { compatible: true, native_protocol: 2, productive_hooks: !this.state.config.noFlow, versions: this.state.versions, plugin_sha256: this.state.config.pluginSha256, auth_status: this.state.auth_status };
290
+ return { compatible: true, native_protocol: 2, observed_runtime: this.state.versions, productive_hooks: !this.state.config.noFlow, versions: this.state.versions, plugin_sha256: this.state.config.pluginSha256, auth_status: this.state.auth_status };
276
291
  }
277
292
  if (operation === "session.create") return this.productive(operation, async () => {
278
293
  const info = await this.client.create(params.title, this.state.config);