@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
@@ -4,6 +4,7 @@ import { ToolObservations, openCodeToolObservation } from "./tool-observations.m
4
4
  import { ObservationClock, observedTimeout, validateObservationDuration } from "./observation-clock.mjs";
5
5
  import { assertNativeLine } from "./native-streams.mjs";
6
6
  import { operationProgressReporter } from "./operation-progress.mjs";
7
+ import { classifyProviderError, safeProviderError } from "./provider-overload-policy.mjs";
7
8
 
8
9
  export const OPENCODE_PROTOCOL = 2;
9
10
  export class OpenCodeError extends Error {
@@ -27,17 +28,16 @@ export function normalizeMessage(message, sessionID) {
27
28
  }
28
29
 
29
30
  export function providerFailure(native, sessionId, turnId) {
30
- const message = String(native?.message ?? "OpenCode execution failed"), status = native?.status, type = String(native?.type ?? "unknown");
31
- const quota = status === 402 || /quota|balance exhausted|insufficient.?credit|billing|payment required/i.test(message);
32
- const code = quota ? "provider_quota_exhausted" : status === 401 || status === 403 ? "provider_auth_failed"
33
- : /overload|server.?busy/i.test(`${type} ${message}`) || status === 503 ? "provider_capacity_transient"
34
- : /rate.?limit|too many requests/i.test(message) ? "provider_rate_limited"
35
- : status === 429 ? "provider_limit_unknown" : "opencode_provider_failed";
31
+ const status = native?.status, type = String(native?.type ?? "unknown");
32
+ const category = classifyProviderError(native), code = category === "provider_failed" ? "opencode_provider_failed" : category;
36
33
  // Never forward response.body or provider credentials into controller/Judge.
37
34
  return new OpenCodeError(code, `OpenCode native execution failed (${type}${status ? `, HTTP ${status}` : ""})`,
38
35
  ["provider_capacity_transient", "provider_rate_limited"].includes(code), {
39
36
  provider_session_id: sessionId, session_id: sessionId, turn_id: turnId, native_error_type: type,
40
37
  native_turn_id: turnId, terminal_status: "failed",
38
+ provider_error: safeProviderError(native),
39
+ will_retry: native?.willRetry === true, native_retry_active: native?.willRetry === true,
40
+ native_turn_items: { observed: true, possible_effects: true, pending: false },
41
41
  ...(Number.isInteger(status) ? { http_status: status } : {}), observed_at: new Date().toISOString(), native_source: "opencode.v2.session.execution" });
42
42
  }
43
43
 
@@ -48,11 +48,16 @@ export class OpenCodeClient {
48
48
  baseUrl: this.baseUrl.href, headers: { authorization: `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}` }
49
49
  }));
50
50
  }
51
- async call(method, input, { timeoutMs, progress, onDispatched, productive = false } = {}) {
51
+ async call(method, input, { timeoutMs, progress, onDispatched, productive = false, signal } = {}) {
52
52
  const observation = this.readObservation?.();
53
53
  timeoutMs ??= observation?.timeoutMs ?? this.timeoutMs; progress ??= observation?.progress;
54
54
  const api = await this.ready, controller = new AbortController();
55
- const timer = observedTimeout(() => controller.abort(), timeoutMs, { progress });
55
+ let timer;
56
+ const abort = () => { controller.abort(); clearInterval(timer); };
57
+ signal?.addEventListener("abort", abort, { once: true });
58
+ if (signal?.aborted) abort();
59
+ timer = observedTimeout(() => controller.abort(), timeoutMs, { progress });
60
+ if (signal?.aborted) clearInterval(timer);
56
61
  try {
57
62
  const keys = method.split("."), name = keys.pop(), domain = keys.reduce((value, key) => value?.[key], api);
58
63
  if (typeof domain?.[name] !== "function") throw new OpenCodeError("opencode_capability_missing", `OpenCode v2 does not expose ${method}`);
@@ -68,7 +73,7 @@ export class OpenCodeClient {
68
73
  if (productive || controller.signal.aborted) throw Object.assign(lost(`OpenCode observer lost during ${method}`, { method }), { cause });
69
74
  throw Object.assign(new OpenCodeError("opencode_unavailable", `OpenCode ${method} observation failed`, true,
70
75
  { method, native_type: cause._tag ?? cause.name ?? null, native_reason: cause.reason ?? null }), { cause });
71
- } finally { clearInterval(timer); }
76
+ } finally { clearInterval(timer); signal?.removeEventListener("abort", abort); }
72
77
  }
73
78
  async health() {
74
79
  const info = await this.call("server.info", undefined, { timeoutMs: 5_000 });
@@ -95,7 +100,8 @@ export class OpenCodeClient {
95
100
  async children(id) { return (await this.pages("session.list", { parentID: id })).map(info => ({ ...info, directory: info.location?.directory })); }
96
101
  async messages(id) { return (await this.pages("message.list", { sessionID: id })).map(message => normalizeMessage(message, id)); }
97
102
  inbox(id) { return this.call("session.inbox.list", { sessionID: id }); }
98
- async shells(id) { const result = await this.call("shell.list", { location: { directory: this.directory } }); return result.data.filter(item => item.metadata?.sessionID === id); }
103
+ async shellDirectory(id) { const api = await this.ready; return typeof api.session?.get === "function" ? (await this.inspect(id)).directory : this.directory; }
104
+ async shells(id) { const directory = await this.shellDirectory(id); const result = await this.call("shell.list", { location: { directory } }); return result.data.filter(item => item.metadata?.sessionID === id).map(item => ({ ...item, location: item.location ?? { directory } })); }
99
105
  async pending(id) {
100
106
  const [inbox, forms, permissions] = await Promise.all([this.inbox(id), this.call("session.form.list", { sessionID: id }), this.call("permission.list", { sessionID: id })]);
101
107
  const value = { inbox, forms: forms.data ?? forms, permissions: permissions.data ?? permissions };
@@ -117,7 +123,7 @@ export class OpenCodeClient {
117
123
  const result = await this.call("session.interrupt", { sessionID: id, resume: false }, { productive: true });
118
124
  const api = await this.ready;
119
125
  if (typeof api.shell?.list === "function") for (const shell of await this.shells(id)) {
120
- if (shell.status === "running") await this.call("shell.remove", { id: shell.id, location: { directory: this.directory } }, { productive: true });
126
+ if (shell.status === "running") await this.call("shell.remove", { id: shell.id, location: shell.location }, { productive: true });
121
127
  }
122
128
  return result;
123
129
  }
@@ -127,7 +133,7 @@ export class OpenCodeClient {
127
133
  import(data) { return this.call("session.import", data, { productive: true }); }
128
134
 
129
135
  async prompt(id, { prompt, messageId = `msg_${randomUUID().replaceAll("-", "")}`, retained,
130
- save, onModelMessages, onObservationError, assertDispatch, onDispatched, livenessTimeoutMs = this.timeoutMs }) {
136
+ save, onModelMessages, onObservationError, assertDispatch, onDispatched, checkHookFailures, livenessTimeoutMs = this.timeoutMs }) {
131
137
  validateObservationDuration(livenessTimeoutMs);
132
138
  const request = { sessionID: id, id: messageId, text: prompt, delivery: "queue", resume: true }, digest = requestDigest(request);
133
139
  if (retained && (retained.request_sha256 !== digest || retained.message_id !== messageId)) throw new OpenCodeError("operation_request_mismatch", "OpenCode operation already binds another prompt", false, { effect: "no_effect" });
@@ -144,6 +150,21 @@ export class OpenCodeClient {
144
150
  const publish = operationProgressReporter(), known = new Set([id, ...baseline.keys()]), signatures = new Map(), shellCursors = new Map(), liveSeen = new Set();
145
151
  let progress = 0, marker, stream;
146
152
  const liveParts = new Map(), liveShells = new Map(), backgroundShells = new Set();
153
+ const completedTools = new Set(), outputReads = new Map(), shellLocations = new Map();
154
+ const observationErrors = { count: 0 };
155
+ const observeOutput = (shellID, location) => {
156
+ if (outputReads.has(shellID) || !location?.directory) return;
157
+ const cursor = shellCursors.get(shellID) ?? 0;
158
+ const pending = this.call("shell.output", { id: shellID, location, cursor, limit: 65536 }, { signal: streamAbort.signal })
159
+ .then(async output => { const data = output.data ?? output; if (!streamAbort.signal.aborted && data.cursor > cursor) { shellCursors.set(shellID, data.cursor); await advance(); } })
160
+ .catch(async error => {
161
+ if (streamAbort.signal.aborted) return;
162
+ const diagnostic = { method: "shell.output", shell_id: shellID, native_type: error.details?.native_type ?? error.cause?._tag ?? null, http_status: error.cause?.status ?? null };
163
+ observationErrors.count++; observationErrors.first ??= diagnostic; observationErrors.last = diagnostic;
164
+ if (observationErrors.count === 1) await onObservationError?.(error);
165
+ }).finally(() => outputReads.delete(shellID));
166
+ outputReads.set(shellID, pending); void pending.catch(() => {});
167
+ };
147
168
  const advance = async (observedAt = Date.now()) => { marker = { cursor: ++progress, observed_at: observedAt }; await publish("native_progress", { session_id: id, observed_at: new Date(marker.observed_at).toISOString() }); };
148
169
  let connected;
149
170
  const connection = new Promise(resolve => { connected = resolve; });
@@ -157,7 +178,8 @@ export class OpenCodeClient {
157
178
  && ["session.text.delta", "session.reasoning.delta", "session.tool.input.delta", "session.tool.progress"].includes(event.type)) {
158
179
  liveSeen.add(event.id); if (liveSeen.size > 1024) liveSeen.delete(liveSeen.values().next().value);
159
180
  const key = `${data.sessionID}:${data.assistantMessageID}:${data.id ?? ""}`, signature = requestDigest(data.metadata ?? null);
160
- if (event.type === "session.tool.progress" && typeof data.metadata?.shellID === "string") liveShells.set(key, data.metadata.shellID);
181
+ if (event.type === "session.tool.progress" && completedTools.has(key)) continue;
182
+ if (event.type === "session.tool.progress" && typeof data.metadata?.shellID === "string" && !completedTools.has(key)) liveShells.set(key, data.metadata.shellID);
161
183
  if (typeof data.delta === "string" && data.delta.length || event.type === "session.tool.progress" && liveParts.get(key) !== signature) {
162
184
  liveParts.set(key, signature); await advance(event.created);
163
185
  }
@@ -189,6 +211,7 @@ export class OpenCodeClient {
189
211
  }
190
212
  for (;;) {
191
213
  try {
214
+ await checkHookFailures?.();
192
215
  const events = await this.logs(id, state.cursor);
193
216
  let delivered = state.delivered === true, terminal = state.terminal;
194
217
  for (const event of events) {
@@ -198,6 +221,7 @@ export class OpenCodeClient {
198
221
  if (delivered && ["session.execution.succeeded", "session.execution.failed", "session.execution.interrupted"].includes(event.type)) terminal = { type: event.type, error: event.data.error };
199
222
  state = { ...state, cursor: event.durable.seq, delivered, terminal }; await save?.(state);
200
223
  }
224
+ let runningTools = false;
201
225
  const observe = async (session, visited = new Set()) => {
202
226
  if (visited.has(session)) throw lost("OpenCode topology contains a cycle"); visited.add(session); known.add(session);
203
227
  const messages = await this.messages(session);
@@ -211,19 +235,24 @@ export class OpenCodeClient {
211
235
  signatures.set(key, { signature: next, seen, highWater: Math.max(before?.highWater ?? 0, encoded.length) });
212
236
  }
213
237
  for (const part of item.parts) {
214
- let shellID = part.type === "tool" ? part.state?.metadata?.shellID ?? liveShells.get(`${session}:${item.info.id}:${part.id}`) : null;
238
+ const toolKey = `${session}:${item.info.id}:${part.id}`;
239
+ if (part.type === "tool" && ["running", "streaming", "pending"].includes(part.state?.status)) runningTools = true;
240
+ if (part.type === "tool" && !["running", "streaming", "pending"].includes(part.state?.status)) { completedTools.add(toolKey); liveShells.delete(toolKey); }
241
+ let shellID = part.type === "tool" ? part.state?.metadata?.shellID ?? liveShells.get(toolKey) : null;
215
242
  if (!shellID && part.tool === "shell" && part.state?.status === "running") {
216
243
  // Tool progress metadata is volatile in the CLI. Reconcile
217
244
  // only an unambiguous native shell in this current message.
218
245
  const candidates = (await this.shells(session)).filter(shell => shell.time.started >= item.info.created);
219
- if (candidates.length === 1) shellID = candidates[0].id;
246
+ if (candidates.length === 1) { shellID = candidates[0].id; shellLocations.set(shellID, candidates[0].location); }
220
247
  }
221
248
  if (!shellID) continue;
222
249
  const cursor = shellCursors.get(shellID) ?? 0;
223
250
  shellCursors.set(shellID, cursor); // Zero-output shells still own their native notification.
224
251
  if (part.tool === "shell" && part.state?.metadata?.status === "running") backgroundShells.add(shellID);
225
- const output = await this.call("shell.output", { id: shellID, location: { directory: this.directory }, cursor, limit: 65536 }), data = output.data ?? output;
226
- if (data.cursor > cursor) { shellCursors.set(shellID, data.cursor); await advance(); }
252
+ if (!completedTools.has(toolKey)) {
253
+ if (!shellLocations.has(shellID)) shellLocations.set(shellID, (await this.shells(session)).find(shell => shell.id === shellID)?.location);
254
+ observeOutput(shellID, shellLocations.get(shellID));
255
+ }
227
256
  }
228
257
  }
229
258
  for (const child of await this.children(session)) {
@@ -258,7 +287,7 @@ export class OpenCodeClient {
258
287
  state = { ...state, delivered: true, terminal: { type: `session.execution.${terminal.info.outcome}`, error: terminal.info.error } }; await save?.(state);
259
288
  }
260
289
  }
261
- const statuses = await this.statuses(); let unsettled = false;
290
+ const statuses = await this.statuses(); let unsettled = runningTools;
262
291
  // A background child's native completion notification is delivered
263
292
  // asynchronously. An idle parent before that delivery is not the
264
293
  // completion of the coordinating operation.
@@ -278,18 +307,20 @@ export class OpenCodeClient {
278
307
  if (typeof api.shell?.list === "function") unsettled ||= (await this.shells(session)).some(shell => shell.status === "running");
279
308
  }
280
309
  if (state.delivered && state.terminal && !unsettled) {
310
+ await checkHookFailures?.();
281
311
  if (state.terminal.type === "session.execution.failed") throw providerFailure(state.terminal.error, id, messageId);
282
312
  if (state.terminal.type === "session.execution.interrupted") throw new OpenCodeError("opencode_execution_interrupted", "OpenCode execution was interrupted", false, { provider_session_id: id, turn_id: messageId });
283
313
  const final = current.findLast(item => item.info.completed !== undefined && item.info.finish && item.info.finish !== "tool-calls");
284
314
  if (!final) throw lost("OpenCode terminal has no current assistant response", { session_id: id, message_id: messageId });
285
315
  if (final.info.error) throw providerFailure(final.info.error, id, messageId);
286
- return { ...final, admission_message_id: messageId, log_cursor: state.cursor };
316
+ return { ...final, admission_message_id: messageId, log_cursor: state.cursor, ...(observationErrors.count ? { observation_errors: observationErrors } : {}) };
287
317
  }
288
318
  } catch (error) {
319
+ await checkHookFailures?.();
289
320
  if (error instanceof OpenCodeError && !["opencode_unavailable", "operation_observation_lost"].includes(error.code)) throw error;
290
321
  await onObservationError?.(error);
291
322
  }
292
- if (clock.sample(marker)) throw lost("OpenCode native progress stopped or observation could not recover", { session_id: id, message_id: messageId, observation: clock.state() });
323
+ if (clock.sample(marker)) throw lost("OpenCode native progress stopped or observation could not recover", { session_id: id, message_id: messageId, observation: clock.state(), ...(observationErrors.count ? { observation_errors: observationErrors } : {}) });
293
324
  await delay(Math.max(5, Math.min(500, livenessTimeoutMs / 4)));
294
325
  }
295
326
  } finally { clearInterval(connectionTimer); this.readObservation = priorObservation; streamAbort.abort(); await stream; }
@@ -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 { admitNativeRequestBuffer } from "./native-streams.mjs";
4
5
  import { ObservationClock } from "./observation-clock.mjs";
6
+ import { prepareDaemonRequest } from "./native-request-proof.mjs";
5
7
  import { spawn } from "node:child_process";
6
8
  import { createHash, randomUUID } from "node:crypto";
7
9
  import { chmod, lstat, mkdir, open, readFile, rename, unlink, writeFile } from "node:fs/promises";
@@ -28,6 +30,7 @@ import {
28
30
  } from "./dd-zcode.mjs";
29
31
 
30
32
  const REQUEST_SCHEMA = "dd-zcode/daemon-request@1";
33
+ import { settledNativeTree } from "./native-children.mjs";
31
34
  const RESPONSE_SCHEMA = "dd-zcode/daemon-response@1";
32
35
  const STATE_SCHEMA = "dd-zcode/daemon-state@1";
33
36
  // ACP silence starts diagnostics; it is not a provider-failure deadline.
@@ -90,7 +93,7 @@ async function removeSocket(socket, dir) {
90
93
  const errorPayload = errorRecord;
91
94
 
92
95
  export async function callDaemon(stateDir, operation, params = {}, timeoutMs = 30_000) {
93
- const request = { schema_id: REQUEST_SCHEMA, id: process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation, params };
96
+ const request = prepareDaemonRequest({ schema_id: REQUEST_SCHEMA, id: process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation, params });
94
97
  return await callDaemonObserved({ stateDir, socket: locations(stateDir).socket, request, responseSchema: RESPONSE_SCHEMA, harness: 'zcode', timeoutMs });
95
98
  }
96
99
 
@@ -110,22 +113,21 @@ function sameConfig(state, config) {
110
113
  }
111
114
 
112
115
  export async function startDaemon(options) {
116
+ if (options.noFlow) options = { ...options, ddFlowHome: undefined, ddFlowBin: undefined, projectRoot: undefined };
113
117
  const paths = locations(options.stateDir);
114
118
  const hasFlowContext = Boolean(options.ddFlowHome || options.projectRoot || options.ddFlowBin);
115
- if (hasFlowContext && (!options.ddFlowHome || !options.projectRoot)) {
116
- throw new DaemonError("flow_context_incomplete", "--dd-flow-home and --project-root must be provided together for ZCode lifecycle forwarding");
119
+ if (hasFlowContext && (!options.ddFlowHome || !options.projectRoot || !options.ddFlowBin)) {
120
+ throw new DaemonError("flow_context_incomplete", "--dd-flow-home, --dd-flow-bin and --project-root must be provided together for ZCode lifecycle forwarding");
117
121
  }
118
122
  const config = {
119
123
  cwd: absolute(options.cwd, "--cwd"),
120
124
  journal: absolute(options.journal, "--journal"),
121
125
  bin: options.bin ?? null,
122
126
  zcodePath: options.zcodePath ?? null,
123
- // The worker itself invokes `dd-flow` from PATH. Use that same executable
124
- // for the trusted event bridge unless a pinned executable was supplied.
125
- ddFlowBin: hasFlowContext ? (options.ddFlowBin ? absolute(options.ddFlowBin, "--dd-flow-bin") : "dd-flow") : null,
127
+ ddFlowBin: hasFlowContext ? absolute(options.ddFlowBin, "--dd-flow-bin") : null,
126
128
  ddFlowHome: options.ddFlowHome ? absolute(options.ddFlowHome, "--dd-flow-home") : null,
127
129
  projectRoot: options.projectRoot ? absolute(options.projectRoot, "--project-root") : null,
128
- resourceHome: process.env.DD_FLOW_RESOURCE_HOME ? absolute(process.env.DD_FLOW_RESOURCE_HOME, "DD_FLOW_RESOURCE_HOME") : null,
130
+ resourceHome: !options.noFlow && process.env.DD_FLOW_RESOURCE_HOME ? absolute(process.env.DD_FLOW_RESOURCE_HOME, "DD_FLOW_RESOURCE_HOME") : null,
129
131
  env: { ...nativeRoutingEnv(), ...(options.ddFlowHome ? { DD_FLOW_HOME: absolute(options.ddFlowHome, "--dd-flow-home"), ...(options.ddFlowBin ? { DD_FLOW_BIN: absolute(options.ddFlowBin, "--dd-flow-bin") } : {}) } : {}) },
130
132
  livenessTimeoutMs: positiveDuration(options.livenessTimeoutMs, "livenessTimeoutMs", DEFAULT_LIVENESS_TIMEOUT_MS),
131
133
  requestTimeoutMs: optionalDuration(options.requestTimeoutMs, "requestTimeoutMs", DEFAULT_REQUEST_TIMEOUT_MS),
@@ -373,6 +375,7 @@ export class DaemonRuntime {
373
375
  current.reject = error => { if (this.nativeHookOperation !== current || current.error) return; current.error = error; this.bridge.lifecycleFailure ??= error; this.bridge.notificationError = this.bridge.lifecycleFailure; this.bridge.rejectAll(this.bridge.lifecycleFailure); rejectHook(this.bridge.lifecycleFailure); };
374
376
  const timer = setInterval(() => { void this.observeHookFailures(current).catch(error => current.reject(error)); }, 100);
375
377
  try {
378
+ if (name === "session.prompt") this.state.last_native_failure = null;
376
379
  await this.persist({ active_tree: true, active_operation: name, ...(request ? { active_request: request } : {}) });
377
380
  const result = await Promise.race([task(), hookFailed]);
378
381
  this.track(result);
@@ -385,6 +388,9 @@ export class DaemonRuntime {
385
388
  // after a late/partial create: the native Session existed, but the
386
389
  // daemon state no longer carried the only correlation to it.
387
390
  const active = this.state?.active_request;
391
+ const response = error?.details?.terminal_response;
392
+ if (name === "session.prompt" && response?.method === "session/prompt" && error.details.provider_session_id)
393
+ this.state.last_native_failure = { provider_session_id: error.details.provider_session_id, request_id: response.request_id };
388
394
  try {
389
395
  await hookBudget(1_000, null).run(() => this.persist({
390
396
  active_operation: null,
@@ -571,7 +577,14 @@ export class DaemonRuntime {
571
577
  if (this.sessions.has(result.provider_session_id)) this.track(result, result.subagents);
572
578
  else if (result.adapter_session_id) this.liveAdapters.add(result.adapter_session_id);
573
579
  await this.persist({ active_tree: this.treeRunning(result) || Boolean(this.activeProductive) });
574
- return { ...result, settled: !this.treeRunning(result) && !this.activeProductive };
580
+ const settled = !this.treeRunning(result) && !this.activeProductive;
581
+ const failure = this.state.last_native_failure;
582
+ const nativeChildren = (result.evidence ?? result).subagents;
583
+ const childIds = [...new Set([...(this.sessions.get(result.provider_session_id)?.known_child_session_ids ?? []), ...zcodeChildSessionIds(nativeChildren)])];
584
+ const childStates = new Map([...(nativeChildren?.ended?.items ?? nativeChildren?.completed ?? []), ...(nativeChildren?.running ?? []).map(child => ({ ...child, status: "running" }))].map(child => [child.childSessionId ?? child.sessionId, child.status]));
585
+ const rootSettled = !this.activeProductive && failure?.provider_session_id === result.provider_session_id && (result.evidence ?? result).read?.projection?.status === "idle";
586
+ return { ...result, settled, ...settledNativeTree(result.provider_session_id, childIds.map(childSessionId => ({ childSessionId, status: childStates.get(childSessionId) ?? "unknown" })), settled, rootSettled),
587
+ ...(failure?.provider_session_id === result.provider_session_id ? { terminal_turn_id: String(failure.request_id) } : {}) };
575
588
  }
576
589
  if (operation === "session.cancel") {
577
590
  if (!this.sessions.has(params.sessionId)) throw new DaemonError("session_identity_mismatch", "cancellation Session is not owned by this daemon");
@@ -683,8 +696,7 @@ export class DaemonRuntime {
683
696
  }
684
697
 
685
698
  export function zcodeBridgeEnv(config, daemonId) {
686
- return { ...config.env, DD_FLOW_DAEMON_ID: daemonId,
687
- DD_FLOW_RUNTIME_OWNER: config.runtime_owner ? JSON.stringify(config.runtime_owner) : undefined };
699
+ return { ...config.env, ...managedFlowEnv(config, daemonId) };
688
700
  }
689
701
 
690
702
  export async function serveDaemon(stateDir) {
@@ -15,6 +15,7 @@ import { commandText } from "./process-json.mjs";
15
15
  import { readNativeLines, streamNativeDiagnostics, nativeObservationError } from "./native-streams.mjs";
16
16
  import { operationProgressReporter } from "./operation-progress.mjs";
17
17
  import { safeNativeFailure } from "./native-children.mjs";
18
+ import { classifyProviderError, safeProviderError } from "./provider-overload-policy.mjs";
18
19
 
19
20
  import { readZcodeUsage } from "./zcode-usage.mjs";
20
21
  const ZCODE_HARNESS_CONTRACT = "dd-zcode-harness@1";
@@ -218,9 +219,12 @@ export class AcpBridge {
218
219
  // ZCode emits this as `session/update`; Grok Build wraps the same ACP
219
220
  // payload in `_x.ai/session_notification`. Preserve either provider's
220
221
  // native error before its generic JSON-RPC terminal response arrives.
221
- if (typeof message.params?.sessionId === "string" && update?.sessionUpdate === "retry_state" && update.type === "failed") {
222
+ if (typeof message.params?.sessionId === "string" && update?.sessionUpdate === "retry_state") {
222
223
  for (const pending of this.pending.values()) {
223
- if (pending.method === "session/prompt" && pending.sessionId === message.params.sessionId && !pending.providerError) pending.providerError = { ...update, observed_at: now(), provider_session_id: pending.sessionId, source: message.method };
224
+ if (pending.method === "session/prompt" && pending.sessionId === message.params.sessionId) {
225
+ pending.nativeRetryActive = update.type !== "failed";
226
+ if (update.type === "failed") pending.providerError = { ...update, observed_at: now(), provider_session_id: pending.sessionId, source: message.method };
227
+ }
224
228
  }
225
229
  }
226
230
  if (message.method === "session/update" && message.params?.update?.sessionUpdate === "agent_message_chunk" && message.params.update.content?.type === "text") {
@@ -283,11 +287,17 @@ export class AcpBridge {
283
287
  const error = new Error(messageText);
284
288
  const nativeCode = typeof data?.code === "string" ? data.code : typeof data?.native_code === "string" ? data.native_code : null;
285
289
  const unknown = ["native_timeout", "native_backend_dead", "native_backend_pipe_broken", "native_outcome_unknown"].includes(nativeCode);
286
- error.code = unknown ? nativeCode : /payment required|quota|usage balance exhausted|insufficient.?credit|billing/i.test(messageText) ? "provider_quota_exhausted" : /rate.?limit|too many requests/i.test(messageText) ? "provider_rate_limited" : /\b429\b/.test(messageText) ? "provider_limit_unknown" : "acp_request_failed";
287
- error.retryable = error.code === "provider_rate_limited";
290
+ const nativeFailure = { ...(provider ?? {}), ...(data ?? {}),
291
+ code: [provider?.code, data?.code, data?.native_code].filter(value => typeof value === "string").join(" "), message: messageText };
292
+ const category = pending.method === "session/prompt" ? classifyProviderError(nativeFailure) : "provider_failed";
293
+ error.code = unknown ? nativeCode : category === "provider_failed" ? "acp_request_failed" : category;
294
+ error.retryable = ["provider_rate_limited", "provider_capacity_transient"].includes(error.code);
288
295
  error.details = { acp_code: message.error.code ?? null,
289
296
  observed_at: provider?.observed_at ?? now(), provider_session_id: pending.sessionId ?? null, source: "acp.error_response", request_id: message.id,
290
297
  terminal_response: { method: pending.method, session_id: pending.sessionId, request_id: message.id },
298
+ ...(pending.method === "session/prompt" ? { native_source: "acp.error_response", terminal_status: "failed", turn_id: String(message.id), native_turn_id: String(message.id), provider_error: safeProviderError(nativeFailure),
299
+ native_retry_active: pending.nativeRetryActive === true, will_retry: nativeFailure.willRetry === true,
300
+ native_turn_items: { observed: this.toolSummary(pending.sessionId).completeness === "complete", possible_effects: this.toolSummary(pending.sessionId).total > 0, pending: this.toolSummary(pending.sessionId).pending > 0 } } : {}),
291
301
  ...(data ? { native: data } : {}), ...(details ? { provider_details: details } : {}), ...(providerDetails ? { provider_update: provider } : {}) };
292
302
  pending.reject(error);
293
303
  }
@@ -1046,6 +1056,9 @@ export async function promptSessionWithBridge(bridge, options) {
1046
1056
  idleSessionId: identity.adapterSessionId,
1047
1057
  idleTimeoutMs: options.livenessTimeoutMs ?? 600_000,
1048
1058
  inactivityMode: "diagnostic",
1059
+ }).catch(error => {
1060
+ if (error.details?.terminal_response?.session_id === identity.adapterSessionId) error.details.provider_session_id = identity.providerSessionId;
1061
+ throw error;
1049
1062
  });
1050
1063
  const stopReason = typeof turn?.stopReason === "string" ? turn.stopReason : null;
1051
1064
  const completionEvidence = turn?._meta?.zcodeCompletionEvidence;
@@ -1,6 +1,7 @@
1
1
  export type DelegationWork = { work_id: string; start_command: string; launch_policy?: string };
2
2
  export type DelegationInput = { harness: string; stage: string; capacity: number; works?: DelegationWork[]; totalWorks?: number; remainingWorks?: number };
3
3
  export function delegationAdapter(harness: string): string | null;
4
+ export function renderNativeChildFollowupInstruction(harness: string, childId: string): string | null;
4
5
  export function delegationContract(harness: string): string;
5
6
  export const lifecycleRetryInstruction: string;
6
7
  export const lifecycleCommandCompletionInstruction: string;
@@ -37,6 +37,17 @@ export function delegationAdapter(harness) {
37
37
  return Object.hasOwn(aliases, harness) ? aliases[harness] : null;
38
38
  }
39
39
 
40
+ /** Only documented same-child APIs may continue an existing native worker. */
41
+ export function renderNativeChildFollowupInstruction(harness, childId) {
42
+ if (typeof childId !== "string" || !childId) return null;
43
+ const adapter = delegationAdapter(harness), target = JSON.stringify(childId);
44
+ if (adapter === "codex") return `Use collaboration.followup_task with target: ${target} and message: "Continue the same assigned Work after the transient provider overload; do not restart it or repeat committed lifecycle commands." Do not spawn a replacement.`;
45
+ if (adapter === "opencode") return `Use the native subagent tool with sessionID: ${target} and prompt: "Continue the same assigned Work after the transient provider overload; do not restart it or repeat committed lifecycle commands." Keep the same model and reasoning. Do not spawn a replacement.`;
46
+ if (adapter === "droid") return `Use the native Task tool with resume: ${target}, subagent_type: "dd-flow-worker", and prompt: "Continue the same assigned Work after the transient provider overload; do not restart it or repeat committed lifecycle commands." Omit model and complexity overrides. Do not spawn a replacement.`;
47
+ if (adapter === "grok") return `Use the native send_subagent_message tool with subagent_id: ${target}, delivery: "queue", and text: "Continue the same assigned Work after the transient provider overload; do not restart it or repeat committed lifecycle commands." Retain its accepted message_id and wait on the same subagent. If admission is uncertain, do not resend. Do not spawn a replacement.`;
48
+ return null;
49
+ }
50
+
40
51
  function requireAdapter(harness) {
41
52
  const key = delegationAdapter(harness);
42
53
  if (!key) throw Object.assign(new Error(`Unsupported delegation adapter: ${harness}`), { code: "delegation_contract_unsupported" });
@@ -49,7 +60,7 @@ export function delegationContract(harness) {
49
60
  return `dd-flow/delegation/${key}@${["codex", "opencode"].includes(key) ? 2 : 1}`;
50
61
  }
51
62
 
52
- export const lifecycleCommandCompletionInstruction = "Run runtime-issued lifecycle CLI calls in the foreground, never with a native background or detached-shell option. A shell tool may yield before the lifecycle process exits. Retain and poll its exact process/session handle to completion, including inside a tool wrapper; empty interim output is not a CLI result. Read final stdout or response_file before deciding continuation, and never reissue the command while it is pending.";
63
+ export const lifecycleCommandCompletionInstruction = "Run runtime-issued lifecycle CLI calls in the foreground, never with a native background or detached-shell option. A shell tool may yield before the lifecycle process exits. Retain and poll its exact process/session handle to completion, including inside a tool wrapper; empty interim output is not a CLI result. If a wrapper returns 'Script running with cell ID …', use that wrapper's wait tool to finish the same cell before expecting an inner shell session handle; do not execute the command again. Read final stdout or response_file before deciding continuation, and never reissue the command while it is pending.";
53
64
 
54
65
  export const lifecycleRetryInstruction = `${lifecycleCommandCompletionInstruction} A failure is not permission to replay a lifecycle command. For proven effect=no_effect with recoverable=true and retry_command, correct the input and execute that exact issued successor. A check failure can retain receipts while leaving Work running: correct the evidenced cause first, then use the CLI's exact retry_command for an intentional fresh check attempt; never replay a check merely because Work is unsettled. If a successful CODE finish returns outcome=verification_required, inspect the retained receipts, update its verification file, and use only next.finish_command: the Stage is still running and the previous invocation ID is consumed. If continuation.kind=repair_required, or a successful result has outcome=repair_required and a repair Work ID, end this Turn for the registered repair child; use any retry_command only after that repair settles and verification is refreshed. Unknown effects require reconciliation, never replay. A committed effect without an explicit continuation is not retryable. A native shell launch refusal before the CLI starts permits correcting only that shell-form error and retrying the same exact command. Otherwise report the primary error and stop; never invent IDs, call hooks directly, or run status as a substitute.`;
55
66
 
@@ -71,7 +82,9 @@ export function workerDelegationTask({ workId, startCommand, launchPolicy }) {
71
82
  "Your first technical action must be this exact lifecycle command:",
72
83
  startCommand,
73
84
  "Execute the issued operation and target exactly. Lifecycle invocation IDs are internal runtime authority and are intentionally omitted; never add or invent them. Preserve any explicit native receipt handle that the command does contain.",
74
- "Use the native shell's inherited working directory. Do not set or override a workdir/cwd field for this lifecycle command.",
85
+ startCommand.startsWith("cd '")
86
+ ? "Execute the literal cd shell prefix exactly; it routes this command into its runtime-owned private execution copy. Do not invent native spawn fields. After start, obey the packet's read_only/write scopes and prefix subsequent issued lifecycle commands with the same literal cd route. A review copy never grants permission to write reviewed inputs. The runtime independently checks the CLI process directory."
87
+ : "Use the native shell's inherited working directory. Do not set or override a workdir/cwd field for this lifecycle command.",
75
88
  "Continue only if it succeeds and returns the authoritative Work packet.",
76
89
  lifecycleRetryInstruction,
77
90
  "After a successful start, complete the assigned Work, write its required result, invoke its exact work finish command, then stop.",
@@ -109,7 +122,7 @@ export function renderDelegationInstructions({ harness, stage, capacity, works =
109
122
 
110
123
  export function renderCapacityInstructions({ harness, maximum }) {
111
124
  const adapter = requireAdapter(harness);
112
- if (!Number.isInteger(maximum) || maximum < 1) throw new Error("Invalid native capacity limit");
125
+ if (!Number.isInteger(maximum) || maximum < 1 || maximum > 15) throw new Error("Native capacity limit must be an integer from 1 through 15");
113
126
  return [
114
127
  `Adapter delegation contract: ${delegationContract(harness)}.`,
115
128
  "This is a technical native-subagent capacity qualification, not product work.",
@@ -25,7 +25,8 @@ export function maintenanceRetryable(error) {
25
25
  let retryable = false;
26
26
  // Inspect only bounded typed causes from our maintenance transport, never text.
27
27
  for (let depth = 0; error && depth < 8; depth++, error = error.cause ?? error.details?.cause) {
28
- if (error.details?.cleanup_unconfirmed === true) return false;
28
+ if (error.details?.cleanup_unconfirmed === true || error.cleanup_error || error.details?.cleanup_error
29
+ || error.details?.store_cleanup_errors?.length || error.details?.transaction_cleanup_error || error.details?.transaction_close_error) return false;
29
30
  if (error.details?.effect === "unknown") return false;
30
31
  if (["process_lease_lost", "controller_lease_lost", "process_maintenance_receipt_invalid", "runtime_owner_mismatch"].includes(error.code)) return false;
31
32
  if (error.code === "process_ownership_unknown" && error.details?.reason === "physical_identity_unavailable" && ["process_confirmation", "observer_registration"].includes(error.details.phase) && error.details.effect === "no_effect" && error.details.native_dispatch_started === false) retryable = true;
@@ -255,6 +255,7 @@ export function parseCommandArgs(argv) {
255
255
  const positional = [], options = new Map();
256
256
  for (let index = 0; index < argv.length; index++) {
257
257
  const value = argv[index];
258
+ if (value === "--") { positional.push(...argv.slice(index + 1)); break; }
258
259
  if (value?.startsWith("--")) {
259
260
  const equals = value.indexOf("=");
260
261
  if (equals > 2) { const key = value.slice(2, equals); options.set(key, [...(options.get(key) ?? []), value.slice(equals + 1)]); continue; }
@@ -8,5 +8,6 @@ export function confirmDaemonProcess(config: RuntimeConfig, record: ProcessBindi
8
8
  export function assertDaemonOwnership(stateDir: string, dispatch?: Record<string, unknown>, assertCurrent?: () => void): Promise<void>;
9
9
  export function heartbeatDaemonProcess(config: RuntimeConfig, record: ProcessBinding | null | undefined): Promise<void>;
10
10
  export function finishDaemonProcess(config: RuntimeConfig, record: ProcessBinding | null | undefined, state?: string, reason?: string): Promise<void>;
11
+ export function cleanupFailedDaemonStart(config: RuntimeConfig, record: ProcessBinding | null, child: ChildProcess | null, original: Error & { details?: Record<string, unknown> }, readState: () => Promise<Record<string, unknown>>, daemonId: string): Promise<void>;
11
12
  export function completeBudgetOperation(stateDir: string, request: { id: string; operation: string; params?: { sessionId?: string } }, result: unknown, observe?: () => Promise<Record<string, unknown>>): Promise<Record<string, unknown>>;
12
13
  export function stopProcessGroup(child: ChildProcess | null | undefined, graceMs?: number, options?: { detached?: boolean; settlementMs?: number }): Promise<void>;
@@ -61,14 +61,16 @@ export async function prepareRuntimeOwner(config, stateDir, env = process.env) {
61
61
  export async function assertDaemonOwnership(stateDir, dispatch = {}, assertCurrent = () => {}, progress = async () => {}, waiting = async () => {}) {
62
62
  const gate = async () => {
63
63
  assertCurrent();
64
+ let confirmed = false;
64
65
  for (const lease of leases.values()) {
65
66
  if (lease.stateDir !== path.resolve(stateDir)) continue;
66
67
  await reconfirmLease(lease, assertCurrent);
67
68
  assertCurrent();
69
+ confirmed = true;
68
70
  }
71
+ if (confirmed) await progress('ownership_confirmed');
69
72
  };
70
73
  await gate();
71
- await progress('ownership_confirmed');
72
74
  const state = JSON.parse(await readFile(path.join(stateDir, "daemon.json"), "utf8"));
73
75
  const owner = bindRuntimeOwner({ runtime_owner: state.config?.runtime_owner ?? null }, {});
74
76
  if (!owner) return;
@@ -264,7 +266,7 @@ async function executeRuntimeProcess(config, action, options = {}, { timeoutMs,
264
266
  const poisonKey = token => JSON.stringify([path.resolve(resourceHome), options.id, token ?? null]);
265
267
  if (action !== "status" && options.id) {
266
268
  const poison = maintenancePoison.get(poisonKey(options["lease-token"])) ?? maintenancePoison.get(poisonKey(null));
267
- if (poison) throw poison;
269
+ if (poison && !(action === "finish" && poison.finish === JSON.stringify(options))) throw poison.error;
268
270
  }
269
271
  const args = ["runtime", "process", action];
270
272
  for (const [key, value] of Object.entries(options)) if (value !== null && value !== undefined) args.push(`--${key}`, String(value));
@@ -275,9 +277,13 @@ async function executeRuntimeProcess(config, action, options = {}, { timeoutMs,
275
277
  try { output = await runBoundedCommand({ bin: ddFlowBin, args: [...args, "--json"], cwd: config.cwd,
276
278
  env: { ...(config.env ?? {}), DD_FLOW_HOME: ddFlowHome, DD_FLOW_RESOURCE_HOME: resourceHome }, timeoutMs,
277
279
  timeoutCode: action === "heartbeat" ? "process_heartbeat_timeout" : "process_maintenance_timeout", inheritDeadline: false, cleanupWithinBudget: true,
278
- maintenanceProgress: { action, processId: options.id ?? null, startupMs: Math.min(timeoutMs, RENEWAL_POLICY.startupMs), inactivityMs: RENEWAL_POLICY.attemptMs }, signal }); }
280
+ // Storage silence is not loss of authority: the original command/lease
281
+ // deadline remains finite, including slow I/O and multiple bounded reads.
282
+ maintenanceProgress: { action, processId: options.id ?? null, startupMs: Math.min(timeoutMs, RENEWAL_POLICY.startupMs), inactivityMs: timeoutMs }, signal }); }
279
283
  catch (error) {
280
- if (action !== "status" && options.id && error.details?.cleanup_unconfirmed) maintenancePoison.set(poisonKey(options["lease-token"]), error);
284
+ if (action !== "status" && options.id && error.details?.cleanup_unconfirmed) maintenancePoison.set(poisonKey(options["lease-token"]), {
285
+ error, ...(action === "finish" && error.code === "process_maintenance_timeout" ? { finish: JSON.stringify(options) } : {})
286
+ });
281
287
  throw error;
282
288
  }
283
289
  let result;
@@ -393,7 +399,9 @@ export async function heartbeatDaemonProcess(config, record) {
393
399
  if (lease.closing) throw Object.assign(new Error("Managed process is closing"), { code: "process_closing" });
394
400
  if (lease.lost) throw lease.error;
395
401
  if (lease.error && !maintenanceRetryable(lease.error)) throw lease.error;
396
- if (!renewalRemaining(lease.renewal)) throw lease.error;
402
+ if (!renewalRemaining(lease.renewal)) throw lease.error ?? Object.assign(new Error("Managed lease authority has expired before heartbeat dispatch"), {
403
+ code: "process_ownership_unknown", details: { process_id: record.id, reason: "lease_authority_expired" }
404
+ });
397
405
  if (lease.pending) return await lease.pending;
398
406
  lease.pending = (async () => {
399
407
  try {
@@ -425,7 +433,7 @@ export async function finishDaemonProcess(config, process, state = "stopped", re
425
433
  // A predecessor owns its transport fence; finish gets an independent
426
434
  // settlement window only after that flight physically settled.
427
435
  const settlement = createRenewalState(); settlement.uncertainSince = performance.now();
428
- const result = await retryBookkeeping(timeoutMs => runtimeProcess(config, "finish", { id: process.id, "lease-token": process.lease_token, state, reason }, { timeoutMs }), settlement);
436
+ const result = await retryBookkeeping(timeoutMs => runtimeProcess(config, "finish", { id: process.id, "lease-token": process.lease_token, state, reason }, { timeoutMs }), settlement, true);
429
437
  if (result?.ok !== true) throw Object.assign(new Error("managed process finish was not accepted"), { code: "process_lease_lost", details: { process_id: process.id } });
430
438
  leases.delete(key);
431
439
  }
@@ -453,11 +461,16 @@ async function reconfirmLease(lease, assertCurrent) {
453
461
  return;
454
462
  }
455
463
  }
456
- async function retryBookkeeping(action, renewal = createRenewalState()) {
464
+ async function retryBookkeeping(action, renewal = createRenewalState(), repeatFinish = false) {
457
465
  renewal.uncertainSince ??= performance.now();
458
466
  for (;;) {
459
467
  try { return await action(renewalRemaining(renewal)); }
460
- catch (error) { if (!maintenanceRetryable(error) || !renewalFailure(renewal)) throw error; await new Promise(resolve => setTimeout(resolve, Math.min(renewalDelay(renewal), renewalRemaining(renewal)))); }
468
+ catch (error) {
469
+ // Exact finish is an idempotent terminal-state CAS, not productive IO.
470
+ // Its old helper may still commit the same finish; all other writes stay fenced.
471
+ if (!(maintenanceRetryable(error) || repeatFinish && error.code === "process_maintenance_timeout" && error.details?.cleanup_unconfirmed === true) || !renewalFailure(renewal)) throw error;
472
+ await new Promise(resolve => setTimeout(resolve, Math.min(renewalDelay(renewal), renewalRemaining(renewal))));
473
+ }
461
474
  }
462
475
  }
463
476
 
@@ -482,7 +495,10 @@ function requireTimelyMaintenance(renewal, action, receipt, record) {
482
495
  export async function cleanupFailedStart(config, record, child, original) {
483
496
  try {
484
497
  await stopProcessGroup(child);
498
+ const stopped = child?.pid ? { pid: child.pid, pgid: process.platform === "win32" ? null : child.pid,
499
+ settled: true, physical_exit: "ESRCH", observed_at: new Date().toISOString() } : undefined;
485
500
  await finishDaemonProcess(config, record, "failed", original.code ?? "startup_failed");
501
+ return stopped;
486
502
  } catch (cleanup) {
487
503
  original.details = { ...original.details, cleanup_failed: errorRecord(cleanup), process_id: record?.id ?? null, pid: child?.pid ?? null };
488
504
  }
@@ -491,7 +507,7 @@ export async function cleanupFailedStart(config, record, child, original) {
491
507
  /** The provider has its own process group. A failed daemon start must settle
492
508
  * that exact retained process as well as the daemon; PID-only cleanup is unsafe. */
493
509
  export async function cleanupFailedDaemonStart(config, daemonRecord, daemonChild, original, readState, daemonId) {
494
- await cleanupFailedStart(config, daemonRecord, daemonChild, original);
510
+ const stopped = await cleanupFailedStart(config, daemonRecord, daemonChild, original);
495
511
  if (!daemonRecord) return;
496
512
  try {
497
513
  const state = await readState();
@@ -502,6 +518,15 @@ export async function cleanupFailedDaemonStart(config, daemonRecord, daemonChild
502
518
  }
503
519
  const provider = state.provider_process;
504
520
  if (provider) await runtimeProcess(config, "stop", { id: provider.id, "lease-token": provider.lease_token });
521
+ // Confirmation can fail before the registry learns the child's PID. The
522
+ // owned child handle still proves its physical exit, not a productive ACK.
523
+ if (stopped && !original.details?.cleanup_failed && !original.details?.provider_cleanup_failed && state.pid === stopped.pid && !provider && state.provider_pid == null && owner) {
524
+ original.details = { ...original.details, startup_child_cleanup: {
525
+ ...stopped, daemon_id: daemonId, process_id: daemonRecord.id,
526
+ owner_id: owner.owner_id, project_id: owner.project_id, run_id: owner.run_id,
527
+ operation_id: owner.operation_id, state_dir: owner.state_dir
528
+ } };
529
+ }
505
530
  } catch (cleanup) {
506
531
  original.details = { ...original.details, provider_cleanup_failed: errorRecord(cleanup) };
507
532
  }
@@ -587,29 +612,39 @@ async function stopOwnedChild(child, graceMs, detached, settlementMs) {
587
612
 
588
613
  /** A daemon acknowledgement is not a completed stop until its endpoint closes. */
589
614
  export async function confirmDaemonStopped(request, socket, timeoutMs = 30_000, { readState } = {}) {
590
- const deadline = performance.now() + timeoutMs;
591
615
  const expected = await readState?.();
592
616
  const identity = state => state?.daemon_id ?? state?.config?.daemonId;
593
617
  if (readState && (!identity(expected) || !Number.isSafeInteger(expected?.pid) || expected.pid < 1)) throw Object.assign(new Error('daemon stop has no retained incarnation/PID proof'), { code: 'daemon_cleanup_identity_mismatch' });
594
618
  const failure = async observer => {
595
619
  const state = await readState?.();
596
- if (expected && identity(state) !== identity(expected)) throw Object.assign(new Error("daemon incarnation changed during stop"), { code: "daemon_identity_mismatch" });
620
+ if (expected && (identity(state) !== identity(expected) || state?.pid !== expected.pid)) throw Object.assign(new Error("daemon incarnation changed during stop"), { code: "daemon_identity_mismatch" });
597
621
  const primary = state?.shutdown_state === "cleanup_failed" ? state.cleanup_error : null;
598
622
  if (primary) throw Object.assign(new Error(primary.message), primary, { details: { ...primary.details, socket, phase: "confirm_daemon_stop", observer_error: errorRecord(observer) } });
599
623
  throw observer;
600
624
  };
601
- let result;
625
+ let result, observationError;
602
626
  try { result = await request(); }
603
- catch (error) { return await failure(error); }
604
- if (result?.stopped !== true) return await failure(Object.assign(new Error("stop acknowledgement does not prove shutdown"), { code: "daemon_stop_incomplete" }));
627
+ catch (error) {
628
+ if (!expected || !["daemon_timeout", "daemon_not_running", "daemon_connection_closed", "operation_observation_lost"].includes(error.code)) return await failure(error);
629
+ observationError = error;
630
+ }
631
+ if (!observationError && result?.stopped !== true) return await failure(Object.assign(new Error("stop acknowledgement does not prove shutdown"), { code: "daemon_stop_incomplete" }));
632
+ // The RPC wait and physical retirement are separate phases. A lost ACK
633
+ // consumes the former, not the existing confirmation budget.
634
+ const deadline = performance.now() + timeoutMs;
605
635
  for (;;) {
606
636
  const state = await readState?.();
607
- if (expected && identity(state) !== identity(expected)) return await failure(Object.assign(new Error("daemon incarnation changed"), { code: "daemon_identity_mismatch" }));
637
+ if (expected && (identity(state) !== identity(expected) || state?.pid !== expected.pid)) return await failure(Object.assign(new Error("daemon incarnation changed"), { code: "daemon_identity_mismatch" }));
638
+ if (state?.shutdown_state === "cleanup_failed") return await failure(observationError ?? Object.assign(new Error("daemon cleanup failed"), { code: "daemon_stop_incomplete" }));
608
639
  let endpointPresent = true;
609
640
  try { await lstat(socket); } catch (error) { if (error.code === "ENOENT") endpointPresent = false; else throw error; }
610
641
  let daemonPresent = false;
611
642
  if (expected?.pid) { try { process.kill(expected.pid, 0); daemonPresent = true; } catch (error) { if (error.code !== "ESRCH") daemonPresent = true; } }
612
643
  if (!endpointPresent && !daemonPresent) {
644
+ if (observationError) {
645
+ if (state?.shutdown_state === "clean" && state.active_tree === false && !state.cleanup_error) return { stopped: true, clean: true };
646
+ return await failure(observationError);
647
+ }
613
648
  if (readState && result.clean === true && (state?.shutdown_state !== 'clean' || state.active_tree !== false)) return await failure(Object.assign(new Error('daemon clean ACK conflicts with its retained state'), { code: 'daemon_stop_incomplete' }));
614
649
  return result;
615
650
  }