@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
@@ -1,5 +1,6 @@
1
1
  import { hasLifecyclePacketAssignments, managedLifecycleCommand, recoverLifecycleCommands } from "./lifecycle-invocations.js";
2
- import { recoveryGuard, assertRunMutationAllowed } from "./run-recovery-guard.js";
2
+ import { recoveryGuard, recoveryPacketReceipt } from "./run-recovery-guard.js";
3
+ export { assertStageSettlementAllowed, assertWorkSettlementAllowed, recoveryPacketReceipt } from "./run-recovery-guard.js";
3
4
  export { recoveryGuard, assertRunMutationAllowed, assertRunRecoveryAllowed } from "./run-recovery-guard.js";
4
5
  import crypto from "node:crypto";
5
6
  import fs from "node:fs";
@@ -19,11 +20,6 @@ import { findRecentMatchingHookEvent, hookSessionIdentity, recoveryAcceptMatchKe
19
20
  import { flowCommand } from "./stage-pause.js";
20
21
  import { materializePendingWorkResults } from "./work-registry.js";
21
22
  import { currentStagePrompt } from "./stage-context.js";
22
- export function recoveryPacketReceipt(binding) {
23
- if (!binding.packet_json || !binding.packet_sha256 || crypto.createHash("sha256").update(binding.packet_json).digest("hex") !== binding.packet_sha256)
24
- throw new AppError("recovery_packet_missing", "Recovery requires its original verified Work packet", 1);
25
- return { packet_json: binding.packet_json, packet_sha256: binding.packet_sha256, packet: JSON.parse(binding.packet_json) };
26
- }
27
23
  /** Read-only custody permission for one prepared recovery ACK, not lifecycle admission. */
28
24
  export class PreparedRecoveryCoordinatorRead {
29
25
  #scope;
@@ -90,12 +86,16 @@ export function recoveredCoordinatorBinding(original, input) {
90
86
  export function capturedStageDelivery(capture, scope, originalPrompt) {
91
87
  const db = new DatabaseSync(path.join(capture, "runtime", "db.sqlite"), { readOnly: true });
92
88
  try {
89
+ const resumed = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='audit_events'").get()
90
+ ? db.prepare("SELECT json_extract(payload_json, '$.pause_id') AS pause_id FROM audit_events WHERE event_type = 'flow_run.stage_resumed' AND json_extract(payload_json, '$.run_id') = ? AND json_extract(payload_json, '$.work_id') = ? AND json_extract(payload_json, '$.stage') = ? ORDER BY id DESC LIMIT 1").get(scope.runId, scope.workId, scope.stage) : undefined;
93
91
  const rows = db.prepare("SELECT command, outcome_json FROM lifecycle_invocations WHERE status = 'settled' AND json_extract(scope_json, '$.projectRoot') = ? AND json_extract(scope_json, '$.runId') = ? AND json_extract(scope_json, '$.rootSessionId') = ? AND json_extract(scope_json, '$.generation') < ? ORDER BY rowid DESC").all(scope.projectRoot, scope.runId, scope.sessionId, scope.generation);
94
92
  for (const row of rows) {
95
93
  const result = row.outcome_json ? JSON.parse(row.outcome_json).result : null;
96
94
  const parsed = parseLifecycleCommand(row.command);
97
- if (parsed.kind === "none" || !parsed.supported || parsed.invocation.operation !== "stage_start" || parsed.invocation.args.options.get("stage")?.[0] !== scope.stage
98
- || result?.stage !== scope.stage || result.attempt !== scope.attempt || result.work_id !== scope.workId || typeof result.worker_prompt_markdown !== "string")
95
+ if (parsed.kind === "none" || !parsed.supported || !["stage_start", "stage_resume"].includes(parsed.invocation.operation) || parsed.invocation.args.options.get("stage")?.[0] !== scope.stage
96
+ || result?.stage !== scope.stage || parsed.invocation.operation === "stage_start" && result.attempt !== scope.attempt || result.work_id !== scope.workId || typeof result.worker_prompt_markdown !== "string")
97
+ continue;
98
+ if (resumed && (parsed.invocation.operation !== "stage_resume" || result.pause_id !== resumed.pause_id))
99
99
  continue;
100
100
  const text = result.worker_prompt_markdown;
101
101
  if (!text.startsWith(originalPrompt))
@@ -105,6 +105,8 @@ export function capturedStageDelivery(capture, scope, originalPrompt) {
105
105
  throw new AppError("recovery_packet_missing", "Captured Stage delivery has no committed lifecycle assignment", 1);
106
106
  return { text, sha256 };
107
107
  }
108
+ if (resumed)
109
+ throw new AppError("recovery_packet_missing", "Accepted HITL answer has no captured settled current delivery; reconcile its retained resume receipt, never fall back to the pre-answer prompt", 1, { work_id: scope.workId, pause_id: resumed.pause_id });
108
110
  return null;
109
111
  }
110
112
  finally {
@@ -174,42 +176,7 @@ export function prepareRecoveryPayload(value, label) {
174
176
  throw new AppError("recovery_not_settled", "Recovery capture requires an explicit settled tree receipt", 1);
175
177
  return payload;
176
178
  }
177
- /** A cooperative drain permits only the WorkSession present at its fence. */
178
- export function assertWorkSettlementAllowed(context, projectId, runId, workId) {
179
- const guard = recoveryGuard(context, projectId, runId);
180
- if (guard?.status === "draining" && context.db.get("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'run_controls'")) {
181
- const admission = context.db.get("SELECT a.work_session_id FROM run_control_admissions a JOIN run_controls c ON c.control_id = a.control_id JOIN work_sessions ws ON ws.id = a.work_session_id JOIN works w ON w.work_id = a.work_id WHERE c.project_id = ? AND c.run_id = ? AND c.recovery_id = ? AND c.action = 'pause' AND c.status = 'requested' AND a.work_id = ? AND ws.work_id = a.work_id AND ws.session_id = a.session_id AND ws.status = 'running' AND w.status = 'running' AND w.project_id = c.project_id AND w.run_id = c.run_id LIMIT 1", [projectId, runId, guard.recovery_id, workId]);
182
- if (admission)
183
- return;
184
- }
185
- assertRunMutationAllowed(context, projectId, runId);
186
- }
187
- /** Completion rights are pinned to the Stage attempt present at the pause fence. */
188
- export function assertStageSettlementAllowed(context, projectId, runId, stage, expectedGeneration) {
189
- const guard = recoveryGuard(context, projectId, runId);
190
- const generation = guard?.generation ?? 0;
191
- if (expectedGeneration !== undefined && generation !== expectedGeneration)
192
- throw new AppError("run_recovery_generation_stale", "RUN execution generation changed while the operation was in flight", 1, { run_id: runId, expected_generation: expectedGeneration, generation });
193
- if (guard?.status === "draining" && context.db.get("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'run_control_stage_admissions'")) {
194
- const admitted = context.db.get(`SELECT a.work_session_id FROM run_control_stage_admissions a JOIN run_controls c ON c.control_id = a.control_id JOIN run_control_admissions w ON w.control_id = a.control_id AND w.work_session_id = a.work_session_id JOIN work_sessions ws ON ws.id = w.work_session_id JOIN runs r ON r.project_id = c.project_id AND r.id = c.run_id, json_each(r.index_json, '$.stage_runs') live
195
- WHERE c.project_id = ? AND c.run_id = ? AND c.recovery_id = ? AND c.action = 'pause' AND c.status = 'requested' AND a.stage = ? AND json_extract(live.value, '$.stage') = a.stage AND COALESCE(json_extract(live.value, '$.attempt'), 'try-001') = a.attempt AND ws.work_id = w.work_id AND ws.session_id = w.session_id AND NOT EXISTS (SELECT 1 FROM work_sessions newer WHERE newer.work_id = w.work_id AND newer.status = 'running' AND newer.id <> ws.id) LIMIT 1`, [projectId, runId, guard.recovery_id, stage]);
196
- if (admitted)
197
- return generation;
198
- }
199
- return assertRunMutationAllowed(context, projectId, runId, expectedGeneration);
200
- }
201
- /** A recovered RUN never grants the retired process a new execution segment. */
202
- export function assertRecoveryHookCurrent(context, projectId, runId, identity) {
203
- const generation = assertRunMutationAllowed(context, projectId, runId);
204
- if (!generation)
205
- return;
206
- const current = identity.daemonId && context.db.get("SELECT b.recovery_id FROM run_recovery_bindings b JOIN run_recovery_guards g ON g.recovery_id = b.recovery_id WHERE g.project_id = ? AND g.run_id = ? AND g.generation = ? AND g.status = 'recovered' AND b.status = 'accepted' AND b.daemon_id = ? LIMIT 1", [projectId, runId, generation, identity.daemonId]);
207
- if (current)
208
- return;
209
- const retired = identity.daemonId && (context.db.get(`SELECT ws.id FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id JOIN hook_events he ON he.project_id = w.project_id AND he.event_key = ws.hook_event_id WHERE w.project_id = ? AND w.run_id = ? AND he.daemon_id = ? LIMIT 1`, [projectId, runId, identity.daemonId]) || context.db.get("SELECT b.recovery_id FROM run_recovery_bindings b JOIN run_recovery_guards g ON g.recovery_id = b.recovery_id WHERE g.project_id = ? AND g.run_id = ? AND b.daemon_id = ? AND (g.generation <> ? OR b.status = 'abandoned') LIMIT 1", [projectId, runId, identity.daemonId, generation]));
210
- if (!identity.daemonId || retired)
211
- throw new AppError("recovery_daemon_stale", "Lifecycle receipt belongs to an unidentified or retired recovery daemon", 1, { run_id: runId, generation, daemon_id: identity.daemonId });
212
- }
179
+ export { assertRecoveryHookCurrent } from "./hook-recovery-guard.js";
213
180
  export function beginRunRecovery(context, input, interruption = prepareRecoveryPayload(input.interruption, "interruption")) {
214
181
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
215
182
  const run = requireRecoveryRun(context, project.id, input.runId);
@@ -1,8 +1,12 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import path from "node:path";
3
3
  import { AppError } from "../shared/errors.js";
4
- import { hybridDigest } from "./hybrid-contracts.js";
4
+ import { hybridDigest } from "./hybrid-digest.js";
5
+ import { runContextRelocations, projectPacketLocators } from "./stage-context.js";
5
6
  export const runStartPayloadLimit = 64 * 1024 * 1024;
7
+ // One exact immutable payload per database, not cached custody or returned
8
+ // objects. Every read still checks current SQL hashes, markers and receipts.
9
+ const validatedPayloadBytes = new WeakMap();
6
10
  export function runStartBytesSha256(bytes) { return createHash("sha256").update(bytes).digest("hex"); }
7
11
  export function refuseRunStart(reason) {
8
12
  throw new AppError("run_start_custody_invalid", "RUN creation requires its exact retained request", 1, { reason, effect: "no_effect", next_action: "reconcile_exact_request" });
@@ -59,8 +63,11 @@ export function validateRunStartPayload(value) {
59
63
  || !entry.path || entry.path.includes("\\") || entry.path.includes("\0") || entry.path.split("/").some(part => !part || part === "." || part === "..")
60
64
  || seen.has(entry.path) || !canonicalBase64(entry.base64))
61
65
  refuseRunStart("invalid_allocation_assets");
62
- if (key === "artifacts" && (!["engine-binding.json", "operational-policy.source.md", "operational-contract.json"].includes(entry.path)
63
- || Buffer.from(entry.base64, "base64").length > 1024 * 1024))
66
+ const upgradeEvidence = value.initial_index.flow_kind === "mb-upgrade" && (["upgrade-baseline.json", "upgrade-target.json"].includes(entry.path)
67
+ || entry.path === `upgrade-target-${runStartBytesSha256(Buffer.from(entry.base64, "base64"))}.bin`
68
+ || entry.path === `upgrade-baseline-${runStartBytesSha256(Buffer.from(entry.base64, "base64"))}.bin`);
69
+ if (key === "artifacts" && (!["engine-binding.json", "operational-policy.source.md", "operational-contract.json"].includes(entry.path) && !upgradeEvidence
70
+ || Buffer.from(entry.base64, "base64").length > (upgradeEvidence ? 16 : 1) * 1024 * 1024))
64
71
  refuseRunStart("invalid_allocation_artifact");
65
72
  seen.add(entry.path);
66
73
  }
@@ -96,7 +103,10 @@ function readRunStartRequestSnapshot(context, projectId, requestId) {
96
103
  catch {
97
104
  return refuseRunStart("invalid_allocation_json");
98
105
  }
99
- validateRunStartPayload(payload);
106
+ if (validatedPayloadBytes.get(context.db) !== row.payload_json) {
107
+ validateRunStartPayload(payload);
108
+ validatedPayloadBytes.set(context.db, row.payload_json);
109
+ }
100
110
  if (!object(receipt))
101
111
  refuseRunStart("invalid_allocation_receipt");
102
112
  const run = context.db.get("SELECT project_root, workspace_root, run_root, flow_kind, subject_type, subject_id, index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, row.run_id]);
@@ -114,6 +124,8 @@ function readRunStartRequestSnapshot(context, projectId, requestId) {
114
124
  if (!object(index))
115
125
  refuseRunStart("allocated_run_changed");
116
126
  const marker = index.start_request, initial = payload.initial_index;
127
+ const relocations = runContextRelocations(context, projectId, row.run_id);
128
+ const currentPath = (value) => projectPacketLocators(value, relocations);
117
129
  if (!payload || payload.schema_id !== "dd-flow/run-start-allocation@1" || payload.project_id !== projectId
118
130
  || payload.request_id !== requestId || payload.run_id !== row.run_id || !Array.isArray(payload.blobs) || !Array.isArray(payload.artifacts)
119
131
  || !initial || initial.run_id !== row.run_id || initial.project?.id !== projectId || initial.start_request !== undefined
@@ -121,7 +133,7 @@ function readRunStartRequestSnapshot(context, projectId, requestId) {
121
133
  || !marker || marker.schema_id !== "dd-flow/run-start-request@1" || marker.request_id !== requestId
122
134
  || marker.request_sha256 !== payload.request_sha256 || marker.allocation_sha256 !== row.payload_sha256
123
135
  || receipt.run_id !== row.run_id || receipt.request_id !== requestId || receipt.request_sha256 !== payload.request_sha256 || receipt.allocation_sha256 !== row.payload_sha256
124
- || run.project_root !== payload.project_root || run.workspace_root !== payload.workspace_root || run.run_root !== payload.run_root
136
+ || run.project_root !== currentPath(payload.project_root) || run.workspace_root !== currentPath(payload.workspace_root) || run.run_root !== currentPath(payload.run_root)
125
137
  || run.flow_kind !== initial.flow_kind || run.subject_id !== payload.subject_id || run.subject_type !== payload.subject_type
126
138
  || !object(index.execution_bundle) || hybridDigest(index.execution_bundle) !== hybridDigest(initial.execution_bundle))
127
139
  refuseRunStart("allocated_run_changed");
@@ -27,6 +27,7 @@ import { RetainedExecutionBundle } from "./execution-bundles.js";
27
27
  import { admitBundledFlow, bundledHandlerContext } from "./stage-handlers.js";
28
28
  import { hybridDigest } from "./hybrid-contracts.js";
29
29
  import { projectPacketLocators, runContextRelocations } from "./stage-context.js";
30
+ import { readRunEngineBinding } from "./run-engine-bindings.js";
30
31
  const runSchemaId = "dd-flow/flow-run@3";
31
32
  const runtimeSchemaId = "dd-flow/flow-run@3";
32
33
  const allowedRunFlowKinds = [
@@ -36,6 +37,7 @@ const allowedRunFlowKinds = [
36
37
  "mb-init",
37
38
  "mb-upgrade",
38
39
  "mb-audit",
40
+ "project-review",
39
41
  "mb-distill",
40
42
  "mb-upgrade-review",
41
43
  "mb-sdlc-review",
@@ -1843,7 +1845,10 @@ export function readFlowRunExecutionBundle(context, input) {
1843
1845
  throw new AppError("run_bundle_authority_invalid", "RUN bundle admission authority changed", 1);
1844
1846
  }
1845
1847
  const admission = { ...retained, receipts: new Map(retained.receipts), descriptorContext: bundledHandlerContext(new Map()) };
1846
- const bundle = RetainedExecutionBundle.read(path.join(runArtifactRoot(run), "execution-bundle"), anchor.manifest_sha256, admission);
1848
+ const relocations = runContextRelocations(context, run.project_id, run.id);
1849
+ const binding = relocations.length ? readRunEngineBinding(path.join(runArtifactRoot(run), "engine-binding.json")) : null;
1850
+ const relocation = binding?.source === "fork_upgrade" ? { binding, relocations } : undefined;
1851
+ const bundle = RetainedExecutionBundle.read(path.join(runArtifactRoot(run), "execution-bundle"), anchor.manifest_sha256, admission, relocation);
1847
1852
  assertBundleRunIdentity(context, run, bundle);
1848
1853
  if (bundle.manifest.descriptor.sha256 !== anchor.descriptor_sha256 || bundle.manifest.flow_id !== anchor.flow_id) {
1849
1854
  throw new AppError("run_bundle_anchor_invalid", "RUN bundle descriptor identity changed", 1);
@@ -1,4 +1,5 @@
1
1
  import crypto from "node:crypto";
2
+ import { nativeShutdownConfirmed } from "../harness-runtime/lib/native-children.mjs";
2
3
  import fs from "node:fs";
3
4
  import path from "node:path";
4
5
  import { isDeepStrictEqual } from "node:util";
@@ -16,15 +17,19 @@ import { cleanupScopeWriters } from "./scope-writer-cleanup.js";
16
17
  import { harnessKey } from "./harness-config.js";
17
18
  import { canonicalNativeOperation } from "../harness-runtime/lib/adapter-timeouts.mjs";
18
19
  import { retainedDaemonReply } from "../harness-runtime/lib/daemon-observation.mjs";
20
+ import { validateNativeLaunchEnvironment } from "../harness-runtime/lib/daemon-operations.mjs";
19
21
  const hash = (value) => crypto.createHash("sha256").update(value).digest("hex");
20
- const nativeLaunchEnvironmentKeys = ["CODEX_HOME", "DD_FLOW_CONFIG_HOME", "DD_FLOW_HOME", "DD_FLOW_BIN", "DD_FLOW_RESOURCE_HOME"];
21
22
  function nativeLaunchEnvironment(context, saved) {
22
23
  const env = { ...context.env };
23
24
  for (const name of ["DD_FLOW_CONTROLLER_TOKEN", "DD_FLOW_CONTROL_TOKEN", "DD_FLOW_SCOPE_CONTROL_TOKEN", "DD_FLOW_DAEMON_ID", "DD_FLOW_RUNTIME_OWNER"])
24
25
  delete env[name];
25
- if (!saved || Object.keys(saved).some(name => !nativeLaunchEnvironmentKeys.includes(name)))
26
- throw new AppError("runtime_scope_native_launch_required", "Native launch has an unsupported environment", 1);
27
- for (const name of nativeLaunchEnvironmentKeys) {
26
+ try {
27
+ validateNativeLaunchEnvironment(saved);
28
+ }
29
+ catch {
30
+ throw new AppError("runtime_scope_native_launch_required", "Native launch has an unsupported or invalid configuration environment", 1);
31
+ }
32
+ for (const name of Object.keys(saved)) {
28
33
  if (saved[name] === null)
29
34
  delete env[name];
30
35
  else if (typeof saved[name] === "string")
@@ -460,8 +465,9 @@ async function retireScopeNative(context, intent, source, assertCurrent) {
460
465
  }
461
466
  if (!launch || launch.schema_id !== "dd-flow/native-launch@1" || launch.executable !== source.runtime_owner?.adapter_executable || !path.isAbsolute(launch.executable) || !path.isAbsolute(launch.cwd) || !Array.isArray(launch.args) || launch.args[0] !== "daemon" || launch.args[1] !== "start" || launch.args[launch.args.indexOf("--state-dir") + 1] !== source.state_dir || !isDeepStrictEqual(launchOwner, source.runtime_owner))
462
467
  throw new AppError("runtime_scope_native_launch_required", "Native recovery requires the original owned launch contract", 1);
463
- if (!launch.env || Object.keys(launch.env).some(name => !nativeLaunchEnvironmentKeys.includes(name)) || nativeLaunchEnvironmentKeys.some(name => launch.env[name] !== null && typeof launch.env[name] !== "string") || launch.args.some(arg => typeof arg !== "string"))
468
+ if (launch.args.some(arg => typeof arg !== "string"))
464
469
  throw new AppError("runtime_scope_native_launch_required", "Native recovery requires explicit configuration presence before retiring its owner", 1);
470
+ nativeLaunchEnvironment(context, launch.env);
465
471
  let dispatch = false;
466
472
  const preparedInventory = db.get("SELECT 1 FROM managed_resources WHERE resource_kind = 'scope-resume-native' AND resource_key = ?", [key]) ? null : nativeInventoryVersion(source.state_dir);
467
473
  const operation = db.writeTransaction(() => {
@@ -494,7 +500,7 @@ async function retireScopeNative(context, intent, source, assertCurrent) {
494
500
  assertOwner();
495
501
  db.run("UPDATE managed_resources SET metadata_json = ?, updated_at = ? WHERE resource_kind = 'scope-resume-native' AND resource_key = ?", [JSON.stringify(operation), context.now(), key]);
496
502
  }
497
- if (operation.receipt.stopped !== true || operation.receipt.clean !== true || state().shutdown_state !== "clean" || processTreeIsAlive(record))
503
+ if (operation.receipt.stopped !== true || !nativeShutdownConfirmed(operation.receipt, { daemonId: source.daemon_id }) || state().shutdown_state !== "clean" || processTreeIsAlive(record))
498
504
  return { ...source, ...operation, pending: true, reason: "native_retirement_settlement_required" };
499
505
  assertOwner();
500
506
  operation.status = "retired";
@@ -1,4 +1,5 @@
1
1
  import crypto from "node:crypto";
2
+ import { nativeShutdownConfirmed } from "../harness-runtime/lib/native-children.mjs";
2
3
  import fs from "node:fs";
3
4
  import path from "node:path";
4
5
  import os from "node:os";
@@ -30,7 +31,7 @@ export async function stopRuntimeScope(context, scopeId, requestId) {
30
31
  if (!state.daemon_id || !owner || owner.budget?.scope_id !== scopeId || owner.owner_id !== record.owner_id || owner.role !== metadata.role || owner.state_dir !== metadata.state_dir || owner.adapter_executable !== metadata.adapter_executable || !retainedResourceHomeMatches(context, owner.resource_home) || (owner.project_id ?? null) !== record.project_id || (owner.run_id ?? null) !== record.run_id || (owner.work_id ?? null) !== record.work_id || !record.operation_id?.startsWith(`${owner.operation_id}:`) || state.resource_process?.id !== record.id || state.resource_process.lease_token !== record.lease_token || state.pid !== record.pid)
31
32
  throw new AppError("runtime_scope_owner_unknown", "Native owner does not match its registration", 1);
32
33
  const closed = state.shutdown_state === "clean" && state.active_tree === false && !processTreeIsAlive(record)
33
- ? retainedDaemonOperations(metadata.state_dir).find(operation => operation.daemon_id === state.daemon_id && operation.operation === "daemon.stop" && operation.state === "completed" && operation.result?.stopped === true && operation.result?.clean === true) : undefined;
34
+ ? retainedDaemonOperations(metadata.state_dir).find(operation => operation.daemon_id === state.daemon_id && operation.operation === "daemon.stop" && operation.state === "completed" && operation.result?.stopped === true && nativeShutdownConfirmed(operation.result, { daemonId: state.daemon_id ?? "" })) : undefined;
34
35
  const operationId = closed ? String(closed.operation_id) : `scope-stop:${crypto.createHash("sha256").update(JSON.stringify([scopeId, record.id, state.daemon_id])).digest("hex")}`;
35
36
  const requested = path.join(metadata.state_dir, "operations", crypto.createHash("sha256").update(operationId).digest("hex"), "requested.json");
36
37
  const prior = fs.existsSync(requested);
@@ -56,7 +57,7 @@ export async function stopRuntimeScope(context, scopeId, requestId) {
56
57
  const finalOwner = managedProcessStatus(context).find(item => item.id === record.id);
57
58
  if (!finalOwner || finalOwner.lease_token !== record.lease_token || finalOwner.pid !== record.pid || finalOwner.pid_started_at !== record.pid_started_at || after.resource_process?.id !== record.id || after.resource_process.lease_token !== record.lease_token || JSON.stringify(after.config?.runtime_owner) !== JSON.stringify(owner))
58
59
  throw new AppError("runtime_scope_owner_changed", "Native ownership changed during control", 1);
59
- if (result?.stopped !== true || result.clean !== true || after.daemon_id !== state.daemon_id || after.shutdown_state !== "clean" || after.active_tree !== false || processTreeIsAlive(finalOwner))
60
+ if (result?.stopped !== true || !nativeShutdownConfirmed(result, { daemonId: state.daemon_id }) || after.daemon_id !== state.daemon_id || after.shutdown_state !== "clean" || after.active_tree !== false || processTreeIsAlive(finalOwner))
60
61
  throw new AppError("runtime_scope_not_settled", "Native close has not proved whole-tree settlement", 1);
61
62
  const processes = managedProcessStatus(context).filter(item => managedProviderBelongsToDaemon(item, finalOwner, after) && item.operation_id?.startsWith(`${owner.operation_id}:`) && !processTreeIsAlive(item)).map(item => ({ process_id: item.id, native_settled: true, binding: managedProcessBinding(item, resourceHome(context)) }));
62
63
  return { process_id: record.id, state_dir: metadata.state_dir, settled: true, native_settled: true, physical_stopped: true, operation_id: operationId, owner_operation_id: owner.operation_id, processes };
@@ -8,6 +8,7 @@ import { AppError, errorRecord } from "../shared/errors.js";
8
8
  import { getResourceDatabase } from "../storage/database.js";
9
9
  import { assertManagedProcessLease, confirmManagedProcess, finishManagedProcess, renewManagedProcess, processIsAlive, processStartedAt, registerManagedProcess, resourceHome } from "./managed-processes.js";
10
10
  import { managedLeaseMonitor } from "./managed-lease-monitor.js";
11
+ import { physicalStartup } from "./physical-startup.js";
11
12
  import { publicRuntimeScopeWorker, requestRuntimeScopeControl, runtimeBudgetStatus, prepareRuntimeScopeFence, parseRuntimeBudget } from "./runtime-budget.js";
12
13
  import { controlRuntimeScopeRuns } from "./runtime-scope-control.js";
13
14
  import { prepareRuntimeScopeResume, recordRuntimeScopeResume } from "./runtime-scope-resume.js";
@@ -138,8 +139,7 @@ export function prepareRuntimeScopeServing(context, scopeId, generation) {
138
139
  }
139
140
  return { key, token };
140
141
  }
141
- function requireStartingScopeWorker(db, key, token) {
142
- const row = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control-worker' AND resource_key = ? AND lease_token = ?", [key, token]);
142
+ function requireStartingScopeWorker(db, key, token, row = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control-worker' AND resource_key = ? AND lease_token = ?", [key, token])) {
143
143
  if (!row)
144
144
  throw new AppError("runtime_scope_owner_unknown", "Scope worker claim changed", 1);
145
145
  const worker = JSON.parse(row.metadata_json);
@@ -150,29 +150,55 @@ function requireStartingScopeWorker(db, key, token) {
150
150
  export async function serveRuntimeScopeControl(context, scopeId, generation) {
151
151
  const { key, token } = prepareRuntimeScopeServing(context, scopeId, generation);
152
152
  const db = getResourceDatabase(resourceHome(context));
153
- const owner = birth();
154
- const worker = db.writeTransaction(() => {
155
- const worker = requireStartingScopeWorker(db, key, token);
156
- worker.owner = owner;
157
- worker.status = "running";
158
- db.run("UPDATE managed_resources SET metadata_json = ?, updated_at = ? WHERE resource_kind = 'scope-control-worker' AND resource_key = ? AND lease_token = ?", [JSON.stringify(worker), context.now(), key, token]);
159
- return worker;
160
- });
153
+ const startup = physicalStartup(context.databaseDeadlineAt);
154
+ let claimedOwner = undefined;
155
+ const assertStartupCurrent = () => {
156
+ const control = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control' AND resource_key = ?", [scopeId]);
157
+ const fence = db.get("SELECT 1 FROM managed_resources WHERE resource_kind = 'provider-fence' AND resource_key = ?", [scopeId]);
158
+ const row = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control-worker' AND resource_key = ? AND lease_token = ?", [key, token]);
159
+ const current = row ? JSON.parse(row.metadata_json) : null;
160
+ if (!control || JSON.parse(control.metadata_json).generation !== generation || fence || !current || current.token !== token || current.status !== (claimedOwner ? "running" : "starting") || current.owner?.pid !== claimedOwner?.pid || current.owner?.pid_started_at !== claimedOwner?.pid_started_at)
161
+ throw new AppError("runtime_scope_control_superseded", "Scope startup no longer owns this control", 1);
162
+ };
163
+ const starting = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control-worker' AND resource_key = ? AND lease_token = ?", [key, token]);
164
+ let worker = requireStartingScopeWorker(db, key, token, starting);
165
+ let retainedMetadata = starting.metadata_json;
166
+ let claimedByThisServe = false;
167
+ const persistWorker = () => {
168
+ const next = JSON.stringify(worker);
169
+ if (db.run("UPDATE managed_resources SET metadata_json = ?, updated_at = ? WHERE resource_kind = 'scope-control-worker' AND resource_key = ? AND lease_token = ? AND metadata_json = ?", [next, context.now(), key, token, retainedMetadata]).changes !== 1)
170
+ throw new AppError("runtime_scope_owner_unknown", "Scope owner changed before its retained result could be saved", 1);
171
+ retainedMetadata = next;
172
+ };
161
173
  let heartbeat;
162
174
  let lease;
163
175
  // A fence must never be held indefinitely by an observer that cannot obtain
164
176
  // fresh provider evidence. A later operator request continues the retained
165
177
  // budget; this owner records why it stopped without claiming settlement.
166
- const observation = recoveryObservationBudget(worker.snapshot);
167
- worker.snapshot = { ...(worker.snapshot ?? {}), recovery_observation: observation.state() };
178
+ let observationCleanup;
168
179
  let cleanupFailure;
169
180
  const result = await (async () => {
170
181
  try {
182
+ const owner = await startup.birth(assertStartupCurrent);
183
+ const claim = db.writeTransaction(() => {
184
+ assertStartupCurrent();
185
+ const current = requireStartingScopeWorker(db, key, token);
186
+ current.owner = owner;
187
+ current.status = "running";
188
+ db.run("UPDATE managed_resources SET metadata_json = ?, updated_at = ? WHERE resource_kind = 'scope-control-worker' AND resource_key = ? AND lease_token = ?", [JSON.stringify(current), context.now(), key, token]);
189
+ return { worker: current, metadata: JSON.stringify(current) };
190
+ });
191
+ worker = claim.worker;
192
+ retainedMetadata = claim.metadata;
193
+ claimedOwner = owner;
194
+ claimedByThisServe = true;
195
+ const observation = observationCleanup = recoveryObservationBudget(worker.snapshot);
196
+ worker.snapshot = { ...(worker.snapshot ?? {}), recovery_observation: observation.state() };
171
197
  const record = registerManagedProcess(context, { kind: "scope-control", ownerId: key, operationId: `scope-control:${key}:${token}`, ownerPid: process.pid, metadata: { role: "control", scope_id: scopeId, generation } });
172
198
  worker.process_id = record.id;
173
199
  worker.process_lease_token = record.lease_token;
174
- const confirmed = confirmManagedProcess(context, { id: record.id, leaseToken: record.lease_token, pid: process.pid, processGroupId: process.pid, ownerPid: process.pid });
175
- db.run("UPDATE managed_resources SET metadata_json = ?, updated_at = ? WHERE resource_kind = 'scope-control-worker' AND resource_key = ? AND lease_token = ?", [JSON.stringify(worker), context.now(), key, token]);
200
+ const confirmed = await startup.observe(() => confirmManagedProcess(context, { id: record.id, leaseToken: record.lease_token, pid: process.pid, processGroupId: process.pid, ownerPid: process.pid }), assertStartupCurrent, record.lease_expires_at);
201
+ persistWorker();
176
202
  lease = managedLeaseMonitor(attemptMs => renewManagedProcess({ ...context, resourceDatabaseMode: "prepared_existing", databaseDeadlineAt: Date.now() + attemptMs }, { id: record.id, leaseToken: record.lease_token }), confirmed.lease_expires_at);
177
203
  let timerFailure;
178
204
  const assertScopeCurrent = () => {
@@ -233,7 +259,7 @@ export async function serveRuntimeScopeControl(context, scopeId, generation) {
233
259
  await lease.ensureConfirmed(assertScopeCurrent);
234
260
  observation.observe(outcome);
235
261
  worker.snapshot = { ...outcome, recovery_observation: observation.state() };
236
- db.run("UPDATE managed_resources SET metadata_json = ?, updated_at = ? WHERE resource_kind = 'scope-control-worker' AND resource_key = ? AND lease_token = ?", [JSON.stringify(worker), context.now(), key, token]);
262
+ persistWorker();
237
263
  if (worker.snapshot.drain?.physical_settled === true) {
238
264
  worker.status = "completed";
239
265
  break;
@@ -259,18 +285,19 @@ export async function serveRuntimeScopeControl(context, scopeId, generation) {
259
285
  lease?.close();
260
286
  if (heartbeat)
261
287
  clearInterval(heartbeat);
262
- observation.exhausted();
263
- worker.snapshot = { ...(worker.snapshot ?? {}), recovery_observation: observation.state() };
288
+ observationCleanup?.exhausted();
289
+ worker.snapshot = { ...(worker.snapshot ?? {}), ...(observationCleanup ? { recovery_observation: observationCleanup.state() } : {}) };
264
290
  let persistenceFailure;
265
291
  try {
266
- db.run("UPDATE managed_resources SET metadata_json = ?, updated_at = ? WHERE resource_kind = 'scope-control-worker' AND resource_key = ? AND lease_token = ?", [JSON.stringify(worker), context.now(), key, token]);
292
+ persistWorker();
267
293
  }
268
294
  catch (error) {
269
295
  persistenceFailure = error;
270
296
  process.stderr.write(`${JSON.stringify({ primary: worker.error, primary_error: worker.snapshot.error, phase: "persist_scope_owner", secondary: errorRecord(error) })}\n`);
271
297
  }
272
298
  try {
273
- if (worker.process_id && worker.process_lease_token)
299
+ const current = claimedByThisServe && db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control-worker' AND resource_key = ? AND lease_token = ?", [key, token]);
300
+ if (current && current.metadata_json === retainedMetadata && !persistenceFailure && worker.process_id && worker.process_lease_token)
274
301
  finishManagedProcess(context, { id: worker.process_id, leaseToken: worker.process_lease_token, state: worker.status === "failed" ? "failed" : "stopped", reason: "scope_owner_exited" });
275
302
  }
276
303
  catch (error) {
@@ -7,6 +7,7 @@ import { assertRunMutationAllowed } from "./run-recovery.js";
7
7
  import { requireProjectByRoot } from "./projects.js";
8
8
  import { resolveRun } from "./runs.js";
9
9
  import { codeExecutionEnvironment, runCheck } from "./code-checks.js";
10
+ import { retainedBrowserEnvironment } from "./check-environment.js";
10
11
  import { confirmManagedProcess, finishManagedProcess, renewManagedProcess, managedProcessStatus, processTreeIsAlive, registerManagedProcess, reservePorts, resourceHome, stopManagedProcess } from "./managed-processes.js";
11
12
  export function prepareRuntimeService(context, input) {
12
13
  validateObservationDuration(input.timeoutMs);
@@ -33,7 +34,7 @@ export async function startRuntimeService(context, input) {
33
34
  const save = () => { const temporary = `${receiptPath}.${process.pid}.tmp`; fs.writeFileSync(temporary, JSON.stringify(receipt, null, 2)); fs.renameSync(temporary, receiptPath); };
34
35
  try {
35
36
  const allocation = await reservePorts(context, { ownerId: record.owner_id, processId: record.id, names: input.ports });
36
- const environment = { ...codeExecutionEnvironment(run.workspace_root), ...Object.fromEntries(Object.entries(allocation.ports).map(([name, port]) => [`DD_FLOW_PORT_${name.toUpperCase()}`, String(port)])), DD_FLOW_EVIDENCE_DIR: directory, DD_FLOW_CHECK_COMPLETION_FILE: path.join(directory, "completion.json") };
37
+ const environment = { ...codeExecutionEnvironment(run.workspace_root, retainedBrowserEnvironment(context.env)), ...Object.fromEntries(Object.entries(allocation.ports).map(([name, port]) => [`DD_FLOW_PORT_${name.toUpperCase()}`, String(port)])), DD_FLOW_EVIDENCE_DIR: directory, DD_FLOW_CHECK_COMPLETION_FILE: path.join(directory, "completion.json") };
37
38
  const quoted = (value) => `'${value.replaceAll("'", "'\\''")}'`;
38
39
  Object.assign(receipt, { ports: allocation.ports, environment: Object.fromEntries(Object.entries(environment).filter(([key]) => key.startsWith("DD_FLOW_PORT_"))), stop_command: `DD_FLOW_HOME=${quoted(context.ddFlowHome)} DD_FLOW_RESOURCE_HOME=${quoted(resourceHome(context))} dd-flow runtime process stop --id ${record.id} --lease-token ${record.lease_token} --json` });
39
40
  save();
@@ -1,4 +1,5 @@
1
1
  import crypto from "node:crypto";
2
+ import { recordFlowSessionObservation } from "./hook-session-observation.js";
2
3
  import { continuationPolicies, flowKinds } from "../domain/contracts.js";
3
4
  import { AppError } from "../shared/errors.js";
4
5
  import { hookSessionIdentity } from "./hooks.js";
@@ -23,25 +24,7 @@ export function publicFlowSession(context, session) {
23
24
  delete publicFields.parent_session_id;
24
25
  return { ...publicFields, harness_id: identity.harness_id, session_id: identity.session_id, parent_session: parent ? publicSessionIdentity(parent) : null };
25
26
  }
26
- export function recordFlowSessionObservation(context, input) {
27
- const duplicate = context.db.get("SELECT id FROM flow_session_segments WHERE project_id = ? AND event_key = ?", [input.projectId, input.eventKey]);
28
- if (duplicate)
29
- return { changed: false, segment_id: duplicate.id };
30
- const open = context.db.get(`SELECT id, run_id, protocol_id FROM flow_session_segments
31
- WHERE project_id = ? AND session_id = ? AND ended_at IS NULL ORDER BY started_at DESC, id DESC LIMIT 1`, [input.projectId, input.sessionId]);
32
- const now = context.now();
33
- const runId = input.runId === undefined ? open?.run_id ?? null : input.runId;
34
- const protocolId = input.protocolId === undefined ? open?.protocol_id ?? null : input.protocolId;
35
- if (open && open.run_id === runId && open.protocol_id === protocolId) {
36
- return { changed: false, segment_id: open.id };
37
- }
38
- if (open)
39
- context.db.run("UPDATE flow_session_segments SET ended_at = ? WHERE id = ? AND ended_at IS NULL", [now, open.id]);
40
- const result = context.db.run(`INSERT INTO flow_session_segments
41
- (project_id, session_id, run_id, protocol_id, started_at, ended_at, cwd, tool_name, event_key)
42
- VALUES (?, ?, ?, ?, ?, NULL, ?, ?, ?)`, [input.projectId, input.sessionId, runId, protocolId, now, input.cwd ?? null, input.toolName ?? null, input.eventKey]);
43
- return { changed: true, segment_id: Number(result.lastInsertRowid) };
44
- }
27
+ export { recordFlowSessionObservation } from "./hook-session-observation.js";
45
28
  export function registerFlowJob(context, input) {
46
29
  const jobId = input.jobId ?? `JOB-${input.runId}-${input.planItemId}`.replace(/[^A-Za-z0-9_-]+/g, "-");
47
30
  const now = context.now();
@@ -167,11 +150,7 @@ export function stoppedMergeWorkerState(context, projectId, workerId) {
167
150
  status: latest?.status ?? null
168
151
  };
169
152
  }
170
- export function activeFlowSessionsForProject(context, projectId) {
171
- return context.db.all(`SELECT * FROM sessions
172
- WHERE project_id = ? AND status IN ('pending', 'active', 'waiting_user', 'blocked', 'stopping')
173
- ORDER BY updated_at DESC`, [projectId]);
174
- }
153
+ export { activeFlowSessionsForProject } from "./hook-session-observation.js";
175
154
  export function flowSessionsForProject(context, projectId, filter = {}) {
176
155
  if (filter.sessionId) {
177
156
  return context.db.all("SELECT * FROM sessions WHERE project_id = ? AND (session_id = ? OR provider_session_id = ?) ORDER BY updated_at DESC", [projectId, filter.sessionId, filter.sessionId]);
@@ -60,7 +60,7 @@ function atomicJson(file, value) {
60
60
  fs.unlinkSync(temporary);
61
61
  }
62
62
  }
63
- function readSetupRequest(file) {
63
+ function readSetupRequest(file, validateExecutor = true) {
64
64
  const value = regularJson(file);
65
65
  const required = ["schema_id", "request_id", "identity", "previous_selection", "phase", "installation"];
66
66
  if (!value || typeof value !== "object" || Array.isArray(value)
@@ -77,7 +77,7 @@ function readSetupRequest(file) {
77
77
  || !["prepared", "canon_cached", "engine_cached", "profiles_saved", "committed"].includes(request.phase)
78
78
  || !request.installation || typeof request.installation !== "object" || Array.isArray(request.installation))
79
79
  refuse("invalid_request_record");
80
- if (Object.hasOwn(request, "executor") && request.executor !== null) {
80
+ if (validateExecutor && Object.hasOwn(request, "executor") && request.executor !== null) {
81
81
  try {
82
82
  prepareOperatorProfile(request.executor);
83
83
  }
@@ -85,7 +85,7 @@ function readSetupRequest(file) {
85
85
  refuse("invalid_request_executor");
86
86
  }
87
87
  }
88
- if (Object.hasOwn(request, "profileCreation")) {
88
+ if (validateExecutor && Object.hasOwn(request, "profileCreation")) {
89
89
  try {
90
90
  validateProfileCreationIntent(request.profileCreation);
91
91
  }
@@ -225,22 +225,72 @@ export function commitSetupOnboarding(home, requestId, selection) {
225
225
  return result;
226
226
  }
227
227
  /** Read-only global readiness. No SQLite connection, migration, project registration or probes. */
228
+ function validateInstallation(root, installation) {
229
+ if (!installation.manifest || typeof installation.manifest_bytes_base64 !== "string" || validateHybridShape("release-manifest", installation.manifest).length ||
230
+ installation.canon_root !== path.join(root, "release-cache", "canon", installation.manifest.canon.version, "payload") ||
231
+ installation.engine_root !== path.join(root, "release-cache", "engine", installation.manifest.engine.version, "payload"))
232
+ refuse("invalid_installation_selection");
233
+ const bytes = Buffer.from(installation.manifest_bytes_base64, "base64");
234
+ if (bytes.toString("base64") !== installation.manifest_bytes_base64 || bytesDigest(bytes) !== installation.manifest_sha256 ||
235
+ hybridDigest(manifestFromBytes(bytes)) !== hybridDigest(installation.manifest))
236
+ refuse("invalid_installation_selection");
237
+ }
238
+ /** Independently committed canonical data, never activation or old code
239
+ * admission. A missing old engine cache is irrelevant to comparing documents.
240
+ * Same canon commit/version with different archive bytes is not disambiguated
241
+ * by picking the newest CLI or whichever request happens to be listed first.
242
+ */
243
+ export function readCommittedCanonRelease(home, version, commit, expectedInstallationSha256) {
244
+ const root = path.resolve(home), directory = path.join(root, "setup-requests");
245
+ if (!/^[0-9]+\.[0-9]+\.[0-9]+(?:-[A-Za-z0-9.-]+)?$/.test(version) || !/^[a-f0-9]{40}$/.test(commit)
246
+ || expectedInstallationSha256 !== undefined && !/^[a-f0-9]{64}$/.test(expectedInstallationSha256))
247
+ refuse("invalid_canonical_baseline_selection");
248
+ for (const target of [root, directory])
249
+ if (fs.existsSync(target) && !fs.lstatSync(target).isDirectory())
250
+ refuse("unsafe_setup_directory");
251
+ if (!fs.existsSync(directory))
252
+ return null;
253
+ const matches = [];
254
+ for (const name of fs.readdirSync(directory).filter(name => name.endsWith(".json")).sort()) {
255
+ const request = readSetupRequest(path.join(directory, name), false);
256
+ if (`${request.request_id}.json` !== name)
257
+ refuse("request_identity_conflict");
258
+ if (request.phase !== "committed" || request.installation.manifest?.canon?.version !== version || request.installation.manifest.canon.commit !== commit)
259
+ continue;
260
+ if (expectedInstallationSha256 !== undefined && request.installation.manifest_sha256 !== expectedInstallationSha256)
261
+ continue;
262
+ validateInstallation(root, request.installation);
263
+ const binding = { installation: request.installation,
264
+ ...(Object.hasOwn(request, "executor") ? { executor: request.executor } : {}),
265
+ ...(request.profileCreation ? { profileCreation: request.profileCreation } : {}),
266
+ ...(request.deferredOnboarding ? { deferredOnboarding: true } : {}) };
267
+ if (!["install", "update"].some(action => hybridDigest({ action, ...binding }) === request.identity))
268
+ refuse("request_identity_conflict");
269
+ matches.push(request.installation);
270
+ }
271
+ if (!matches.length)
272
+ return null;
273
+ if (new Set(matches.map(installation => `${installation.manifest.canon.archive.sha256}:${installation.manifest.canon.archive.bytes}`)).size !== 1)
274
+ refuse("ambiguous_canonical_baseline");
275
+ const selected = matches.sort((left, right) => left.manifest_sha256.localeCompare(right.manifest_sha256, "en"))[0];
276
+ try {
277
+ verifyPublishedReleaseCache(path.dirname(selected.canon_root), selected.manifest.canon.archive);
278
+ }
279
+ catch (error) {
280
+ if (error.code === "ENOENT")
281
+ return null;
282
+ throw error;
283
+ }
284
+ return structuredClone(selected);
285
+ }
228
286
  export function readSetupStatus(home) {
229
287
  const root = path.resolve(home);
230
288
  if (fs.existsSync(root) && !fs.lstatSync(root).isDirectory())
231
289
  refuse("unsafe_setup_home");
232
290
  const file = path.join(root, "installation.json");
233
291
  const installation = fs.existsSync(file) ? regularJson(file) : null;
234
- if (installation) {
235
- if (!installation.manifest || typeof installation.manifest_bytes_base64 !== "string" || validateHybridShape("release-manifest", installation.manifest).length ||
236
- installation.canon_root !== path.join(root, "release-cache", "canon", installation.manifest.canon.version, "payload") ||
237
- installation.engine_root !== path.join(root, "release-cache", "engine", installation.manifest.engine.version, "payload"))
238
- refuse("invalid_installation_selection");
239
- const bytes = Buffer.from(installation.manifest_bytes_base64, "base64");
240
- if (bytes.toString("base64") !== installation.manifest_bytes_base64 || bytesDigest(bytes) !== installation.manifest_sha256 ||
241
- hybridDigest(manifestFromBytes(bytes)) !== hybridDigest(installation.manifest))
242
- refuse("invalid_installation_selection");
243
- }
292
+ if (installation)
293
+ validateInstallation(root, installation);
244
294
  let readiness = installation ? "installed" : "not_installed";
245
295
  if (installation) {
246
296
  try {