@deksden-com/dd-flow-cli 0.9.0-beta.144 → 0.9.0-beta.149

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (254) hide show
  1. package/CHANGELOG.md +130 -0
  2. package/README.md +13 -8
  3. package/dist/build-info.json +7 -7
  4. package/dist/cli/command-inputs.js +13 -3
  5. package/dist/cli/command-metadata.js +51 -16
  6. package/dist/cli/diagnostic-invocation.js +46 -0
  7. package/dist/cli/diagnostic.js +120 -0
  8. package/dist/cli/help.js +67 -1
  9. package/dist/cli/hook-ingress.js +27 -3
  10. package/dist/cli/memory-flow-admission.js +9 -3
  11. package/dist/cli/memory-init-invocation.js +10 -3
  12. package/dist/cli/memory-init.js +77 -68
  13. package/dist/cli/memory-storage-admission.js +3 -2
  14. package/dist/cli/memory-storage-qualification.js +105 -0
  15. package/dist/cli/memory-storage-transition.js +123 -0
  16. package/dist/cli/memory-upgrade.js +189 -0
  17. package/dist/cli/native-hook-ingress.js +14 -4
  18. package/dist/cli/native-hook-preparation.js +55 -0
  19. package/dist/cli/response-file.js +2 -1
  20. package/dist/cli/run-cli.js +79 -40
  21. package/dist/cli/setup.js +2 -1
  22. package/dist/cli.js +18 -0
  23. package/dist/domain/diagnostic-aspects.js +103 -0
  24. package/dist/domain/diagnostic-flows.js +8 -0
  25. package/dist/domain/stage-handler-identities.js +8 -0
  26. package/dist/harness-runtime/bin/dd-agy.mjs +3 -3
  27. package/dist/harness-runtime/bin/dd-grok.mjs +3 -3
  28. package/dist/harness-runtime/bin/dd-zcode.mjs +5 -5
  29. package/dist/harness-runtime/lib/agy-error-boundary.mjs +58 -0
  30. package/dist/harness-runtime/lib/agy-native-inspection.mjs +216 -0
  31. package/dist/harness-runtime/lib/agy-native_pb.js +117 -0
  32. package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
  33. package/dist/harness-runtime/lib/daemon-operations.mjs +84 -10
  34. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +366 -52
  35. package/dist/harness-runtime/lib/dd-agy.mjs +17 -3
  36. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +27 -13
  37. package/dist/harness-runtime/lib/dd-codex.mjs +4 -0
  38. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +4 -3
  39. package/dist/harness-runtime/lib/dd-droid.mjs +71 -29
  40. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +73 -16
  41. package/dist/harness-runtime/lib/dd-grok.mjs +6 -3
  42. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +66 -20
  43. package/dist/harness-runtime/lib/dd-opencode.mjs +30 -3
  44. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +7 -6
  45. package/dist/harness-runtime/lib/dd-zcode.mjs +44 -14
  46. package/dist/harness-runtime/lib/delegation-instructions.mjs +1 -1
  47. package/dist/harness-runtime/lib/driver-recovery.mjs +9 -8
  48. package/dist/harness-runtime/lib/entity-references.d.mts +2 -0
  49. package/dist/harness-runtime/lib/entity-references.mjs +9 -0
  50. package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -1
  51. package/dist/harness-runtime/lib/managed-daemon.mjs +24 -9
  52. package/dist/harness-runtime/lib/native-children.d.mts +8 -1
  53. package/dist/harness-runtime/lib/native-children.mjs +114 -8
  54. package/dist/harness-runtime/lib/native-hook-command.mjs +4 -4
  55. package/dist/harness-runtime/lib/native-request-proof.mjs +1 -1
  56. package/dist/harness-runtime/lib/observation-clock.d.mts +2 -2
  57. package/dist/harness-runtime/lib/observation-clock.mjs +7 -4
  58. package/dist/harness-runtime/lib/opencode-plugin.mjs +3 -3
  59. package/dist/harness-runtime/lib/operation-errors.mjs +1 -1
  60. package/dist/harness-runtime/lib/provider-overload-policy.d.mts +5 -3
  61. package/dist/harness-runtime/lib/provider-overload-policy.mjs +20 -7
  62. package/dist/harness-runtime/lib/runner-events.mjs +3 -1
  63. package/dist/harness-runtime/lib/setup-progress.mjs +8 -6
  64. package/dist/harness-runtime/lib/tool-observations.mjs +1 -1
  65. package/dist/schemas/diagnostic-stage-result.schema.json +354 -0
  66. package/dist/schemas/diagnostic-work-result.schema.json +296 -0
  67. package/dist/schemas/execution-bundle.schema.json +7 -1
  68. package/dist/schemas/mb-upgrade-stage-result.schema.json +863 -0
  69. package/dist/schemas/memory-repair-work-result.schema.json +9 -2
  70. package/dist/schemas/project-flow-pack-manifest.schema.json +1 -1
  71. package/dist/schemas/release-impact.schema.json +3 -3
  72. package/dist/schemas/stage-start-response.schema.json +4 -0
  73. package/dist/services/accepted-intent.js +13 -1
  74. package/dist/services/canon.js +43 -7
  75. package/dist/services/check-environment.js +21 -0
  76. package/dist/services/check-runner-evidence.js +267 -15
  77. package/dist/services/cli-operation-classifier.js +1 -1
  78. package/dist/services/cli-upgrade-allowlist.js +1 -1
  79. package/dist/services/code-checks.js +34 -19
  80. package/dist/services/compatibility-preflight.js +5 -2
  81. package/dist/services/controller-execution.js +10 -4
  82. package/dist/services/controller-fanout.js +34 -10
  83. package/dist/services/controller-memory-task.js +45 -0
  84. package/dist/services/controller-memory-worker.js +49 -0
  85. package/dist/services/dashboard.js +2 -2
  86. package/dist/services/descriptor-run-settlement.js +5 -3
  87. package/dist/services/descriptor-run-start.js +46 -20
  88. package/dist/services/descriptor-stage-coordinator.js +66 -13
  89. package/dist/services/descriptor-stage-settlement.js +2 -1
  90. package/dist/services/descriptor-stage-start.js +40 -4
  91. package/dist/services/diagnostic-evidence.js +73 -0
  92. package/dist/services/diagnostic-report-publication.js +72 -0
  93. package/dist/services/diagnostic-report.js +67 -0
  94. package/dist/services/diagnostic-run-bundle.js +74 -0
  95. package/dist/services/diagnostic-snapshot.js +159 -0
  96. package/dist/services/diagnostic-stage-handlers.js +108 -0
  97. package/dist/services/diagnostic-stage-results.js +132 -0
  98. package/dist/services/diagnostic-start.js +84 -0
  99. package/dist/services/diagnostic-work-batch.js +34 -0
  100. package/dist/services/diagnostic-work-collection.js +97 -0
  101. package/dist/services/diagnostic-work-registration.js +101 -0
  102. package/dist/services/diagnostic-work-results.js +93 -0
  103. package/dist/services/engines.js +64 -14
  104. package/dist/services/eval-snapshots.js +186 -119
  105. package/dist/services/execution-bundles.js +19 -7
  106. package/dist/services/external-work-launch.js +5 -3
  107. package/dist/services/flow-catalog.js +13 -4
  108. package/dist/services/frozen-engine.js +148 -11
  109. package/dist/services/hook-memory-routing.js +188 -0
  110. package/dist/services/hook-recovery-guard.js +14 -0
  111. package/dist/services/hook-session-observation.js +24 -0
  112. package/dist/services/hooks.js +27 -8
  113. package/dist/services/hybrid-contracts.js +4 -13
  114. package/dist/services/hybrid-digest.js +14 -0
  115. package/dist/services/installed-flow-source.js +47 -4
  116. package/dist/services/lifecycle-invocations.js +179 -38
  117. package/dist/services/memory-accepted-composition.js +5 -4
  118. package/dist/services/memory-completed-correction-wave.js +23 -15
  119. package/dist/services/memory-compose-handler.js +163 -21
  120. package/dist/services/memory-composition-acceptance.js +186 -10
  121. package/dist/services/memory-composition-graph.js +42 -11
  122. package/dist/services/memory-composition-registration.js +23 -8
  123. package/dist/services/memory-composition-work-custody.js +14 -9
  124. package/dist/services/memory-composition-work-results.js +15 -3
  125. package/dist/services/memory-corrected-review-batch.js +10 -3
  126. package/dist/services/memory-corrected-review-findings.js +4 -2
  127. package/dist/services/memory-corrected-review-inputs.js +10 -5
  128. package/dist/services/memory-corrected-review-receipt.js +17 -9
  129. package/dist/services/memory-corrected-review-registration.js +3 -2
  130. package/dist/services/memory-correction-check-receipt.js +20 -4
  131. package/dist/services/memory-custody-snapshot.js +36 -0
  132. package/dist/services/memory-finish-evidence.js +44 -22
  133. package/dist/services/memory-finish-handler.js +37 -23
  134. package/dist/services/memory-finish-integration.js +218 -48
  135. package/dist/services/memory-finish-readiness.js +66 -19
  136. package/dist/services/memory-git-integration.js +87 -18
  137. package/dist/services/memory-historical-corrected-review.js +21 -15
  138. package/dist/services/memory-historical-original-review.js +22 -9
  139. package/dist/services/memory-historical-repair-registration.js +14 -8
  140. package/dist/services/memory-historical-second-review.js +30 -11
  141. package/dist/services/memory-init-contexts.js +44 -2
  142. package/dist/services/memory-init-controller-start.js +27 -8
  143. package/dist/services/memory-init-git-policy.js +50 -21
  144. package/dist/services/memory-init-lint-capability.js +24 -6
  145. package/dist/services/memory-init-native-capacity.js +17 -4
  146. package/dist/services/memory-init-preflight.js +21 -17
  147. package/dist/services/memory-init-request-candidate.js +73 -17
  148. package/dist/services/memory-init-request-input.js +23 -7
  149. package/dist/services/memory-init-run-bundle.js +49 -12
  150. package/dist/services/memory-init-storage.js +17 -7
  151. package/dist/services/memory-init-worktrunk-capability.js +3 -7
  152. package/dist/services/memory-layer.js +71 -6
  153. package/dist/services/memory-lint-config.js +111 -2
  154. package/dist/services/memory-native-review-copy.js +5 -67
  155. package/dist/services/memory-normal-flow-entry.js +12 -0
  156. package/dist/services/memory-ordinary-defaults.js +22 -5
  157. package/dist/services/memory-preflight-handler.js +15 -0
  158. package/dist/services/memory-repair-current-view.js +1 -1
  159. package/dist/services/memory-repair-document-delta.js +70 -33
  160. package/dist/services/memory-repair-history.js +12 -7
  161. package/dist/services/memory-repair-physical-binding.js +1 -119
  162. package/dist/services/memory-repair-publication-plan.js +5 -3
  163. package/dist/services/memory-repair-publication-source.js +1 -1
  164. package/dist/services/memory-repair-publication.js +3 -2
  165. package/dist/services/memory-repair-registration.js +2 -1
  166. package/dist/services/memory-repair-work-baseline.js +4 -2
  167. package/dist/services/memory-repair-work-completion.js +32 -12
  168. package/dist/services/memory-repair-work-results.js +21 -2
  169. package/dist/services/memory-repair-workspace.js +11 -2
  170. package/dist/services/memory-repair-writer-copy.js +2 -1
  171. package/dist/services/memory-result-binding.js +78 -12
  172. package/dist/services/memory-retained-review-authority.js +17 -8
  173. package/dist/services/memory-retained-second-baseline.js +1 -1
  174. package/dist/services/memory-retained-second-repairs.js +12 -6
  175. package/dist/services/memory-review-correction-plan.js +47 -12
  176. package/dist/services/memory-review-handler.js +17 -17
  177. package/dist/services/memory-review-inputs.js +55 -9
  178. package/dist/services/memory-review-limits.js +10 -0
  179. package/dist/services/memory-review-receipt.js +2 -2
  180. package/dist/services/memory-review-registration.js +13 -4
  181. package/dist/services/memory-review-work-custody.js +3 -1
  182. package/dist/services/memory-review-work-input.js +26 -7
  183. package/dist/services/memory-review-work-results.js +26 -4
  184. package/dist/services/memory-second-corrected-review-inputs.js +5 -3
  185. package/dist/services/memory-second-correction-plan.js +2 -1
  186. package/dist/services/memory-second-repair-launch.js +4 -4
  187. package/dist/services/memory-second-repair-source.js +1 -1
  188. package/dist/services/memory-stage-dispatch.js +38 -1
  189. package/dist/services/memory-stage-results.js +113 -22
  190. package/dist/services/memory-tool-probe.js +25 -0
  191. package/dist/services/memory-upgrade-analysis-handler.js +243 -0
  192. package/dist/services/memory-upgrade-bootstrap-readiness.js +251 -0
  193. package/dist/services/memory-upgrade-evidence.js +100 -0
  194. package/dist/services/memory-upgrade-inventory.js +442 -0
  195. package/dist/services/memory-upgrade-publication-custody.js +73 -0
  196. package/dist/services/memory-upgrade-publication.js +413 -0
  197. package/dist/services/memory-upgrade-selection.js +430 -0
  198. package/dist/services/memory-upgrade-start.js +60 -0
  199. package/dist/services/memory-upgrade-storage-readiness.js +340 -0
  200. package/dist/services/memory-verification-acceptance.js +38 -17
  201. package/dist/services/memory-verification-checks.js +92 -27
  202. package/dist/services/memory-verification-receipt.js +6 -5
  203. package/dist/services/memory-verify-handler.js +15 -13
  204. package/dist/services/memory-work-result-json.js +12 -0
  205. package/dist/services/memory-work-result-preparation.js +5 -4
  206. package/dist/services/merge-server.js +4 -2
  207. package/dist/services/migrations.js +127 -98
  208. package/dist/services/native-daemon-history.js +2 -1
  209. package/dist/services/physical-startup.js +45 -0
  210. package/dist/services/project-registry.js +97 -0
  211. package/dist/services/project-summary.js +1 -1
  212. package/dist/services/projects.js +3 -93
  213. package/dist/services/prompt-delivery.js +71 -0
  214. package/dist/services/provider-capacity.js +112 -46
  215. package/dist/services/recovery-snapshot-database.js +32 -4
  216. package/dist/services/release-cache.js +8 -0
  217. package/dist/services/repair-intents.js +1 -12
  218. package/dist/services/repair-receipt.js +13 -0
  219. package/dist/services/run-control-worker.js +35 -18
  220. package/dist/services/run-control.js +2 -1
  221. package/dist/services/run-controller-adapter.js +2 -1
  222. package/dist/services/run-controller-process.js +4 -7
  223. package/dist/services/run-controller-recovery.js +8 -2
  224. package/dist/services/run-controller.js +215 -94
  225. package/dist/services/run-fork.js +1 -1
  226. package/dist/services/run-recovery-guard.js +30 -0
  227. package/dist/services/run-recovery.js +11 -44
  228. package/dist/services/run-start-custody.js +17 -5
  229. package/dist/services/runs.js +6 -1
  230. package/dist/services/runtime-scope-resume.js +12 -6
  231. package/dist/services/runtime-scope-stop.js +3 -2
  232. package/dist/services/runtime-scope-worker.js +46 -19
  233. package/dist/services/runtime-service.js +2 -1
  234. package/dist/services/sessions.js +3 -24
  235. package/dist/services/setup.js +63 -13
  236. package/dist/services/stage-handlers.js +22 -7
  237. package/dist/services/stage-pause.js +111 -44
  238. package/dist/services/version-status.js +1 -1
  239. package/dist/services/vnext-code.js +2 -2
  240. package/dist/services/vnext-fanout.js +51 -9
  241. package/dist/services/vnext-plan-review.js +3 -5
  242. package/dist/services/work-registry.js +117 -39
  243. package/dist/services/workspace-bootstrap.js +2 -1
  244. package/dist/services/workspace-files.js +1 -1
  245. package/dist/shared/entity-references.js +1 -8
  246. package/dist/storage/data-preservation.js +65 -0
  247. package/dist/storage/database.js +53 -19
  248. package/dist/storage/import-guard.js +84 -27
  249. package/dist/storage/memory-upgrade-schema-bootstrap.js +38 -0
  250. package/dist/storage/paired-writer-migration.js +424 -0
  251. package/dist/storage/writer-contract.js +11 -4
  252. package/dist/storage/writer-migration.js +313 -52
  253. package/dist/storage/writer-restore.js +217 -0
  254. package/package.json +13 -6
@@ -0,0 +1,71 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { AppError } from "../shared/errors.js";
5
+ import { publishImmutableInput } from "../storage/immutable-input.js";
6
+ const digest = (text) => crypto.createHash("sha256").update(text).digest("hex");
7
+ /** Describe the exact delivery before the existing operation receipt is settled.
8
+ * This does not issue commands or replace the original semantic prompt. */
9
+ export function preparePromptDelivery(value) {
10
+ if (!value || typeof value !== "object" || Array.isArray(value))
11
+ return value;
12
+ const packet = value, text = packet.worker_prompt_markdown;
13
+ if (typeof text !== "string" || typeof packet.prompt_path !== "string")
14
+ return value;
15
+ if (!path.isAbsolute(packet.prompt_path))
16
+ throw new AppError("prompt_delivery_invalid", "Prompt delivery requires an absolute original prompt path", 1, { effect: "committed" });
17
+ const sha256 = digest(text);
18
+ let file = path.join(path.dirname(packet.prompt_path), "prompt-deliveries", `${sha256}.md`);
19
+ try {
20
+ if (fs.lstatSync(packet.prompt_path).isFile() && !fs.lstatSync(packet.prompt_path).isSymbolicLink()
21
+ && fs.readFileSync(packet.prompt_path, "utf8") === text)
22
+ file = packet.prompt_path;
23
+ }
24
+ catch { /* A retained delivery can republish an absent original without replay. */ }
25
+ const retained = packet.prompt_delivery;
26
+ if (retained && (retained.sha256 !== sha256 || retained.path !== file)) {
27
+ // An original file may disappear after commit. Keep the exact retained locator.
28
+ if (retained.sha256 !== sha256 || !path.isAbsolute(retained.path)
29
+ || retained.path !== packet.prompt_path && retained.path !== path.join(path.dirname(packet.prompt_path), "prompt-deliveries", `${sha256}.md`)) {
30
+ throw new AppError("prompt_delivery_invalid", "Retained delivery differs from its committed bytes", 1, { effect: "committed" });
31
+ }
32
+ file = retained.path;
33
+ }
34
+ const delivery = { path: file, sha256 };
35
+ // Keep the locator visible even when native tools clip verbose JSON metadata.
36
+ return Object.assign({ prompt_delivery: delivery }, packet, { prompt_delivery: delivery });
37
+ }
38
+ /** Publish only retained bytes, after commit and before advertising the locator. */
39
+ export function publishPromptDelivery(value) {
40
+ if (!value || typeof value !== "object")
41
+ return;
42
+ const packet = value, delivery = packet.prompt_delivery;
43
+ if (!delivery)
44
+ return;
45
+ const text = packet.worker_prompt_markdown;
46
+ try {
47
+ if (typeof text !== "string" || digest(text) !== delivery.sha256 || !path.isAbsolute(delivery.path))
48
+ throw new Error("Delivery checksum mismatch");
49
+ if (typeof packet.prompt_path !== "string" || !path.isAbsolute(packet.prompt_path))
50
+ throw new Error("Delivery has no original prompt directory");
51
+ // Revalidate retained routing before touching the filesystem. System path
52
+ // aliases (/var on macOS) are allowed; a delivery-directory symlink is not.
53
+ preparePromptDelivery(packet);
54
+ const directory = path.dirname(delivery.path);
55
+ if (delivery.path !== packet.prompt_path) {
56
+ const stat = fs.lstatSync(directory, { throwIfNoEntry: false });
57
+ if (stat && (!stat.isDirectory() || stat.isSymbolicLink()))
58
+ throw new Error("Delivery directory is not an owned regular directory");
59
+ }
60
+ fs.mkdirSync(path.dirname(delivery.path), { recursive: true, mode: 0o700 });
61
+ if (delivery.path !== packet.prompt_path
62
+ && fs.realpathSync(directory) !== path.join(fs.realpathSync(path.dirname(packet.prompt_path)), "prompt-deliveries"))
63
+ throw new Error("Delivery directory escaped its original prompt directory");
64
+ publishImmutableInput(delivery.path, Buffer.from(text), "prompt_delivery_conflict");
65
+ fs.accessSync(delivery.path, fs.constants.R_OK);
66
+ }
67
+ catch (cause) {
68
+ throw new AppError("prompt_delivery_pending", "Operation committed; reconcile its retained delivery before continuing. Do not repeat the lifecycle operation or execute the old prompt", 1, { effect: "committed", publication_pending: true, path: delivery.path, sha256: delivery.sha256, cause: cause instanceof Error ? cause.message : String(cause) });
69
+ }
70
+ }
71
+ export const promptDeliveryInstruction = "Read the complete plain Markdown file at prompt_delivery.path; it is the exact current instruction packet (SHA-256 in prompt_delivery.sha256). Read all chunks if needed. The JSON response is routing metadata, not the primary prompt. If prompt_delivery is absent on a legacy response, read the complete worker_prompt_markdown from response_file. If publication is pending, stop and reconcile the existing operation; never replay Stage/Work start or use an old prompt.";
@@ -1,8 +1,52 @@
1
1
  import { setTimeout as delay } from "node:timers/promises";
2
2
  import crypto from "node:crypto";
3
- import { CAPACITY_POLICY, MAX_OVERLOAD_RETRIES, capacityBackoff, nativeItemsSettled, normalizeProviderFailure, overloadExhausted, parseCapacityOperation, refusalObservation, terminalProviderOverload } from "../harness-runtime/lib/provider-overload-policy.mjs";
3
+ import { CAPACITY_POLICY, MAX_OVERLOAD_RETRIES, capacityBackoff, nativeItemsSettled, normalizeProviderFailure, overloadExhausted, parseCapacityOperation, refusalObservation, terminalProviderOverload, streamRequestKey } from "../harness-runtime/lib/provider-overload-policy.mjs";
4
4
  import { AppError } from "../shared/errors.js";
5
5
  import { isObservationLoss } from "../harness-runtime/lib/operation-errors.mjs";
6
+ import { parseLifecycleCommand, commandOption } from "./lifecycle-command.js";
7
+ import { nativeTreeReadiness } from "../harness-runtime/lib/native-children.mjs";
8
+ import { ObservationClock } from "../harness-runtime/lib/observation-clock.mjs";
9
+ /** Native output is evidence, not authority: match the committed current scope. */
10
+ export function ownedProviderBoundary(context, input, boundary) {
11
+ if (typeof boundary.native_hook_event_key !== "string" || typeof boundary.daemon_instance !== "string"
12
+ || !context.db.get("SELECT 1 FROM hook_events WHERE project_id = ? AND event_key = ? AND daemon_id = ? AND provider_session_id = ? AND status = 'claimed'", [input.projectId, boundary.native_hook_event_key, boundary.daemon_instance, input.sessionId]))
13
+ return false;
14
+ const invocation = context.db.get("SELECT scope_json, outcome_json, command FROM lifecycle_invocations WHERE event_key = ? AND status = 'settled'", [boundary.native_hook_event_key]);
15
+ if (!invocation)
16
+ return false;
17
+ const scope = JSON.parse(invocation.scope_json);
18
+ const outcome = JSON.parse(invocation.outcome_json);
19
+ if (scope.runId !== input.runId || scope.daemonId !== boundary.daemon_instance || scope.rootSessionId !== input.sessionId || scope.generation !== input.generation
20
+ || outcome.error || outcome.effect !== undefined && outcome.effect !== "committed" || outcome.result?.ok !== true
21
+ || outcome.result.effect !== undefined && outcome.result.effect !== "committed")
22
+ return false;
23
+ const parsed = parseLifecycleCommand(invocation.command);
24
+ if (parsed.kind === "none" || parsed.kind === "compound" && !(parsed.supported === true && parsed.invocations?.length === 1) || !["stage_pause", "stage_finish", "work_finish"].includes(parsed.invocation.operation)
25
+ || boundary.outcome === "completed" && parsed.invocation.operation === "stage_pause" || boundary.outcome === "paused" && parsed.invocation.operation !== "stage_pause"
26
+ || boundary.lifecycle_operation !== undefined && boundary.lifecycle_operation !== parsed.invocation.operation
27
+ || parsed.invocation.operation !== "work_finish" && commandOption(parsed.invocation, "stage") !== input.stage)
28
+ return false;
29
+ const result = outcome.result;
30
+ if (result.run_id !== undefined && result.run_id !== input.runId || result.stage !== undefined && result.stage !== input.stage
31
+ || boundary.outcome === "paused" && (result.pause?.id !== boundary.pause?.id || result.outcome !== "paused")
32
+ || boundary.work_id !== undefined && result.work_id !== undefined && boundary.work_id !== result.work_id)
33
+ return false;
34
+ if (boundary.run_id !== undefined && boundary.run_id !== input.runId || boundary.stage !== undefined && boundary.stage !== input.stage
35
+ || boundary.work_id !== undefined && input.workId && boundary.work_id !== input.workId)
36
+ return false;
37
+ const run = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [input.projectId, input.runId]);
38
+ const index = JSON.parse(run?.index_json ?? "null");
39
+ const stage = index?.stage_runs?.find(item => item.stage === input.stage);
40
+ if (!stage?.attempt)
41
+ return false;
42
+ const workId = input.workId ?? (typeof boundary.work_id === "string" ? boundary.work_id : null);
43
+ const rows = context.db.all(`SELECT DISTINCT w.work_id, w.status FROM works w JOIN work_sessions ws ON ws.work_id = w.work_id JOIN sessions s ON s.project_id = w.project_id AND s.session_id = ws.session_id JOIN stage_work_memberships m ON m.project_id = w.project_id AND m.run_id = w.run_id AND m.work_id = w.work_id WHERE w.project_id = ? AND w.run_id = ? AND ws.status IN ('running', 'completed') AND s.provider_session_id = ? AND m.stage = ? AND m.attempt = ? AND (? IS NULL OR w.work_id = ?) ${!workId && input.scope === "stage" ? "AND w.parent_work_id IS NULL" : ""}`, [input.projectId, input.runId, input.sessionId, input.stage, stage.attempt, workId, workId]);
44
+ if (rows.length !== 1 || boundary.work_id !== undefined && boundary.work_id !== rows[0].work_id)
45
+ return false;
46
+ if (boundary.outcome === "paused")
47
+ return rows[0].status === "paused" && stage.pause?.work_id === rows[0].work_id && stage.pause.id === boundary.pause?.id;
48
+ return boundary.outcome === "completed" && (input.scope === "work" ? rows[0].status === "completed" : stage.status === "done");
49
+ }
6
50
  export function providerChildFailure(child) {
7
51
  return child.provider_failure ?? { code: child.native_source ? "provider_capacity_transient" : "turn_interrupted", details: { native_source: child.native_source, provider_session_id: child.session_id, turn_id: child.terminal_turn_id, native_turn_id: child.terminal_turn_id, terminal_status: child.status, observed_at: child.terminal_observed_at ?? null, provider_error: child.provider_error, native_turn_items: child.native_turn_items } };
8
52
  }
@@ -12,26 +56,9 @@ export function eligibleProviderChildOverload(child, parentSessionId) {
12
56
  }
13
57
  /** A terminal root is not a settled tree. Only the daemon's exact owned tree
14
58
  * with a settled root and positively live descendants permits waiting. */
15
- export function capacityReadiness(seen, sessionId, operationId, turnId, items, allowDescendants = false) {
16
- if (seen.provider_session_id !== sessionId || seen.terminal_turn_id !== undefined && seen.terminal_turn_id !== turnId || !nativeItemsSettled(items))
17
- return "unproven";
18
- const ids = Array.isArray(seen.tree_session_ids) ? seen.tree_session_ids : [];
19
- if (!ids.length || ids[0] !== sessionId || ids.some(id => typeof id !== "string" || !id) || new Set(ids).size !== ids.length)
20
- return "unproven";
21
- const observations = Array.isArray(seen.tree_observations) ? seen.tree_observations : [];
22
- const exactTree = observations.length === ids.length && ids.every(id => observations.filter(item => item.provider_session_id === id).length === 1);
23
- const settlement = seen.settlement;
24
- const budgetSettled = !settlement || settlement.state === "not_required" || settlement.state === "settled" && settlement.operation_ids?.includes(operationId) === true;
25
- const treeSettled = allowDescendants ? exactTree && observations.every(item => item.settled === true) && Array.isArray(seen.unsettled_sessions) && seen.unsettled_sessions.length === 0 : ids.length === 1;
26
- if (seen.settled === true && treeSettled && budgetSettled)
27
- return "ready";
28
- if (!allowDescendants || seen.settled !== false || !exactTree || ids.length < 2 || observations[0]?.provider_session_id !== sessionId || observations[0].settled !== true
29
- || settlement && settlement.state !== "not_required" && !(settlement.state === "pending" && settlement.reason === "tree_unsettled"))
30
- return "unproven";
31
- const live = observations.filter(item => item.settled === false);
32
- const unsettled = seen.unsettled_sessions;
33
- const knownLive = live.length > 0 && live.every(item => item.provider_session_id !== sessionId && ["active", "running", "inProgress"].includes(String(typeof item.status === "object" && item.status ? item.status.type : item.status)));
34
- return knownLive && observations.every(item => typeof item.settled === "boolean") && Array.isArray(unsettled) && unsettled.length === live.length && live.every(item => unsettled.includes(item.provider_session_id)) ? "owned_children_live" : "unproven";
59
+ export function capacityReadiness(seen, sessionId, operationId, turnId, items, allowDescendants = false, requestProof) {
60
+ const readiness = nativeTreeReadiness(seen, { sessionId, operationId, turnId, allowDescendants, requestProof });
61
+ return readiness === "inspection_pending" ? readiness : nativeItemsSettled(items) ? readiness : "unproven";
35
62
  }
36
63
  /** Permit acquisition must still belong to the original task, not merely an
37
64
  * open RUN. Controls/inspection and ordinary successor prompts are unaffected. */
@@ -237,9 +264,13 @@ export async function continueProviderCapacity(input) {
237
264
  const decide = async (observedOperationId = operationId) => {
238
265
  input.signal?.throwIfAborted();
239
266
  input.assertCurrent();
240
- const decision = await observe(async () => await input.decision?.() ?? "continue", observedOperationId);
267
+ const normalized = normalizeProviderFailure(error, input.sessionId);
268
+ const candidate = normalized?.request_proof.kind === "stream_request" ? normalized.primary_error.details?.recovered_error_boundary : undefined;
269
+ const decision = await observe(async () => await input.decision?.(candidate?.receipt) ?? "continue", observedOperationId);
241
270
  input.signal?.throwIfAborted();
242
271
  input.assertCurrent();
272
+ if (normalized?.request_proof.kind === "stream_request" && ["completed", "paused"].includes(decision) && !candidate?.receipt)
273
+ throw blocked("Stream refusal has no ordered evidence of recovery before the committed boundary");
243
274
  if (decision === "reconciliation_blocked")
244
275
  throw new AppError("capacity_continuation_blocked", "Native refusal cannot be reconciled with its durable owner boundary", 1, { primary_error: error, root_operation_id: rootId, provider_session_id: input.sessionId });
245
276
  if (decision === "continue" && input.deadline !== undefined && Date.now() >= input.deadline)
@@ -254,9 +285,11 @@ export async function continueProviderCapacity(input) {
254
285
  return null;
255
286
  throw error;
256
287
  }
257
- const completed = await decide();
258
- if (completed)
259
- return completed;
288
+ if (normalizeProviderFailure(error, input.sessionId)?.request_proof.kind !== "stream_request") {
289
+ const completed = await decide();
290
+ if (completed)
291
+ return completed;
292
+ }
260
293
  const prior = await observe(() => input.inspectOperation(operationId));
261
294
  const failure = prior.error;
262
295
  const confirmed = normalizeProviderFailure(failure, input.sessionId);
@@ -266,25 +299,71 @@ export async function continueProviderCapacity(input) {
266
299
  if (prior.primary_error)
267
300
  throw prior.primary_error;
268
301
  if (prior.operation_id !== operationId || prior.operation !== "session.prompt" || prior.session_id !== input.sessionId
269
- || prior.state !== "failed" || prior.primary_error || !turnId || !terminalProviderOverload(failure, input.sessionId, turnId)) {
302
+ || prior.state !== "failed" || prior.primary_error || !turnId || !terminalProviderOverload(failure, input.sessionId, turnId)
303
+ || confirmed?.request_proof.kind === "stream_request" && confirmed.request_proof.operation_id !== operationId) {
270
304
  throw blocked("Retained predecessor does not prove the exact terminal overloaded Turn");
271
305
  }
306
+ let pendingClock;
272
307
  const readiness = async () => {
273
308
  input.signal?.throwIfAborted();
274
309
  input.assertCurrent();
275
- const seen = await observe(() => input.inspectSession(turnId));
310
+ const seen = await observe(() => input.inspectSession(confirmed?.request_proof.kind === "stream_request" ? undefined : turnId));
276
311
  input.signal?.throwIfAborted();
277
312
  input.assertCurrent();
278
313
  // An unloaded exact-Turn snapshot is not conflicting tool evidence.
279
314
  // Preserve the bound failure receipt, but never hide a pending item.
280
315
  const freshItems = seen.native_turn_items;
281
- const items = (seen.terminal_turn_id === turnId && seen.terminal_status === "failed"
316
+ const stream = confirmed?.request_proof.kind === "stream_request";
317
+ const streamMetadata = seen.terminal_metadata;
318
+ const exactFresh = stream ? streamRequestKey(streamMetadata?.request_proof) === turnId : seen.terminal_turn_id === turnId && seen.terminal_status === "failed";
319
+ const items = (exactFresh
282
320
  && !(freshItems?.observed === false && freshItems.pending !== true) ? freshItems : confirmed?.native_turn_items);
283
- return capacityReadiness(seen, input.sessionId, operationId, turnId, items, input.allowSettledDescendants);
321
+ if (confirmed?.request_proof.kind === "stream_request") {
322
+ const metadata = seen.terminal_metadata;
323
+ if (streamRequestKey(metadata?.request_proof) !== turnId)
324
+ return "unproven";
325
+ }
326
+ const current = capacityReadiness(seen, input.sessionId, operationId, turnId, items, input.allowSettledDescendants, confirmed?.request_proof);
327
+ if (current === "inspection_pending") {
328
+ const saved = seen.native_settlement.observation_clock;
329
+ pendingClock ??= new ObservationClock({ timeoutMs: saved.timeout_ms, saved, snapshotIsLive: true });
330
+ if (pendingClock.sample(saved.cursor, saved.observed_at ?? undefined))
331
+ return "unproven";
332
+ }
333
+ return current;
284
334
  };
285
- const ready = await readiness();
335
+ // Pending is a bound observer, not a refusal or an intent to write. Resolve
336
+ // it before retaining a productive retry. Its producer owns the old clock.
337
+ const waitForReadiness = async (initial) => {
338
+ let current = initial;
339
+ while (current === "owned_children_live" || current === "inspection_pending") {
340
+ await delay(250, undefined, { signal: input.signal });
341
+ const boundary = await decide();
342
+ if (boundary)
343
+ return boundary;
344
+ current = await readiness();
345
+ }
346
+ if (current !== "ready")
347
+ throw blocked("Native predecessor readiness became unproven");
348
+ return null;
349
+ };
350
+ let ready = await readiness();
351
+ while (ready === "inspection_pending") {
352
+ await delay(250, undefined, { signal: input.signal });
353
+ const boundary = await decide();
354
+ if (boundary)
355
+ return boundary;
356
+ ready = await readiness();
357
+ }
286
358
  if (ready === "unproven")
287
359
  throw blocked("Native predecessor readiness is unproven; do not cancel or replay to manufacture proof");
360
+ if (ready === "ready") {
361
+ const completed = await decide();
362
+ if (completed)
363
+ return completed;
364
+ if (confirmed?.request_proof.kind === "stream_request" && confirmed.primary_error.details?.recovered_error_boundary)
365
+ throw blocked("Recovered native error does not match its current durable owner boundary");
366
+ }
288
367
  const native = normalizeProviderFailure(failure, input.sessionId, turnId);
289
368
  let refusal = refusals.find(item => item.ordinal === ordinal - 1);
290
369
  if (refusal && (refusal.turn_id !== turnId || refusal.session_id !== input.sessionId || refusal.policy !== CAPACITY_POLICY))
@@ -332,26 +411,13 @@ export async function continueProviderCapacity(input) {
332
411
  throw new AppError("capacity_owner_deadline_exceeded", "Continuation wait exceeds its original owner deadline", 1, { primary_error: failure, deadline: input.deadline ?? null });
333
412
  }
334
413
  if (!existing) {
335
- const intent = { predecessor: operationId, operation_id: nextId, turn_id: turnId, ordinal, prompt_sha256: promptSha, not_before: new Date(Date.now() + wait).toISOString(), policy: CAPACITY_POLICY, root_operation_id: rootId, session_id: input.sessionId, deadline: input.deadline ?? null, refusal };
414
+ const intent = { predecessor: operationId, operation_id: nextId, turn_id: turnId, request_key: native.request_key, request_proof: native.request_proof, ordinal, prompt_sha256: promptSha, not_before: new Date(Date.now() + wait).toISOString(), policy: CAPACITY_POLICY, root_operation_id: rootId, session_id: input.sessionId, deadline: input.deadline ?? null, refusal };
336
415
  input.retainIntent(intent);
337
416
  history.push(intent);
338
417
  }
339
418
  // Intent/refusal are durable before the first wait; no permit or writer is
340
419
  // held here, and live siblings are never cancelled as a settlement proof.
341
- const waitForOwnedChildren = async (initial) => {
342
- let current = initial;
343
- while (current === "owned_children_live") {
344
- await delay(250, undefined, { signal: input.signal });
345
- const boundary = await decide();
346
- if (boundary)
347
- return boundary;
348
- current = await readiness();
349
- }
350
- if (current !== "ready")
351
- throw blocked("Owned native child readiness became unproven");
352
- return null;
353
- };
354
- const childBoundary = await waitForOwnedChildren(ready);
420
+ const childBoundary = await waitForReadiness(ready);
355
421
  if (childBoundary)
356
422
  return childBoundary;
357
423
  // Node timers truncate large delays. Bounded slices preserve cancellation
@@ -366,7 +432,7 @@ export async function continueProviderCapacity(input) {
366
432
  const afterWait = await decide();
367
433
  if (afterWait)
368
434
  return afterWait;
369
- const refreshedBoundary = await waitForOwnedChildren(await readiness());
435
+ const refreshedBoundary = await waitForReadiness(await readiness());
370
436
  if (refreshedBoundary)
371
437
  return refreshedBoundary;
372
438
  const beforeDispatch = await decide();
@@ -74,7 +74,7 @@ export function captureRecoveryDatabase(source, output, projectId, runId, protoc
74
74
  const bootstrapReceipts = `EXISTS (SELECT 1 FROM run_bundle_authorities b JOIN runs r
75
75
  ON r.project_id=b.project_id AND r.id=b.run_id
76
76
  WHERE (b.project_id,b.run_id) IN (SELECT * FROM capture_scope)
77
- AND json_extract(r.index_json,'$.execution_bundle.flow_id')='mb-init'
77
+ AND json_extract(r.index_json,'$.execution_bundle.flow_id') IN ('mb-init','mb-upgrade')
78
78
  AND (audit_events.project_id IS NULL OR audit_events.project_id=b.project_id)
79
79
  AND (
80
80
  (event_type IN ('memory.init_git_policy','memory.init_release_selection','memory.init_startup_input','memory.init_startup_input_intent','memory.init_project_bootstrap_intent',
@@ -109,6 +109,33 @@ export function captureRecoveryDatabase(source, output, projectId, runId, protoc
109
109
  AND ((event_type='memory.bootstrap_ready' AND json_extract(payload_json,'$.schema_id')='dd-flow/memory-bootstrap-authority@1')
110
110
  OR (event_type='memory.bootstrap_readiness_intent' AND json_extract(payload_json,'$.schema_id')='dd-flow/memory-bootstrap-readiness-intent@1')))))`;
111
111
  rules.audit_events = `(${rules.audit_events}) OR (${bootstrapReceipts})`;
112
+ // Upgrade selection/storage bytes are request-owned historical evidence,
113
+ // not ambient Home configuration. Keep only the bundle's original scope.
114
+ const upgradeScope = `SELECT 1 FROM run_bundle_authorities b JOIN runs r
115
+ ON r.project_id=b.project_id AND r.id=b.run_id
116
+ WHERE (b.project_id,b.run_id) IN (SELECT * FROM capture_scope)
117
+ AND json_extract(r.index_json,'$.execution_bundle.flow_id')='mb-upgrade'`;
118
+ rules.runtime_config = `CASE WHEN key GLOB 'mb-upgrade.storage.*' THEN EXISTS (${upgradeScope}
119
+ AND key='mb-upgrade.storage.' || b.project_id || '.' || json_extract(b.admission_json,'$.executionScope.requestId')
120
+ AND json_extract(value_json,'$.project_id')=b.project_id
121
+ AND json_extract(value_json,'$.scope.requestId')=json_extract(b.admission_json,'$.executionScope.requestId')
122
+ AND json_extract(value_json,'$.scope.projectRoot')=json_extract(b.admission_json,'$.executionScope.projectRoot')
123
+ AND json_extract(value_json,'$.scope.workspaceRoot')=json_extract(b.admission_json,'$.executionScope.workspace')) ELSE 0 END`;
124
+ rules.audit_events = `(${rules.audit_events}) OR (event_type='memory.upgrade_storage_ready'
125
+ AND EXISTS (${upgradeScope} AND audit_events.project_id=b.project_id
126
+ AND json_extract(payload_json,'$.request_id')=json_extract(b.admission_json,'$.executionScope.requestId')
127
+ AND json_extract(payload_json,'$.project_root')=json_extract(b.admission_json,'$.executionScope.projectRoot')
128
+ AND json_extract(payload_json,'$.workspace_root')=json_extract(b.admission_json,'$.executionScope.workspace')))`;
129
+ const publication = `CASE WHEN key GLOB 'memory_upgrade_publication:*' THEN EXISTS (${upgradeScope}
130
+ AND json_extract(value_json,'$.binding.scope.project_id')=b.project_id
131
+ AND json_extract(value_json,'$.binding.scope.run_id')=b.run_id
132
+ AND json_extract(value_json,'$.binding.scope.stage')='upgrade') ELSE 0 END`;
133
+ rules.runtime_config = `(${rules.runtime_config}) OR (${publication})`;
134
+ rules.audit_events = `(${rules.audit_events}) OR (event_type IN ('memory.upgrade_publication_intent','memory.upgrade_publication_complete')
135
+ AND EXISTS (SELECT 1 FROM runtime_config WHERE ${publication}
136
+ AND key='memory_upgrade_publication:' || json_extract(audit_events.payload_json,'$.scope_sha256')
137
+ AND json_extract(value_json,'$.binding.scope.project_id')=audit_events.project_id
138
+ AND json_extract(value_json,'$.binding.scope.run_id')=json_extract(audit_events.payload_json,'$.run_id')))`;
112
139
  }
113
140
  // Batch and composition registration are portable RUN evidence, not a host lease. Keep their
114
141
  // independent receipt alongside the selected Works, never a neighboring
@@ -361,7 +388,7 @@ export function captureRecoveryDatabase(source, output, projectId, runId, protoc
361
388
  AND json_extract(payload_json,'$.readiness_sha256') NOT GLOB '*[^0-9a-f]*'
362
389
  AND EXISTS (SELECT 1 FROM runs r,json_each(r.index_json,'$.stage_runs') f,json_each(r.index_json,'$.stage_runs') v
363
390
  WHERE r.project_id=audit_events.project_id AND r.id=json_extract(audit_events.payload_json,'$.run_id')
364
- AND json_extract(r.index_json,'$.execution_bundle.flow_id')='mb-init'
391
+ AND json_extract(r.index_json,'$.execution_bundle.flow_id') IN ('mb-init','mb-upgrade')
365
392
  AND json_extract(r.index_json,'$.execution_bundle.manifest_sha256')=json_extract(audit_events.payload_json,'$.bundle_sha256')
366
393
  AND json_extract(f.value,'$.stage')='finish'
367
394
  AND json_extract(f.value,'$.attempt')=json_extract(audit_events.payload_json,'$.attempt')
@@ -379,12 +406,13 @@ export function captureRecoveryDatabase(source, output, projectId, runId, protoc
379
406
  AND json_extract(payload_json,'$.evidence_sha256') NOT GLOB '*[^0-9a-f]*'
380
407
  AND EXISTS (SELECT 1 FROM runs r,json_each(r.index_json,'$.stage_runs') f
381
408
  WHERE r.project_id=audit_events.project_id AND r.id=json_extract(audit_events.payload_json,'$.run_id')
382
- AND json_extract(r.index_json,'$.execution_bundle.flow_id')='mb-init'
409
+ AND json_extract(r.index_json,'$.execution_bundle.flow_id') IN ('mb-init','mb-upgrade')
383
410
  AND json_extract(r.index_json,'$.execution_bundle.manifest_sha256')=json_extract(audit_events.payload_json,'$.bundle_sha256')
384
411
  AND json_extract(f.value,'$.stage')='finish' AND json_extract(f.value,'$.status')='done'
385
412
  AND json_extract(f.value,'$.attempt')=json_extract(audit_events.payload_json,'$.attempt')
386
413
  AND json_extract(f.value,'$.input_sha256')=json_extract(audit_events.payload_json,'$.input_sha256')
387
- AND json_extract(f.value,'$.data_schema_id')='dd-flow/mb-init-finish-result@1'
414
+ AND json_extract(f.value,'$.data_schema_id')=CASE json_extract(r.index_json,'$.execution_bundle.flow_id')
415
+ WHEN 'mb-upgrade' THEN 'dd-flow/mb-upgrade-stage-result@1' ELSE 'dd-flow/mb-init-finish-result@1' END
388
416
  AND json_extract(f.value,'$.data_sha256')=json_extract(audit_events.payload_json,'$.result_sha256'))`;
389
417
  rules.audit_events = `(${rules.audit_events}) OR (${ownProject} AND (${memoryFinishAcceptance}))`;
390
418
  const descriptorRunSettlementReceipt = `event_type = 'flow_run.descriptor_run_settlement'
@@ -6,7 +6,9 @@ import { inspectReleaseArchive } from "./release-archive.js";
6
6
  import { verifyReleaseArchive } from "./release-manifest.js";
7
7
  import { acquireInstallationLock } from "./installation-lock.js";
8
8
  import { hybridDigest } from "./hybrid-contracts.js";
9
+ import { publishSetup } from "../harness-runtime/lib/setup-progress.mjs";
9
10
  const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
11
+ let cacheVerificationPass = 0;
10
12
  function refuse(reason) {
11
13
  throw new AppError("cache_integrity", "Release cache cannot be published", 1, { reason, phase: "cache_publication", effect: "no_effect", help: "setup" });
12
14
  }
@@ -33,6 +35,8 @@ function write(target, bytes, mode) {
33
35
  }
34
36
  }
35
37
  function retained(target, archive, entries) {
38
+ const observation = `release-cache:${++cacheVerificationPass}:${target}`;
39
+ publishSetup("probe_started", observation);
36
40
  if (!fs.lstatSync(target).isDirectory())
37
41
  refuse("unsafe_retained_cache");
38
42
  const archiveFile = path.join(target, "archive.tgz");
@@ -53,6 +57,7 @@ function retained(target, archive, entries) {
53
57
  directories.add(parts.slice(0, index).join("/"));
54
58
  }
55
59
  const seen = new Set();
60
+ let checked = 0;
56
61
  const visit = (root, relative) => {
57
62
  if (!fs.lstatSync(root).isDirectory())
58
63
  refuse("unsafe_retained_cache");
@@ -70,12 +75,15 @@ function retained(target, archive, entries) {
70
75
  if (!stat.isFile() || entry?.kind !== "file" || stat.size !== entry.bytes.length || digest(fs.readFileSync(full)) !== digest(entry.bytes) || (stat.mode & 0o111) !== (entry.mode & 0o111))
71
76
  refuse("retained_cache_changed");
72
77
  seen.add(logical);
78
+ if (++checked % 64 === 0)
79
+ publishSetup("identity_digest_progress", `${observation}:${checked}`);
73
80
  }
74
81
  }
75
82
  };
76
83
  visit(payload, "");
77
84
  if (seen.size !== entries.length)
78
85
  refuse("retained_cache_changed");
86
+ publishSetup("identity_digest_completed", observation);
79
87
  }
80
88
  /** Verify retained compressed and expanded bytes without executing or activating them. */
81
89
  export function verifyPublishedReleaseCache(target, archive) {
@@ -28,15 +28,4 @@ export function ensureRepairIntent(context, input, create) {
28
28
  return { schema_id: "dd-flow/continuation@1", kind: "repair_required", project_id: input.projectId, run_id: input.runId, stage: input.stage, attempt, identity, work_id: workId };
29
29
  });
30
30
  }
31
- export function isRegisteredRepair(context, value, scope) {
32
- if (!value || typeof value !== "object")
33
- return false;
34
- const receipt = value;
35
- if (receipt.schema_id !== "dd-flow/continuation@1" || receipt.kind !== "repair_required")
36
- return false;
37
- if (scope && (receipt.project_id !== scope.projectId || receipt.run_id !== scope.runId))
38
- return false;
39
- if (!context.db.get("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'repair_intents'"))
40
- return false;
41
- return Boolean(context.db.get("SELECT 1 FROM repair_intents i JOIN works w ON w.work_id = i.work_id AND w.project_id = i.project_id AND w.run_id = i.run_id WHERE i.identity = ? AND i.project_id = ? AND i.run_id = ? AND i.stage = ? AND i.attempt = ? AND i.work_id = ?", [receipt.identity, receipt.project_id, receipt.run_id, receipt.stage, receipt.attempt, receipt.work_id]));
42
- }
31
+ export { isRegisteredRepair } from "./repair-receipt.js";
@@ -0,0 +1,13 @@
1
+ /** Read-only receipt validation must not load repair execution on hook admission. */
2
+ export function isRegisteredRepair(context, value, scope) {
3
+ if (!value || typeof value !== "object")
4
+ return false;
5
+ const receipt = value;
6
+ if (receipt.schema_id !== "dd-flow/continuation@1" || receipt.kind !== "repair_required")
7
+ return false;
8
+ if (scope && (receipt.project_id !== scope.projectId || receipt.run_id !== scope.runId))
9
+ return false;
10
+ if (!context.db.get("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'repair_intents'"))
11
+ return false;
12
+ return Boolean(context.db.get("SELECT 1 FROM repair_intents i JOIN works w ON w.work_id = i.work_id AND w.project_id = i.project_id AND w.run_id = i.run_id WHERE i.identity = ? AND i.project_id = ? AND i.run_id = ? AND i.stage = ? AND i.attempt = ? AND i.work_id = ?", [receipt.identity, receipt.project_id, receipt.run_id, receipt.stage, receipt.attempt, receipt.work_id]));
13
+ }
@@ -3,14 +3,14 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import os from "node:os";
5
5
  import { spawn } from "node:child_process";
6
- import { fileURLToPath } from "node:url";
7
6
  import { setTimeout as delay } from "node:timers/promises";
8
7
  import { AppError, errorRecord } from "../shared/errors.js";
9
8
  import { resolveProjectRoot } from "../storage/paths.js";
10
9
  import { requireProjectByRoot } from "./projects.js";
11
10
  import { getResourceDatabase } from "../storage/database.js";
12
- import { assertManagedProcessLease, confirmManagedProcess, finishManagedProcess, renewManagedProcess, managedProcessRuntimeHome, processIsAlive, processStartedAt, processTreeIsAlive, registerManagedProcess, resourceHome } from "./managed-processes.js";
11
+ import { assertManagedProcessLease, confirmManagedProcess, prepareManagedProcessConfirmation, finishManagedProcess, renewManagedProcess, managedProcessRuntimeHome, processIsAlive, processStartedAt, processTreeIsAlive, registerManagedProcess, resourceHome } from "./managed-processes.js";
13
12
  import { managedLeaseMonitor } from "./managed-lease-monitor.js";
13
+ import { physicalStartup } from "./physical-startup.js";
14
14
  import { interruptControlledSessions, observeControlledSessions, recordRunControlIntent, runControlStatus } from "./run-control.js";
15
15
  import { recoveryGuard, recordRecoveryCapture, sealRunRecovery } from "./run-recovery.js";
16
16
  import { createEvalRunSnapshot, verifyRecoverySnapshot } from "./eval-snapshots.js";
@@ -70,9 +70,7 @@ async function launchControlWorker(context, input) {
70
70
  if (!claimed && !reclaimControlWorker(context, input, project.id, token, home))
71
71
  return;
72
72
  try {
73
- const entry = fileURLToPath(new URL("../cli.js", import.meta.url));
74
- if (!fs.existsSync(entry))
75
- throw new AppError("harness_runtime_unbuilt", "Build the CLI before launching its detached control owner", 1);
73
+ const entry = (await import("./engines.js")).boundRunEngineEntrypoint(context, { projectRoot: project.root, runId: input.runId });
76
74
  fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
77
75
  const log = path.join(directory, "control.log");
78
76
  const output = fs.openSync(log, "a", 0o600);
@@ -228,18 +226,19 @@ export async function serveRunControl(context, input) {
228
226
  throw new AppError("run_control_owner_mismatch", "Control registration is not authorized", 1);
229
227
  // Physical identity is committed with the sole RUN claim, before the worker
230
228
  // registers itself in the separate resource store or performs any dispatch.
231
- const workerBirth = processStartedAt(process.pid);
232
- context.db.writeTransaction(() => {
233
- const claimed = context.db.run("UPDATE run_control_workers SET status = 'running', owner_pid = ?, updated_at = ? WHERE control_id = ? AND owner_token = ? AND status = 'starting' AND owner_pid IS NULL", [process.pid, context.now(), input.controlId, worker.owner_token]);
234
- if (claimed.changes !== 1)
235
- throw new AppError("run_control_already_owned", "A control owner has already claimed this request", 1);
236
- const now = context.now();
237
- context.db.run("INSERT INTO run_control_operations (operation_id, control_id, target_json, status, receipt_json, created_at, updated_at) VALUES (?, ?, ?, 'completed', ?, ?, ?)", [`${launchOperationId(input.controlId, worker.owner_token)}:worker-claim`, input.controlId, JSON.stringify({ kind: "control_worker_self_claim" }), JSON.stringify({ pid: process.pid, pid_started_at: workerBirth }), now, now]);
238
- });
229
+ const startup = physicalStartup(context.databaseDeadlineAt);
230
+ const assertStartupCurrent = () => {
231
+ const current = context.db.get("SELECT * FROM run_control_workers WHERE control_id = ?", [input.controlId]);
232
+ const currentIntent = context.db.get("SELECT * FROM run_controls WHERE control_id = ?", [input.controlId]);
233
+ const guard = recoveryGuard(context, project.id, input.runId);
234
+ if (!current || current.owner_token !== worker.owner_token || current.owner_pid !== worker.owner_pid || current.status !== worker.status || current.process_id !== worker.process_id || current.process_lease_token !== worker.process_lease_token || !["requested", "settled"].includes(currentIntent?.status ?? "") || guard?.recovery_id !== intent.recovery_id || !["draining", "sealed"].includes(guard.status))
235
+ throw new AppError("run_control_owner_mismatch", "Control startup no longer owns this request", 1);
236
+ };
239
237
  const resourceContext = { ...context, env: { ...context.env, DD_FLOW_RESOURCE_HOME: worker.resource_home } };
240
238
  const lease = managedLeaseMonitor(attemptMs => renewManagedProcess({ ...resourceContext, resourceDatabaseMode: "prepared_existing", databaseDeadlineAt: Date.now() + attemptMs }, { id: worker.process_id, leaseToken: worker.process_lease_token }));
241
239
  let timerFailure;
242
240
  let primaryFailure;
241
+ let claimedByThisServe = false;
243
242
  const assertControlCurrent = () => {
244
243
  const latest = recoveryGuard(context, project.id, input.runId);
245
244
  if (latest?.recovery_id !== intent.recovery_id || !["draining", "sealed"].includes(latest.status))
@@ -250,8 +249,9 @@ export async function serveRunControl(context, input) {
250
249
  assertManagedProcessLease(resourceContext, { id: worker.process_id, leaseToken: worker.process_lease_token, pid: process.pid });
251
250
  assertControlCurrent();
252
251
  } };
252
+ let admitted = false;
253
253
  const heartbeat = setInterval(() => {
254
- if (!worker.process_id)
254
+ if (!admitted)
255
255
  return;
256
256
  lease.background(assertControlCurrent, error => { timerFailure = error; });
257
257
  }, 10_000);
@@ -259,17 +259,33 @@ export async function serveRunControl(context, input) {
259
259
  let cleanupFailure;
260
260
  const result = await (async () => {
261
261
  try {
262
+ const workerBirth = (await startup.birth(assertStartupCurrent)).pid_started_at;
263
+ context.db.writeTransaction(() => {
264
+ assertStartupCurrent();
265
+ const claimed = context.db.run("UPDATE run_control_workers SET status = 'running', owner_pid = ?, updated_at = ? WHERE control_id = ? AND owner_token = ? AND status = 'starting' AND owner_pid IS NULL", [process.pid, context.now(), input.controlId, worker.owner_token]);
266
+ if (claimed.changes !== 1)
267
+ throw new AppError("run_control_already_owned", "A control owner has already claimed this request", 1);
268
+ const now = context.now();
269
+ context.db.run("INSERT INTO run_control_operations (operation_id, control_id, target_json, status, receipt_json, created_at, updated_at) VALUES (?, ?, ?, 'completed', ?, ?, ?)", [`${launchOperationId(input.controlId, worker.owner_token)}:worker-claim`, input.controlId, JSON.stringify({ kind: "control_worker_self_claim" }), JSON.stringify({ pid: process.pid, pid_started_at: workerBirth }), now, now]);
270
+ });
271
+ worker.owner_pid = process.pid;
272
+ worker.status = "running";
273
+ claimedByThisServe = true;
262
274
  if (!worker.process_id) {
263
275
  const log = path.join(worker.state_dir, "control.log");
264
276
  const record = registerManagedProcess(resourceContext, { kind: "run-control", ownerId: input.controlId, projectId: project.id, runId: input.runId, operationId: `control:${input.controlId}`, uniqueActiveOperation: true, ownerPid: process.pid, stdoutPath: log, stderrPath: log, metadata: { control_id: input.controlId } });
265
277
  worker.process_id = record.id;
266
278
  worker.process_lease_token = record.lease_token;
267
- context.db.run("UPDATE run_control_workers SET process_id = ?, process_lease_token = ?, updated_at = ? WHERE control_id = ? AND owner_token = ? AND owner_pid = ? AND status = 'running'", [record.id, record.lease_token, context.now(), input.controlId, worker.owner_token, process.pid]);
279
+ if (context.db.run("UPDATE run_control_workers SET process_id = ?, process_lease_token = ?, updated_at = ? WHERE control_id = ? AND owner_token = ? AND owner_pid = ? AND status = 'running'", [record.id, record.lease_token, context.now(), input.controlId, worker.owner_token, process.pid]).changes !== 1)
280
+ throw new AppError("run_control_owner_mismatch", "Control owner changed during registration", 1);
268
281
  }
269
282
  if (!worker.process_lease_token)
270
283
  throw new AppError("run_control_owner_mismatch", "Control owner has no resource lease", 1);
271
- const confirmed = confirmManagedProcess(resourceContext, { id: worker.process_id, leaseToken: worker.process_lease_token, pid: process.pid, processGroupId: process.pid, ownerPid: process.pid });
284
+ const confirmation = { id: worker.process_id, leaseToken: worker.process_lease_token, pid: process.pid, processGroupId: process.pid, ownerPid: process.pid };
285
+ const originalLease = prepareManagedProcessConfirmation(resourceContext, confirmation).lease_expires_at;
286
+ const confirmed = await startup.observe(() => confirmManagedProcess(resourceContext, confirmation), assertStartupCurrent, originalLease);
272
287
  lease.retainAuthority(confirmed.lease_expires_at);
288
+ admitted = true;
273
289
  let priorObservation = null;
274
290
  try {
275
291
  priorObservation = worker.snapshot_json ? JSON.parse(worker.snapshot_json) : null;
@@ -380,7 +396,7 @@ export async function serveRunControl(context, input) {
380
396
  catch (error) {
381
397
  primaryFailure = error;
382
398
  try {
383
- context.db.run("UPDATE run_control_workers SET status = 'failed', error_json = ?, updated_at = ? WHERE control_id = ? AND owner_token = ?", [JSON.stringify(failure(error)), context.now(), input.controlId, worker.owner_token]);
399
+ context.db.run("UPDATE run_control_workers SET status = 'failed', error_json = ?, updated_at = ? WHERE control_id = ? AND owner_token = ? AND owner_pid IS ? AND process_id IS ? AND process_lease_token IS ? AND status = ?", [JSON.stringify(failure(error)), context.now(), input.controlId, worker.owner_token, claimedByThisServe ? process.pid : null, worker.process_id, worker.process_lease_token, claimedByThisServe ? "running" : "starting"]);
384
400
  }
385
401
  catch (secondary) {
386
402
  process.stderr.write(`${JSON.stringify({ primary: failure(error), phase: "persist_control_failure", secondary: failure(secondary) })}\n`);
@@ -391,7 +407,8 @@ export async function serveRunControl(context, input) {
391
407
  lease.close();
392
408
  clearInterval(heartbeat);
393
409
  try {
394
- if (worker.process_id && worker.process_lease_token)
410
+ const current = claimedByThisServe && context.db.get("SELECT * FROM run_control_workers WHERE control_id = ?", [input.controlId]);
411
+ if (current && current.owner_token === worker.owner_token && current.owner_pid === process.pid && current.process_id === worker.process_id && current.process_lease_token === worker.process_lease_token && worker.process_id && worker.process_lease_token)
395
412
  finishManagedProcess(resourceContext, { id: worker.process_id, leaseToken: worker.process_lease_token, state: "stopped", reason: "control_owner_exited" });
396
413
  }
397
414
  catch (secondary) {
@@ -1,5 +1,6 @@
1
1
  import { setFlowRunRecoveryPaused } from "./runs.js";
2
2
  import crypto from "node:crypto";
3
+ import { nativeShutdownConfirmed } from "../harness-runtime/lib/native-children.mjs";
3
4
  import fs from "node:fs";
4
5
  import { ensureRunControlStorage } from "../storage/database.js";
5
6
  import { canonicalPath, resolveProjectRoot } from "../storage/paths.js";
@@ -414,7 +415,7 @@ async function reconcileOwnedSessions(context, input, interrupt, boundary) {
414
415
  const unfinished = receipts.filter(receipt => !["completed", "failed", "interrupted"].includes(String(receipt.state)));
415
416
  if (unfinished.some(receipt => receipt.daemon_id !== daemonId || String(receipt.operation) !== "session.create" || receipt.session_id !== null))
416
417
  throw new AppError("shutdown_operation_unsettled", "Closed daemon retains an unfinished non-create operation", 1);
417
- const stopped = receipts.find(receipt => receipt.daemon_id === daemonId && receipt.operation === "daemon.stop" && receipt.state === "completed" && receipt.result?.stopped === true && receipt.result?.clean === true);
418
+ const stopped = receipts.find(receipt => receipt.daemon_id === daemonId && receipt.operation === "daemon.stop" && receipt.state === "completed" && receipt.result?.stopped === true && nativeShutdownConfirmed(receipt.result, { daemonId }));
418
419
  if (!stopped)
419
420
  throw new AppError("shutdown_receipt_required", "Closed daemon has no clean native shutdown receipt for its current identity", 1);
420
421
  closedDaemons.set(directory, { source: "retained_clean_shutdown", daemon_id: daemonId, operation_id: stopped.operation_id, session_ids: state.sessions.map(item => typeof item === "string" ? item : adapterSessionId(item)), stopped: true, settled: true, processes: records.map(record => ({ process_id: record.id, pid: record.pid, pid_started_at: record.pid_started_at })), result: stopped.result });
@@ -12,6 +12,7 @@ import { canonicalNativeOperation } from "../harness-runtime/lib/adapter-timeout
12
12
  import { retainedDaemonReply } from "../harness-runtime/lib/daemon-observation.mjs";
13
13
  import { readOperationRecord } from "../harness-runtime/lib/operation-progress.mjs";
14
14
  import { readNativeDaemonState } from "./managed-daemon-binding.js";
15
+ import { validateCheckEnvironment } from "./check-environment.js";
15
16
  /** A paused RUN admits only its controller's exact accepted answer Turn. */
16
17
  export function controllerHitlAdmission(context, record, input) {
17
18
  if (input.operation !== "session.prompt" || !input.operationId || !input.promptSha256)
@@ -76,7 +77,7 @@ export function sessionArguments(context, manifest, session) {
76
77
  return ["--dd-flow-bin", manifest.flow_executable, "--dd-flow-home", context.ddFlowHome, "--project-root", manifest.request.project_root, "--state-dir", session.state_dir, "--journal", session.journal, "--cwd", session.cwd, "--provider", profile.provider, "--model", profile.model, "--reasoning", profile.reasoning, "--mode", profile.mode, "--permission", profile.permission, ...manifest.adapters[runtimeHarness(profile)].runtime_args, "--json"];
77
78
  }
78
79
  export function controllerEnvironment(context, manifest, session) {
79
- const env = { ...context.env, DD_FLOW_HOME: context.ddFlowHome, DD_FLOW_BIN: manifest.flow_executable, DD_FLOW_RESOURCE_HOME: manifest.resource_home, DD_FLOW_RUNTIME_BUDGET: manifest.budget ? JSON.stringify(manifest.budget) : undefined };
80
+ const env = { ...context.env, ...validateCheckEnvironment(manifest.check_environment), DD_FLOW_HOME: context.ddFlowHome, DD_FLOW_BIN: manifest.flow_executable, DD_FLOW_RESOURCE_HOME: manifest.resource_home, DD_FLOW_RUNTIME_BUDGET: manifest.budget ? JSON.stringify(manifest.budget) : undefined };
80
81
  for (const key of ["DD_FLOW_CONTROLLER_TOKEN", "DD_FLOW_CONTROL_TOKEN", "DD_FLOW_DAEMON_ID", "DD_FLOW_RUNTIME_OWNER", "DD_FLOW_WORK_ID", "DD_FLOW_SESSION_ID", "CODEX_THREAD_ID"])
81
82
  delete env[key];
82
83
  if (session?.codex_home)