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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +9 -0
  3. package/dist/build-info.json +6 -6
  4. package/dist/cli/argument-values.js +5 -5
  5. package/dist/cli/command-inputs.js +89 -20
  6. package/dist/cli/command-metadata.js +56 -3
  7. package/dist/cli/command-option-descriptions.js +5 -0
  8. package/dist/cli/executor-onboarding.js +8 -3
  9. package/dist/cli/flows.js +29 -0
  10. package/dist/cli/help.js +240 -6
  11. package/dist/cli/input-preparation.js +54 -5
  12. package/dist/cli/memory-flow-admission.js +44 -0
  13. package/dist/cli/memory-init-invocation.js +78 -0
  14. package/dist/cli/memory-init.js +240 -0
  15. package/dist/cli/memory-storage-admission.js +53 -0
  16. package/dist/cli/native-hook-ingress.js +1 -1
  17. package/dist/cli/response-file.js +2 -0
  18. package/dist/cli/run-cli.js +283 -160
  19. package/dist/cli/setup.js +9 -7
  20. package/dist/cli.js +54 -2
  21. package/dist/domain/stage-handler-identities.js +20 -0
  22. package/dist/harness-runtime/DD-ZCODE.md +13 -0
  23. package/dist/harness-runtime/bin/dd-opencode.mjs +3 -3
  24. package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
  25. package/dist/harness-runtime/lib/daemon-operations.mjs +4 -3
  26. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +4 -2
  27. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +11 -5
  28. package/dist/harness-runtime/lib/dd-codex.mjs +22 -10
  29. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +2 -1
  30. package/dist/harness-runtime/lib/dd-droid.mjs +2 -1
  31. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +4 -2
  32. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +20 -5
  33. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +45 -10
  34. package/dist/harness-runtime/lib/dd-zcode.mjs +7 -3
  35. package/dist/harness-runtime/lib/delegation-instructions.mjs +5 -3
  36. package/dist/harness-runtime/lib/lifecycle-command.mjs +1 -0
  37. package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -0
  38. package/dist/harness-runtime/lib/managed-daemon.mjs +50 -15
  39. package/dist/harness-runtime/lib/managed-flow-env.mjs +15 -0
  40. package/dist/harness-runtime/lib/native-hook-command.mjs +1 -1
  41. package/dist/harness-runtime/lib/native-request-proof.d.mts +2 -0
  42. package/dist/harness-runtime/lib/native-request-proof.mjs +65 -0
  43. package/dist/harness-runtime/lib/opencode-plugin.mjs +5 -1
  44. package/dist/harness-runtime/lib/operation-progress.mjs +3 -1
  45. package/dist/runtime/context.js +1 -1
  46. package/dist/schemas/discovery-result.schema.json +184 -0
  47. package/dist/schemas/execution-bundle.schema.json +205 -0
  48. package/dist/schemas/execution-settings.schema.json +162 -0
  49. package/dist/schemas/flow-descriptor.schema.json +54 -0
  50. package/dist/schemas/flow-run.schema.json +22 -0
  51. package/dist/schemas/harness-catalog.schema.json +80 -0
  52. package/dist/schemas/mb-init-compose-result.schema.json +457 -0
  53. package/dist/schemas/mb-init-discover-result.schema.json +500 -0
  54. package/dist/schemas/mb-init-finish-result.schema.json +422 -0
  55. package/dist/schemas/mb-init-preflight-result.schema.json +367 -0
  56. package/dist/schemas/mb-init-review-result.schema.json +396 -0
  57. package/dist/schemas/mb-init-verify-result.schema.json +391 -0
  58. package/dist/schemas/memory-bootstrap-receipt-v2.schema.json +67 -0
  59. package/dist/schemas/memory-bootstrap-receipt.schema.json +354 -0
  60. package/dist/schemas/memory-composition-work-result.schema.json +38 -0
  61. package/dist/schemas/memory-repair-work-result.schema.json +49 -0
  62. package/dist/schemas/memory-review-correction-plan.schema.json +30 -0
  63. package/dist/schemas/memory-review-work-result.schema.json +43 -0
  64. package/dist/schemas/model-catalog.schema.json +153 -0
  65. package/dist/schemas/profile-settings.schema.json +122 -0
  66. package/dist/schemas/release-manifest.schema.json +291 -0
  67. package/dist/schemas/run-engine-binding.schema.json +1 -1
  68. package/dist/services/check-runner-evidence.js +51 -4
  69. package/dist/services/cli-operation-classifier.js +8 -47
  70. package/dist/services/code-checks.js +387 -34
  71. package/dist/services/controller-child-observer.js +99 -0
  72. package/dist/services/controller-execution.js +88 -0
  73. package/dist/services/controller-fanout.js +79 -13
  74. package/dist/services/dashboard.js +8 -2
  75. package/dist/services/descriptor-coordinator-session.js +35 -0
  76. package/dist/services/descriptor-root-work.js +53 -0
  77. package/dist/services/descriptor-run-settlement.js +105 -0
  78. package/dist/services/descriptor-run-start.js +449 -0
  79. package/dist/services/descriptor-stage-coordinator.js +197 -0
  80. package/dist/services/descriptor-stage-settlement.js +222 -0
  81. package/dist/services/descriptor-stage-start.js +139 -0
  82. package/dist/services/engines.js +39 -22
  83. package/dist/services/eval-snapshots.js +83 -25
  84. package/dist/services/execution-bundles.js +263 -0
  85. package/dist/services/execution-policy.js +20 -16
  86. package/dist/services/external-work-launch.js +91 -11
  87. package/dist/services/flow-catalog.js +21 -0
  88. package/dist/services/flow-descriptors.js +82 -0
  89. package/dist/services/frozen-engine.js +242 -0
  90. package/dist/services/generic-execution.js +164 -0
  91. package/dist/services/harness-adapter.js +28 -2
  92. package/dist/services/hooks.js +24 -6
  93. package/dist/services/hybrid-contracts.js +31 -3
  94. package/dist/services/ids.js +29 -1
  95. package/dist/services/installation-lock.js +7 -4
  96. package/dist/services/installed-flow-source.js +183 -0
  97. package/dist/services/lifecycle-invocations.js +257 -78
  98. package/dist/services/memory-accepted-composition.js +77 -0
  99. package/dist/services/memory-baseline-preservation.js +65 -0
  100. package/dist/services/memory-completed-correction-wave.js +320 -0
  101. package/dist/services/memory-compose-handler.js +258 -0
  102. package/dist/services/memory-composition-acceptance.js +199 -0
  103. package/dist/services/memory-composition-graph.js +134 -0
  104. package/dist/services/memory-composition-registration.js +146 -0
  105. package/dist/services/memory-composition-work-custody.js +151 -0
  106. package/dist/services/memory-composition-work-results.js +96 -0
  107. package/dist/services/memory-corrected-review-acceptance.js +129 -0
  108. package/dist/services/memory-corrected-review-batch.js +30 -0
  109. package/dist/services/memory-corrected-review-findings.js +106 -0
  110. package/dist/services/memory-corrected-review-inputs.js +210 -0
  111. package/dist/services/memory-corrected-review-projection.js +78 -0
  112. package/dist/services/memory-corrected-review-receipt.js +170 -0
  113. package/dist/services/memory-corrected-review-registration.js +191 -0
  114. package/dist/services/memory-correction-check-receipt.js +219 -0
  115. package/dist/services/memory-correction-input-owner.js +6 -0
  116. package/dist/services/memory-discover-handler.js +104 -0
  117. package/dist/services/memory-finish-evidence.js +435 -0
  118. package/dist/services/memory-finish-handler.js +215 -0
  119. package/dist/services/memory-finish-integration.js +280 -0
  120. package/dist/services/memory-finish-readiness.js +122 -0
  121. package/dist/services/memory-git-integration.js +151 -0
  122. package/dist/services/memory-historical-corrected-review.js +206 -0
  123. package/dist/services/memory-historical-original-review.js +130 -0
  124. package/dist/services/memory-historical-repair-registration.js +99 -0
  125. package/dist/services/memory-historical-second-review.js +128 -0
  126. package/dist/services/memory-init-bootstrap-readiness.js +210 -0
  127. package/dist/services/memory-init-contexts.js +89 -0
  128. package/dist/services/memory-init-controller-start.js +84 -0
  129. package/dist/services/memory-init-engine-release.js +223 -0
  130. package/dist/services/memory-init-git-policy.js +136 -0
  131. package/dist/services/memory-init-lint-capability.js +185 -0
  132. package/dist/services/memory-init-native-capacity.js +241 -0
  133. package/dist/services/memory-init-preflight.js +312 -0
  134. package/dist/services/memory-init-project-bootstrap.js +599 -0
  135. package/dist/services/memory-init-request-candidate.js +157 -0
  136. package/dist/services/memory-init-request-input.js +66 -0
  137. package/dist/services/memory-init-resource-storage.js +125 -0
  138. package/dist/services/memory-init-run-allocation.js +81 -0
  139. package/dist/services/memory-init-run-bundle.js +98 -0
  140. package/dist/services/memory-init-start.js +175 -0
  141. package/dist/services/memory-init-startup-custody.js +80 -0
  142. package/dist/services/memory-init-startup-input.js +240 -0
  143. package/dist/services/memory-init-storage.js +81 -0
  144. package/dist/services/memory-init-stores.js +359 -0
  145. package/dist/services/memory-init-worktrunk-capability.js +172 -0
  146. package/dist/services/memory-layer-publication.js +611 -0
  147. package/dist/services/memory-layer.js +156 -0
  148. package/dist/services/memory-lint-config.js +85 -0
  149. package/dist/services/memory-native-review-copy.js +165 -0
  150. package/dist/services/memory-normal-flow-entry.js +75 -0
  151. package/dist/services/memory-ordinary-defaults.js +112 -0
  152. package/dist/services/memory-preflight-bootstrap-custody.js +115 -0
  153. package/dist/services/memory-preflight-handler.js +259 -0
  154. package/dist/services/memory-repair-current-view.js +299 -0
  155. package/dist/services/memory-repair-document-delta.js +451 -0
  156. package/dist/services/memory-repair-history.js +491 -0
  157. package/dist/services/memory-repair-physical-binding.js +312 -0
  158. package/dist/services/memory-repair-publication-files.js +152 -0
  159. package/dist/services/memory-repair-publication-intent.js +27 -0
  160. package/dist/services/memory-repair-publication-plan.js +160 -0
  161. package/dist/services/memory-repair-publication-source.js +225 -0
  162. package/dist/services/memory-repair-publication.js +794 -0
  163. package/dist/services/memory-repair-registration.js +239 -0
  164. package/dist/services/memory-repair-work-baseline.js +413 -0
  165. package/dist/services/memory-repair-work-completion.js +97 -0
  166. package/dist/services/memory-repair-work-custody.js +73 -0
  167. package/dist/services/memory-repair-work-results.js +89 -0
  168. package/dist/services/memory-repair-workspace.js +252 -0
  169. package/dist/services/memory-repair-writer-copy.js +735 -0
  170. package/dist/services/memory-result-binding.js +173 -0
  171. package/dist/services/memory-retained-review-authority.js +129 -0
  172. package/dist/services/memory-retained-review-results.js +70 -0
  173. package/dist/services/memory-retained-second-baseline.js +102 -0
  174. package/dist/services/memory-retained-second-repairs.js +156 -0
  175. package/dist/services/memory-review-acceptance.js +214 -0
  176. package/dist/services/memory-review-correction-plan.js +171 -0
  177. package/dist/services/memory-review-handler.js +585 -0
  178. package/dist/services/memory-review-inputs.js +200 -0
  179. package/dist/services/memory-review-receipt.js +180 -0
  180. package/dist/services/memory-review-registration.js +108 -0
  181. package/dist/services/memory-review-work-custody.js +124 -0
  182. package/dist/services/memory-review-work-input.js +72 -0
  183. package/dist/services/memory-review-work-results.js +73 -0
  184. package/dist/services/memory-second-corrected-review-inputs.js +165 -0
  185. package/dist/services/memory-second-correction-plan.js +76 -0
  186. package/dist/services/memory-second-repair-baseline.js +91 -0
  187. package/dist/services/memory-second-repair-launch.js +614 -0
  188. package/dist/services/memory-second-repair-registration.js +88 -0
  189. package/dist/services/memory-second-repair-source.js +187 -0
  190. package/dist/services/memory-second-review-acceptance.js +115 -0
  191. package/dist/services/memory-source-observations.js +117 -0
  192. package/dist/services/memory-stage-dispatch.js +28 -0
  193. package/dist/services/memory-stage-results.js +199 -0
  194. package/dist/services/memory-verification-acceptance.js +135 -0
  195. package/dist/services/memory-verification-checks.js +252 -0
  196. package/dist/services/memory-verification-receipt.js +159 -0
  197. package/dist/services/memory-verify-handler.js +135 -0
  198. package/dist/services/memory-work-result-preparation.js +63 -0
  199. package/dist/services/projects.js +17 -2
  200. package/dist/services/recovery-snapshot-database.js +416 -1
  201. package/dist/services/repair-intents.js +1 -1
  202. package/dist/services/review-copy.js +10 -2
  203. package/dist/services/run-control.js +80 -9
  204. package/dist/services/run-controller-capture.js +10 -3
  205. package/dist/services/run-controller-process.js +21 -0
  206. package/dist/services/run-controller-recovery.js +3 -1
  207. package/dist/services/run-controller-state.js +1 -1
  208. package/dist/services/run-controller.js +184 -48
  209. package/dist/services/run-recovery-guard.js +2 -0
  210. package/dist/services/run-recovery.js +145 -5
  211. package/dist/services/run-start-custody.js +177 -0
  212. package/dist/services/runs.js +319 -25
  213. package/dist/services/stage-context.js +9 -1
  214. package/dist/services/stage-handlers.js +74 -0
  215. package/dist/services/stage-lifecycle.js +2 -1
  216. package/dist/services/stage-report-renderer.js +76 -0
  217. package/dist/services/stage-work-graph.js +17 -0
  218. package/dist/services/vnext-code.js +3 -3
  219. package/dist/services/vnext-execution-profile.js +2 -2
  220. package/dist/services/vnext-fanout.js +136 -10
  221. package/dist/services/vnext-merge.js +8 -3
  222. package/dist/services/vnext-plan-review.js +6 -4
  223. package/dist/services/vnext-protocolize.js +60 -2
  224. package/dist/services/work-registry.js +616 -71
  225. package/dist/services/workspace-files.js +1 -1
  226. package/dist/storage/database.js +84 -6
  227. package/dist/storage/writer-contract.js +11 -7
  228. package/package.json +13 -3
  229. package/tools/create-canon-release.mjs +33 -0
  230. package/tools/produce-hybrid-release.mjs +41 -0
@@ -4,11 +4,16 @@ export { recoveryGuard, assertRunMutationAllowed, assertRunRecoveryAllowed } fro
4
4
  import crypto from "node:crypto";
5
5
  import fs from "node:fs";
6
6
  import path from "node:path";
7
+ import { DatabaseSync } from "node:sqlite";
8
+ import { parseLifecycleCommand } from "../harness-runtime/lib/lifecycle-command.mjs";
7
9
  import { AppError } from "../shared/errors.js";
8
10
  import { entityReferenceVariants } from "../shared/entity-references.js";
9
11
  import { resolveProjectRoot } from "../storage/paths.js";
10
12
  import { requireProjectByRoot } from "./projects.js";
11
- import { appendFlowRunTimelineEvent, setFlowRunRecoveryPaused } from "./runs.js";
13
+ import { appendFlowRunTimelineEvent, setFlowRunRecoveryPaused, readFlowRunExecutionBundle, assertPreparedRunBundleSqlAuthority } from "./runs.js";
14
+ import { readDescriptorCoordinatorSession } from "./descriptor-coordinator-session.js";
15
+ import { appendAudit } from "./audit.js";
16
+ import { hybridDigest } from "./hybrid-contracts.js";
12
17
  import { verifyRecoverySnapshot } from "./eval-snapshots.js";
13
18
  import { findRecentMatchingHookEvent, hookSessionIdentity, recoveryAcceptMatchKey } from "./hooks.js";
14
19
  import { flowCommand } from "./stage-pause.js";
@@ -19,6 +24,93 @@ export function recoveryPacketReceipt(binding) {
19
24
  throw new AppError("recovery_packet_missing", "Recovery requires its original verified Work packet", 1);
20
25
  return { packet_json: binding.packet_json, packet_sha256: binding.packet_sha256, packet: JSON.parse(binding.packet_json) };
21
26
  }
27
+ /** Read-only custody permission for one prepared recovery ACK, not lifecycle admission. */
28
+ export class PreparedRecoveryCoordinatorRead {
29
+ #scope;
30
+ #recoveryId;
31
+ #eventKey;
32
+ #generation;
33
+ #signature;
34
+ constructor(scope, recoveryId, eventKey, generation, signature) {
35
+ this.#scope = Object.freeze(scope);
36
+ this.#recoveryId = recoveryId;
37
+ this.#eventKey = eventKey;
38
+ this.#generation = generation;
39
+ this.#signature = signature;
40
+ Object.freeze(this);
41
+ }
42
+ get scope() { return this.#scope; }
43
+ static prepare(context, scope, recoveryId, eventKey) {
44
+ const proof = recoveryCoordinatorReadProof(context, scope, recoveryId, eventKey);
45
+ return new PreparedRecoveryCoordinatorRead({ ...scope }, recoveryId, eventKey, proof.generation, proof.signature);
46
+ }
47
+ assertRead(context, scope) {
48
+ const proof = recoveryCoordinatorReadProof(context, scope, this.#recoveryId, this.#eventKey);
49
+ if (hybridDigest(scope) !== hybridDigest(this.#scope) || proof.generation !== this.#generation || proof.signature !== this.#signature)
50
+ throw new AppError("recovery_owner_changed", "Recovery read custody changed", 1);
51
+ }
52
+ }
53
+ function recoveryCoordinatorReadProof(context, scope, recoveryId, eventKey) {
54
+ const guard = recoveryGuard(context, scope.projectId, scope.runId);
55
+ const binding = context.db.get("SELECT * FROM run_recovery_bindings WHERE recovery_id = ?", [recoveryId]);
56
+ const identity = hookSessionIdentity(context, scope.projectId, eventKey);
57
+ const hook = context.db.get("SELECT status,event_name,match_key FROM hook_events WHERE id = ?", [identity.hookEventId]);
58
+ const run = context.db.get("SELECT r.index_json,p.root AS project_root FROM runs r JOIN projects p ON p.id=r.project_id WHERE r.project_id=? AND r.id=?", [scope.projectId, scope.runId]);
59
+ const packet = binding ? recoveryPacketReceipt(binding).packet : null;
60
+ const delivery = packet?.lifecycle_delivery;
61
+ const prior = binding && context.db.get("SELECT ws.id,ws.status AS session_status,w.status AS work_status FROM work_sessions ws JOIN works w ON w.work_id=ws.work_id WHERE ws.id=? AND ws.work_id=? AND ws.session_id=? AND ws.status='running' AND w.project_id=? AND w.run_id=? AND w.status IN ('running','paused')", [binding.prior_work_session_id, binding.work_id, binding.session_id, scope.projectId, scope.runId]);
62
+ if (!guard || guard.recovery_id !== recoveryId || guard.status !== "resuming" || !guard.capture_path || !binding || binding.status !== "prepared" || !run || !prior
63
+ || binding.session_id !== identity.sessionId || binding.harness !== identity.harness || binding.provider_session_id !== identity.providerSessionId || binding.daemon_id !== identity.daemonId || identity.parentSessionId
64
+ || hook?.status !== "observed" || hook.event_name !== "PreToolUse" || !entityReferenceVariants(scope.runId).some(reference => hook.match_key === recoveryAcceptMatchKey(reference, recoveryId, run.project_root))
65
+ || packet?.schema_id !== "dd-flow/root-recovery-packet@1" || packet.recovery_id !== recoveryId || packet.mode !== "same-session" || packet.continuation !== "unfinished_work" || packet.run_id !== scope.runId || packet.work_id !== binding.work_id
66
+ || packet.retained_session_status !== prior.session_status || packet.retained_work_status !== prior.work_status
67
+ || packet.capture_path !== guard.capture_path
68
+ || packet.generation !== guard.generation || packet.stage !== scope.stage || typeof delivery?.text !== "string"
69
+ || crypto.createHash("sha256").update(delivery.text).digest("hex") !== delivery.sha256
70
+ || JSON.parse(run.index_json).current_stage !== scope.stage)
71
+ throw new AppError("recovery_binding_mismatch", "Read requires exact prepared recovery acknowledgement custody", 1);
72
+ return { generation: guard.generation, signature: hybridDigest({ scope, guard, binding, identity, hook, run, prior }) };
73
+ }
74
+ export function recoveredCoordinatorBinding(original, input) {
75
+ const scope = original.scope;
76
+ if (original.run_id !== input.runId || scope?.project_id !== input.projectId || scope.run_id !== input.runId || scope.stage !== input.stage
77
+ || original.work_id !== input.workId || original.work_session_id !== input.priorWorkSessionId || original.session_id !== input.sessionId
78
+ || original.harness !== input.harness || original.provider_session_id !== input.providerSessionId || input.model !== input.frozenModel
79
+ || original.execution_sha256 !== input.executionSha256 || original.profile_id !== input.profileId || original.profile_sha256 !== input.profileSha256 || original.session_mode !== input.sessionMode
80
+ || !Number.isInteger(input.generation) || input.generation < 1 || !/^[a-f0-9]{64}$/.test(input.packetSha256)
81
+ || !input.daemonId || !input.hookEventKey)
82
+ throw new AppError("recovery_binding_mismatch", "Recovered coordinator differs from original frozen physical custody", 1);
83
+ return { ...original, work_session_id: input.workSessionId, recovery: {
84
+ recovery_id: input.recoveryId, generation: input.generation, original_work_session_id: input.priorWorkSessionId,
85
+ original_binding_sha256: hybridDigest(original), packet_sha256: input.packetSha256,
86
+ acknowledgement_hook: input.hookEventKey, daemon_id: input.daemonId
87
+ } };
88
+ }
89
+ /** Read issued delivery authority from the verified capture, never a mutable prompt. */
90
+ export function capturedStageDelivery(capture, scope, originalPrompt) {
91
+ const db = new DatabaseSync(path.join(capture, "runtime", "db.sqlite"), { readOnly: true });
92
+ try {
93
+ 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
+ for (const row of rows) {
95
+ const result = row.outcome_json ? JSON.parse(row.outcome_json).result : null;
96
+ 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")
99
+ continue;
100
+ const text = result.worker_prompt_markdown;
101
+ if (!text.startsWith(originalPrompt))
102
+ throw new AppError("recovery_packet_mismatch", "Captured Stage delivery differs from its original semantic prompt", 1);
103
+ const sha256 = crypto.createHash("sha256").update(text).digest("hex");
104
+ if (!db.prepare("SELECT 1 FROM lifecycle_packet_assignments WHERE packet_sha256 = ? LIMIT 1").get(sha256))
105
+ throw new AppError("recovery_packet_missing", "Captured Stage delivery has no committed lifecycle assignment", 1);
106
+ return { text, sha256 };
107
+ }
108
+ return null;
109
+ }
110
+ finally {
111
+ db.close();
112
+ }
113
+ }
22
114
  function rootRecoveryPacket(context, run, guard, owner) {
23
115
  const work = context.db.get("SELECT task, status, result, result_schema, payload_json, depends_on_json FROM works WHERE work_id = ?", [owner.work_id]);
24
116
  const session = context.db.get("SELECT prompt_path, status FROM work_sessions WHERE id = ?", [owner.id]);
@@ -34,9 +126,18 @@ function rootRecoveryPacket(context, run, guard, owner) {
34
126
  }
35
127
  const prompt = fs.readFileSync(promptPath, "utf8");
36
128
  const currentPrompt = currentStagePrompt(context, session.prompt_path, run.run_root, prompt);
129
+ const index = JSON.parse(run.index_json);
130
+ const stage = index.current_stage ?? null;
131
+ const attempt = index.stage_runs?.find((item) => item.stage === stage)?.attempt;
132
+ const provider = context.db.get("SELECT s.provider_session_id FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id JOIN sessions s ON s.session_id = ws.session_id AND s.project_id = w.project_id WHERE ws.id = ?", [owner.id]);
133
+ const delivery = !boundary && stage && attempt ? capturedStageDelivery(guard.capture_path, { projectRoot: run.project_root, runId: run.id, sessionId: provider.provider_session_id, stage, workId: owner.work_id, generation: guard.generation, attempt }, prompt) : null;
134
+ if (!boundary && ["mb-init", "mb-upgrade"].includes(run.flow_kind) && !delivery)
135
+ throw new AppError("recovery_packet_missing", "Unfinished Memory Stage requires its captured published lifecycle delivery", 1);
37
136
  const dependencies = JSON.parse(work.depends_on_json);
38
137
  return JSON.stringify({
39
138
  schema_id: "dd-flow/root-recovery-packet@1", recovery_id: guard.recovery_id, generation: guard.generation, run_id: run.id, work_id: owner.work_id,
139
+ stage,
140
+ ...(delivery ? { lifecycle_delivery: delivery } : {}),
40
141
  mode: "same-session", continuation: boundary ? "stage_boundary" : "unfinished_work", retained_work_status: work.status, retained_session_status: session.status, retained_result: work.result,
41
142
  original_task: work.task, original_task_sha256: crypto.createHash("sha256").update(work.task).digest("hex"),
42
143
  original_prompt: { path: promptPath, text: prompt, sha256: crypto.createHash("sha256").update(prompt).digest("hex") },
@@ -53,7 +154,7 @@ export function acceptanceCommand(context, runId, recoveryId, projectRoot) {
53
154
  return managedLifecycleCommand(context, `${flowCommand(context)} run recovery accept ${JSON.stringify(runId)} --recovery-id ${JSON.stringify(recoveryId)} --project-root ${JSON.stringify(projectRoot)} --json`);
54
155
  }
55
156
  export function requireRecoveryRun(context, projectId, runId) {
56
- const run = context.db.get("SELECT r.id, r.run_root, r.workspace_root, p.root AS project_root FROM runs r JOIN projects p ON p.id = r.project_id WHERE r.project_id = ? AND r.id = ?", [projectId, runId]);
157
+ const run = context.db.get("SELECT r.id, r.run_root, r.workspace_root, r.index_json, r.flow_kind, p.root AS project_root FROM runs r JOIN projects p ON p.id = r.project_id WHERE r.project_id = ? AND r.id = ?", [projectId, runId]);
57
158
  if (!run?.run_root)
58
159
  throw new AppError("runtime_missing", "RUN artifact root is unavailable", 1, { run_id: runId });
59
160
  return run;
@@ -251,6 +352,7 @@ export function resumeRunRecovery(context, input, prepared = prepareRunRecoveryR
251
352
  return { signature, owner, packet, running, evidence };
252
353
  });
253
354
  const { owner, packet, running } = publication;
355
+ verifyRecoverySnapshot(guard.capture_path, { runId: run.id, recoveryId: guard.recovery_id, projectRoot: run.project_root, workspaceRoot: run.workspace_root });
254
356
  context.db.writeTransaction(() => {
255
357
  if (authority() !== publication.signature)
256
358
  throw new AppError("recovery_state_changed", "Recovery inputs changed while its packet was prepared", 1, { recovery_id: guard.recovery_id });
@@ -278,6 +380,16 @@ export function acceptRunRecovery(context, input) {
278
380
  const eventKey = input.hookEventId ?? findRecentMatchingHookEvent(context, { projectId: project.id, matchKey, errorCode: "trusted_recovery_acceptance_required", operation: "run recovery accept" }).eventKey;
279
381
  const identity = hookSessionIdentity(context, project.id, eventKey);
280
382
  const now = context.now();
383
+ const preparedBinding = context.db.get("SELECT * FROM run_recovery_bindings WHERE recovery_id = ?", [input.recoveryId]);
384
+ const preparedPacket = preparedBinding ? recoveryPacketReceipt(preparedBinding).packet : null;
385
+ const bundle = preparedBinding?.status === "prepared" && preparedPacket?.continuation === "unfinished_work" && preparedPacket.lifecycle_delivery
386
+ ? readFlowRunExecutionBundle(context, { projectRoot: run.project_root, runId: run.id }) : null;
387
+ const coordinatorScope = bundle && preparedPacket?.stage ? { projectId: project.id, runId: run.id, stage: preparedPacket.stage } : null;
388
+ if (preparedBinding?.status === "prepared" && preparedPacket?.continuation === "unfinished_work"
389
+ && ["mb-init", "mb-upgrade"].includes(run.flow_kind) && (!preparedPacket.lifecycle_delivery || !coordinatorScope))
390
+ throw new AppError("recovery_packet_missing", "Memory recovery lacks retained Stage delivery and frozen coordinator scope", 1);
391
+ const recoveryRead = coordinatorScope ? PreparedRecoveryCoordinatorRead.prepare(context, coordinatorScope, input.recoveryId, eventKey) : undefined;
392
+ const preparedCustody = coordinatorScope && bundle ? readDescriptorCoordinatorSession(context, { scope: coordinatorScope, bundle, workId: preparedBinding.work_id, recoveryRead: recoveryRead }) : null;
281
393
  const result = context.db.writeTransaction(() => {
282
394
  assertRecoveryRunCanResume(context, project.id, run.id);
283
395
  const guard = recoveryGuard(context, project.id, run.id);
@@ -287,11 +399,14 @@ export function acceptRunRecovery(context, input) {
287
399
  throw new AppError("recovery_binding_mismatch", "Recovery acknowledgement does not belong to the authorized physical Session and generation", 1);
288
400
  const packetReceipt = recoveryPacketReceipt(binding);
289
401
  const retained = packetReceipt.packet;
290
- if (binding.harness === "zcode-acp" || (binding.harness === "codex-desktop" && hasLifecyclePacketAssignments(context, retained.original_prompt.text))) {
402
+ const authorityPrompt = retained.lifecycle_delivery?.text ?? retained.original_prompt.text;
403
+ if (retained.lifecycle_delivery && crypto.createHash("sha256").update(authorityPrompt).digest("hex") !== retained.lifecycle_delivery.sha256)
404
+ throw new AppError("recovery_packet_mismatch", "Retained lifecycle delivery checksum differs", 1);
405
+ if (binding.harness === "zcode-acp" || (binding.harness === "codex-desktop" && hasLifecyclePacketAssignments(context, authorityPrompt))) {
291
406
  const scope = { projectRoot: run.project_root, daemonId: binding.daemon_id, rootSessionId: binding.provider_session_id, runId: run.id, generation: guard.generation };
292
407
  const assignments = new Set();
293
408
  packetReceipt.lifecycle_recovery = {
294
- commands: retained.continuation === "stage_boundary" ? [] : recoverLifecycleCommands({ ...context, captureLifecycleAssignment: id => { assignments.add(id); } }, retained.original_prompt.text, scope),
409
+ commands: retained.continuation === "stage_boundary" ? [] : recoverLifecycleCommands({ ...context, captureLifecycleAssignment: id => { assignments.add(id); } }, authorityPrompt, scope),
295
410
  instruction: "original_prompt is immutable historical evidence. Do not execute its old command authority. Use only replacement commands below; reconcile unknown outcomes before continuing. At a completed stage boundary return control to the runner.",
296
411
  work_list_command: `${flowCommand({ ...context, env: { ...context.env, DD_FLOW_INVOCATION_SCOPE: JSON.stringify(scope) } })} work ls --run ${run.id} --ready --project-root ${JSON.stringify(run.project_root)} --json`
297
412
  };
@@ -308,10 +423,35 @@ export function acceptRunRecovery(context, input) {
308
423
  const prior = context.db.get("SELECT ws.prompt_path, ws.result_path FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id WHERE ws.id = ? AND ws.work_id = ? AND ws.session_id = ? AND ws.status = ? AND w.status = ?", [binding.prior_work_session_id, binding.work_id, binding.session_id, packet.retained_session_status ?? "running", packet.retained_work_status ?? (JSON.parse(binding.packet_json).paused ? "paused" : "running")]);
309
424
  if (!prior)
310
425
  throw new AppError("recovery_owner_changed", "Retained root WorkSession changed before recovery acceptance", 1);
426
+ let physicalBinding = null;
427
+ if (!boundary && coordinatorScope && bundle) {
428
+ const currentRun = assertPreparedRunBundleSqlAuthority(context, coordinatorScope, bundle);
429
+ if (preparedBinding?.packet_sha256 !== binding.packet_sha256 || preparedBinding.prior_work_session_id !== binding.prior_work_session_id
430
+ || preparedPacket?.stage !== JSON.parse(currentRun.index_json).current_stage
431
+ || readDescriptorCoordinatorSession(context, { scope: coordinatorScope, bundle, workId: binding.work_id, recoveryRead: recoveryRead }) !== preparedCustody)
432
+ throw new AppError("recovery_owner_changed", "Original coordinator custody changed before recovery acknowledgement", 1);
433
+ const route = bundle.execution.stage(coordinatorScope.stage);
434
+ if (identity.model !== route.profile.model)
435
+ throw new AppError("recovery_binding_mismatch", "Recovery acknowledgement model differs from the frozen coordinator", 1);
436
+ const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id = ? AND event_type = 'stage.coordinator_binding' AND json_extract(payload_json, '$.run_id') = ? AND json_extract(payload_json, '$.scope.stage') = ? AND json_extract(payload_json, '$.work_session_id') = ?", [project.id, run.id, coordinatorScope.stage, binding.prior_work_session_id]);
437
+ physicalBinding = JSON.parse(rows[0].payload_json);
438
+ }
311
439
  context.db.run("UPDATE hook_events SET status = 'claimed', claimed_at = ? WHERE id = ? AND status = 'observed'", [now, identity.hookEventId]);
312
440
  if (!boundary) {
441
+ const recoveredWorkSessionId = `WS-${crypto.randomUUID()}`;
313
442
  context.db.run("UPDATE work_sessions SET status = 'interrupted', completed_at = ?, updated_at = ? WHERE id = ?", [now, now, binding.prior_work_session_id]);
314
- context.db.run("INSERT INTO work_sessions (id, work_id, session_id, hook_event_id, status, prompt_path, result_path, created_at, updated_at) VALUES (?, ?, ?, ?, 'running', ?, ?, ?, ?)", [`WS-${crypto.randomUUID()}`, binding.work_id, binding.session_id, eventKey, prior.prompt_path, prior.result_path, now, now]);
443
+ context.db.run("INSERT INTO work_sessions (id, work_id, session_id, hook_event_id, status, prompt_path, result_path, created_at, updated_at) VALUES (?, ?, ?, ?, 'running', ?, ?, ?, ?)", [recoveredWorkSessionId, binding.work_id, binding.session_id, eventKey, prior.prompt_path, prior.result_path, now, now]);
444
+ if (physicalBinding)
445
+ appendAudit(context, { projectId: project.id, eventType: "stage.coordinator_binding", payload: recoveredCoordinatorBinding(physicalBinding, {
446
+ projectId: project.id, runId: run.id, stage: coordinatorScope.stage, workId: binding.work_id,
447
+ priorWorkSessionId: binding.prior_work_session_id, workSessionId: recoveredWorkSessionId,
448
+ sessionId: identity.sessionId, harness: identity.harness, providerSessionId: identity.providerSessionId,
449
+ model: identity.model, frozenModel: bundle.execution.stage(coordinatorScope.stage).profile.model,
450
+ executionSha256: bundle.execution.sha256, profileId: bundle.execution.stage(coordinatorScope.stage).profile.id,
451
+ profileSha256: hybridDigest(bundle.execution.stage(coordinatorScope.stage).profile), sessionMode: bundle.execution.stage(coordinatorScope.stage).session_mode,
452
+ recoveryId: guard.recovery_id, generation: guard.generation, packetSha256: binding.packet_sha256,
453
+ hookEventKey: eventKey, daemonId: identity.daemonId
454
+ }) });
315
455
  }
316
456
  context.db.run("UPDATE run_recovery_bindings SET status = 'accepted', hook_event_key = ?, accepted_at = ? WHERE recovery_id = ?", [eventKey, now, binding.recovery_id]);
317
457
  context.db.run("UPDATE run_recovery_guards SET status = 'recovered', resumed_at = ?, updated_at = ? WHERE recovery_id = ?", [now, now, guard.recovery_id]);
@@ -0,0 +1,177 @@
1
+ import { createHash } from "node:crypto";
2
+ import path from "node:path";
3
+ import { AppError } from "../shared/errors.js";
4
+ import { hybridDigest } from "./hybrid-contracts.js";
5
+ export const runStartPayloadLimit = 64 * 1024 * 1024;
6
+ export function runStartBytesSha256(bytes) { return createHash("sha256").update(bytes).digest("hex"); }
7
+ export function refuseRunStart(reason) {
8
+ throw new AppError("run_start_custody_invalid", "RUN creation requires its exact retained request", 1, { reason, effect: "no_effect", next_action: "reconcile_exact_request" });
9
+ }
10
+ export function hasRunStartTable(context) {
11
+ return Boolean(context.db.get("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = 'run_start_requests'"));
12
+ }
13
+ function object(value) {
14
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
15
+ }
16
+ function exactKeys(value, keys) {
17
+ return Object.keys(value).sort().join("\0") === keys.sort().join("\0");
18
+ }
19
+ function digest(value) { return typeof value === "string" && /^[a-f0-9]{64}$/.test(value); }
20
+ function canonicalBase64(value) {
21
+ return typeof value === "string" && Buffer.from(value, "base64").toString("base64") === value;
22
+ }
23
+ /** Retained data can drive filesystem writes only after a closed shape check. */
24
+ export function validateRunStartPayload(value) {
25
+ if (!object(value) || !exactKeys(value, ["schema_id", "request_id", "project_id", "project_root", "workspace_root", "home_root",
26
+ "run_id", "request_sha256", "bundle_sha256", "subject_type", "subject_id", "slug", "run_root", "initial_index",
27
+ "bundle_manifest_base64", "blobs", "artifacts", "owners", "git"]))
28
+ refuseRunStart("invalid_allocation_shape");
29
+ for (const key of ["request_id", "project_id", "run_id", "slug", "subject_type", "subject_id"])
30
+ if (typeof value[key] !== "string" || !value[key].trim() || value[key].length > 1024)
31
+ refuseRunStart("invalid_allocation_shape");
32
+ if (!/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(value.project_id)
33
+ || !/^RUN-\d+-[a-z0-9][a-z0-9-]*$/.test(value.run_id))
34
+ refuseRunStart("invalid_allocation_shape");
35
+ for (const key of ["project_root", "workspace_root", "home_root", "run_root"])
36
+ if (typeof value[key] !== "string" || !path.isAbsolute(value[key]) || path.resolve(value[key]) !== value[key])
37
+ refuseRunStart("invalid_allocation_scope");
38
+ if (value.run_root !== path.join(value.home_root, "projects", value.project_id, "runs", value.run_id)
39
+ || !digest(value.request_sha256) || !digest(value.bundle_sha256) || !canonicalBase64(value.bundle_manifest_base64)
40
+ || runStartBytesSha256(Buffer.from(value.bundle_manifest_base64, "base64")) !== value.bundle_sha256
41
+ || !object(value.initial_index) || !object(value.owners) || !exactKeys(value.owners, ["home", "project", "workspace"]))
42
+ refuseRunStart("invalid_allocation_shape");
43
+ for (const key of ["home", "project", "workspace"]) {
44
+ const owner = value.owners[key];
45
+ if (!object(owner) || !exactKeys(owner, ["dev", "ino"]) || ![owner.dev, owner.ino].every(number => Number.isSafeInteger(number) && Number(number) >= 0))
46
+ refuseRunStart("invalid_allocation_owner");
47
+ }
48
+ if (!object(value.git) || !exactKeys(value.git, ["branch", "head", "status_sha256"]) || typeof value.git.branch !== "string" || !value.git.branch
49
+ || value.git.head !== null && (typeof value.git.head !== "string" || !/^[a-f0-9]{40,64}$/.test(value.git.head))
50
+ || !digest(value.git.status_sha256))
51
+ refuseRunStart("invalid_allocation_git");
52
+ for (const key of ["blobs", "artifacts"]) {
53
+ const entries = value[key];
54
+ if (!Array.isArray(entries) || entries.length > 4096)
55
+ refuseRunStart("invalid_allocation_assets");
56
+ const seen = new Set();
57
+ for (const entry of entries) {
58
+ if (!object(entry) || !exactKeys(entry, ["path", "base64"]) || typeof entry.path !== "string"
59
+ || !entry.path || entry.path.includes("\\") || entry.path.includes("\0") || entry.path.split("/").some(part => !part || part === "." || part === "..")
60
+ || seen.has(entry.path) || !canonicalBase64(entry.base64))
61
+ 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))
64
+ refuseRunStart("invalid_allocation_artifact");
65
+ seen.add(entry.path);
66
+ }
67
+ if (key === "artifacts" && (!seen.has("engine-binding.json") || seen.has("operational-policy.source.md") && !seen.has("operational-contract.json")))
68
+ refuseRunStart("invalid_allocation_artifact");
69
+ }
70
+ }
71
+ /** Metadata and copied SQL bytes only. No engine/filesystem/provider admission. */
72
+ export function readRunStartRequest(context, projectId, requestId) {
73
+ return context.db.readSnapshot(() => readRunStartRequestSnapshot(context, projectId, requestId));
74
+ }
75
+ function readRunStartRequestSnapshot(context, projectId, requestId) {
76
+ if (!/^[A-Za-z0-9][A-Za-z0-9_.-]{0,95}$/.test(requestId))
77
+ refuseRunStart("invalid_request_id");
78
+ const row = hasRunStartTable(context) ? context.db.get("SELECT run_id, payload_json, payload_sha256 FROM run_start_requests WHERE project_id = ? AND request_id = ?", [projectId, requestId]) : undefined;
79
+ const starts = context.db.all("SELECT payload_json FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.start_request' AND json_extract(payload_json, '$.request_id') = ? LIMIT 2", [projectId, requestId]);
80
+ if (!row) {
81
+ const marker = context.db.get("SELECT 1 FROM runs WHERE project_id = ? AND json_extract(index_json, '$.start_request.request_id') = ? LIMIT 1", [projectId, requestId]);
82
+ const bound = context.db.get("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = 'run_bundle_authorities'")
83
+ && context.db.get("SELECT 1 FROM run_bundle_authorities WHERE project_id = ? AND json_extract(admission_json, '$.executionScope.requestId') = ? LIMIT 1", [projectId, requestId]);
84
+ if (starts.length || marker || bound)
85
+ refuseRunStart("request_receipt_missing");
86
+ return null;
87
+ }
88
+ if (starts.length !== 1 || Buffer.byteLength(row.payload_json) > runStartPayloadLimit
89
+ || runStartBytesSha256(row.payload_json) !== row.payload_sha256)
90
+ refuseRunStart("allocation_changed");
91
+ let payload, receipt;
92
+ try {
93
+ payload = JSON.parse(row.payload_json);
94
+ receipt = JSON.parse(starts[0].payload_json);
95
+ }
96
+ catch {
97
+ return refuseRunStart("invalid_allocation_json");
98
+ }
99
+ validateRunStartPayload(payload);
100
+ if (!object(receipt))
101
+ refuseRunStart("invalid_allocation_receipt");
102
+ 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]);
103
+ let index;
104
+ try {
105
+ if (!run)
106
+ refuseRunStart("allocated_run_missing");
107
+ index = JSON.parse(run.index_json);
108
+ }
109
+ catch (error) {
110
+ if (error instanceof AppError)
111
+ throw error;
112
+ return refuseRunStart("allocated_run_changed");
113
+ }
114
+ if (!object(index))
115
+ refuseRunStart("allocated_run_changed");
116
+ const marker = index.start_request, initial = payload.initial_index;
117
+ if (!payload || payload.schema_id !== "dd-flow/run-start-allocation@1" || payload.project_id !== projectId
118
+ || payload.request_id !== requestId || payload.run_id !== row.run_id || !Array.isArray(payload.blobs) || !Array.isArray(payload.artifacts)
119
+ || !initial || initial.run_id !== row.run_id || initial.project?.id !== projectId || initial.start_request !== undefined
120
+ || initial.execution_bundle?.manifest_sha256 !== payload.bundle_sha256 || !/^[a-f0-9]{64}$/.test(payload.request_sha256)
121
+ || !marker || marker.schema_id !== "dd-flow/run-start-request@1" || marker.request_id !== requestId
122
+ || marker.request_sha256 !== payload.request_sha256 || marker.allocation_sha256 !== row.payload_sha256
123
+ || 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
125
+ || run.flow_kind !== initial.flow_kind || run.subject_id !== payload.subject_id || run.subject_type !== payload.subject_type
126
+ || !object(index.execution_bundle) || hybridDigest(index.execution_bundle) !== hybridDigest(initial.execution_bundle))
127
+ refuseRunStart("allocated_run_changed");
128
+ const publications = context.db.all("SELECT payload_json FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.start_published' AND json_extract(payload_json, '$.run_id') = ? LIMIT 2", [projectId, row.run_id]);
129
+ if (publications.length > 1 || Boolean(marker.publication_sha256) !== (publications.length === 1))
130
+ refuseRunStart("publication_receipt_missing");
131
+ if (publications.length) {
132
+ let publication;
133
+ try {
134
+ publication = JSON.parse(publications[0].payload_json);
135
+ }
136
+ catch {
137
+ return refuseRunStart("invalid_publication_receipt");
138
+ }
139
+ if (!object(publication) || publication.schema_id !== "dd-flow/run-start-publication@1" || hybridDigest(publication) !== marker.publication_sha256 || publication.request_id !== requestId
140
+ || publication.run_id !== row.run_id || publication.allocation_sha256 !== row.payload_sha256 || publication.bundle_sha256 !== payload.bundle_sha256)
141
+ refuseRunStart("publication_receipt_changed");
142
+ const owners = context.db.all("SELECT payload_json FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.start_root_owned' AND json_extract(payload_json, '$.run_id') = ? LIMIT 2", [projectId, row.run_id]);
143
+ if (owners.length !== 1)
144
+ refuseRunStart("root_ownership_receipt_missing");
145
+ let owner;
146
+ try {
147
+ owner = JSON.parse(owners[0].payload_json);
148
+ }
149
+ catch {
150
+ return refuseRunStart("root_ownership_receipt_changed");
151
+ }
152
+ if (!object(owner) || owner.schema_id !== "dd-flow/run-start-owner@1" || owner.run_id !== row.run_id || owner.request_id !== requestId
153
+ || owner.allocation_sha256 !== row.payload_sha256 || hybridDigest(owner) !== publication.root_owner_sha256)
154
+ refuseRunStart("root_ownership_receipt_changed");
155
+ }
156
+ return { payload, payloadJson: row.payload_json, sha256: row.payload_sha256, published: Boolean(marker.publication_sha256) };
157
+ }
158
+ /** Lost markers/receipts cannot silently become legacy mutable execution. */
159
+ export function assertRunStartupSettled(context, projectId, runId) {
160
+ const run = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
161
+ let marker;
162
+ try {
163
+ marker = run ? JSON.parse(run.index_json).start_request : undefined;
164
+ }
165
+ catch {
166
+ return refuseRunStart("allocated_run_changed");
167
+ }
168
+ const row = hasRunStartTable(context) ? context.db.get("SELECT request_id FROM run_start_requests WHERE project_id = ? AND run_id = ?", [projectId, runId]) : undefined;
169
+ const audit = context.db.get("SELECT json_extract(payload_json, '$.request_id') AS request_id FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.start_request' AND json_extract(payload_json, '$.run_id') = ? LIMIT 1", [projectId, runId]);
170
+ if (!marker && !row && !audit)
171
+ return;
172
+ if (!marker || !row || !audit || row.request_id !== marker.request_id || audit.request_id !== marker.request_id)
173
+ refuseRunStart("request_receipt_missing");
174
+ const retained = readRunStartRequest(context, projectId, marker.request_id);
175
+ if (!retained?.published)
176
+ throw new AppError("run_start_publication_pending", "Finish the original RUN creation request before lifecycle writes", 1, { effect: "no_effect", run_id: runId, request_id: marker.request_id, next_action: "reconcile_exact_request" });
177
+ }