@deksden-com/dd-flow-cli 0.9.0-beta.11 → 0.9.0-beta.111

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 (162) hide show
  1. package/CHANGELOG.md +526 -0
  2. package/README.md +68 -0
  3. package/dist/build-info.json +8 -8
  4. package/dist/cli/command-inputs.js +305 -0
  5. package/dist/cli/help.js +89 -13
  6. package/dist/cli/hook-ingress.js +77 -0
  7. package/dist/cli/input-preparation.js +116 -0
  8. package/dist/cli/native-hook-ingress.js +48 -0
  9. package/dist/cli/run-cli.js +1465 -384
  10. package/dist/cli.js +8 -2
  11. package/dist/harness-runtime/DD-ZCODE.md +99 -0
  12. package/dist/harness-runtime/bin/dd-agy.mjs +52 -0
  13. package/dist/harness-runtime/bin/dd-codex.mjs +26 -0
  14. package/dist/harness-runtime/bin/dd-droid.mjs +33 -0
  15. package/dist/harness-runtime/bin/dd-grok.mjs +30 -0
  16. package/dist/harness-runtime/bin/dd-opencode.mjs +21 -0
  17. package/dist/harness-runtime/bin/dd-zcode.mjs +83 -0
  18. package/dist/harness-runtime/lib/agy-hooks.d.mts +1 -0
  19. package/dist/harness-runtime/lib/agy-hooks.mjs +9 -0
  20. package/dist/harness-runtime/lib/daemon-operations.mjs +226 -0
  21. package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +6 -0
  22. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +500 -0
  23. package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
  24. package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
  25. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +149 -0
  26. package/dist/harness-runtime/lib/dd-codex.mjs +563 -0
  27. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
  28. package/dist/harness-runtime/lib/dd-droid.mjs +475 -0
  29. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +394 -0
  30. package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
  31. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +219 -0
  32. package/dist/harness-runtime/lib/dd-opencode.mjs +98 -0
  33. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +645 -0
  34. package/dist/harness-runtime/lib/dd-zcode.mjs +982 -0
  35. package/dist/harness-runtime/lib/delegation-instructions.d.mts +13 -0
  36. package/dist/harness-runtime/lib/delegation-instructions.mjs +144 -0
  37. package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
  38. package/dist/harness-runtime/lib/driver-recovery.mjs +129 -0
  39. package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
  40. package/dist/harness-runtime/lib/managed-daemon.mjs +327 -0
  41. package/dist/harness-runtime/lib/model-observations.mjs +77 -0
  42. package/dist/harness-runtime/lib/native-hook-command.d.mts +3 -0
  43. package/dist/harness-runtime/lib/native-hook-command.mjs +108 -0
  44. package/dist/harness-runtime/lib/observation-clock.mjs +36 -0
  45. package/dist/harness-runtime/lib/operation-context.mjs +5 -0
  46. package/dist/harness-runtime/lib/operation-errors.mjs +19 -0
  47. package/dist/harness-runtime/lib/process-json.mjs +71 -0
  48. package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
  49. package/dist/harness-runtime/lib/runner-events.mjs +200 -0
  50. package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
  51. package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
  52. package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
  53. package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
  54. package/dist/harness-runtime/lib/zcode-usage.mjs +54 -0
  55. package/dist/protocol/local-files.js +18 -9
  56. package/dist/runtime/context.js +2 -2
  57. package/dist/schemas/agent-profile.schema.json +1 -1
  58. package/dist/schemas/code-review-result.schema.json +1 -1
  59. package/dist/schemas/code-work-batch.schema.json +3 -3
  60. package/dist/schemas/code-work-result.schema.json +3 -3
  61. package/dist/schemas/plan-review-result.schema.json +2 -2
  62. package/dist/schemas/run-control-receipt.schema.json +99 -0
  63. package/dist/schemas/run-control-request.schema.json +36 -0
  64. package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
  65. package/dist/services/canon.js +11 -8
  66. package/dist/services/cleanup.js +132 -39
  67. package/dist/services/cli-operation-classifier.js +22 -24
  68. package/dist/services/code-checks.js +589 -106
  69. package/dist/services/codex-hook-delivery.js +28 -0
  70. package/dist/services/command-context.js +80 -0
  71. package/dist/services/config.js +11 -8
  72. package/dist/services/continuation-outcome.js +20 -0
  73. package/dist/services/controller-fanout.js +173 -0
  74. package/dist/services/dashboard.js +103 -31
  75. package/dist/services/engines.js +121 -76
  76. package/dist/services/eval-snapshots.js +994 -97
  77. package/dist/services/execution-policy.js +181 -0
  78. package/dist/services/external-work-launch.js +166 -0
  79. package/dist/services/harness-adapter.js +147 -24
  80. package/dist/services/harness-config.js +2 -0
  81. package/dist/services/hooks.js +702 -327
  82. package/dist/services/lanes.js +78 -65
  83. package/dist/services/lifecycle-command.js +113 -18
  84. package/dist/services/lifecycle-contract.js +30 -0
  85. package/dist/services/lifecycle-invocations.js +1379 -0
  86. package/dist/services/managed-daemon-binding.js +38 -0
  87. package/dist/services/managed-processes.js +302 -34
  88. package/dist/services/merge-queue.js +228 -149
  89. package/dist/services/merge-server.js +66 -28
  90. package/dist/services/migrations.js +37 -16
  91. package/dist/services/native-daemon-history.js +49 -0
  92. package/dist/services/native-session-control.js +39 -0
  93. package/dist/services/plan-runtime.js +26 -11
  94. package/dist/services/plans.js +28 -25
  95. package/dist/services/projects.js +20 -15
  96. package/dist/services/prompts.js +25 -16
  97. package/dist/services/protocols.js +160 -86
  98. package/dist/services/recovery-observation-budget.js +61 -0
  99. package/dist/services/recovery-snapshot-database.js +142 -0
  100. package/dist/services/repair-intents.js +40 -0
  101. package/dist/services/review-copy.js +136 -0
  102. package/dist/services/run-control-receipt.js +56 -0
  103. package/dist/services/run-control-worker.js +351 -0
  104. package/dist/services/run-control.js +888 -0
  105. package/dist/services/run-controller-adapter.js +231 -0
  106. package/dist/services/run-controller-capture.js +167 -0
  107. package/dist/services/run-controller-process.js +180 -0
  108. package/dist/services/run-controller-recovery.js +221 -0
  109. package/dist/services/run-controller-state.js +36 -0
  110. package/dist/services/run-controller.js +798 -0
  111. package/dist/services/run-engine-bindings.js +1 -1
  112. package/dist/services/run-fork.js +142 -0
  113. package/dist/services/run-observations.js +112 -0
  114. package/dist/services/run-projection.js +22 -64
  115. package/dist/services/run-recovery-runtime.js +80 -0
  116. package/dist/services/run-recovery.js +345 -0
  117. package/dist/services/runs.js +536 -181
  118. package/dist/services/runtime-budget.js +413 -0
  119. package/dist/services/runtime-command.js +43 -0
  120. package/dist/services/runtime-scope-capture.js +52 -0
  121. package/dist/services/runtime-scope-control.js +460 -0
  122. package/dist/services/runtime-scope-resume.js +577 -0
  123. package/dist/services/runtime-scope-stop.js +159 -0
  124. package/dist/services/runtime-scope-worker.js +242 -0
  125. package/dist/services/runtime-service.js +97 -0
  126. package/dist/services/schema-validation.js +10 -8
  127. package/dist/services/scope-writer-cleanup.js +26 -0
  128. package/dist/services/sessions.js +35 -69
  129. package/dist/services/stage-blocker.js +17 -7
  130. package/dist/services/stage-context.js +50 -19
  131. package/dist/services/stage-lifecycle.js +100 -103
  132. package/dist/services/stage-pause.js +88 -75
  133. package/dist/services/stage-report-renderer.js +116 -6
  134. package/dist/services/stage-work-graph.js +35 -0
  135. package/dist/services/usage.js +35 -98
  136. package/dist/services/vnext-code-review.js +197 -84
  137. package/dist/services/vnext-code.js +278 -251
  138. package/dist/services/vnext-execution-profile.js +5 -3
  139. package/dist/services/vnext-fanout.js +172 -17
  140. package/dist/services/vnext-merge.js +345 -111
  141. package/dist/services/vnext-plan-review.js +171 -111
  142. package/dist/services/vnext-plan.js +180 -87
  143. package/dist/services/vnext-protocolize.js +320 -74
  144. package/dist/services/vnext-specify.js +115 -77
  145. package/dist/services/work-registry.js +1001 -187
  146. package/dist/services/workspace-bootstrap.js +76 -0
  147. package/dist/services/workspace-files.js +83 -0
  148. package/dist/services/worktrees.js +88 -36
  149. package/dist/shared/entity-references.js +8 -0
  150. package/dist/shared/errors.js +12 -0
  151. package/dist/storage/database.js +692 -35
  152. package/dist/storage/foreign-key-transaction.js +39 -0
  153. package/dist/storage/immutable-input.js +45 -0
  154. package/dist/storage/import-guard.js +102 -0
  155. package/dist/storage/native-cleanup.js +52 -0
  156. package/dist/storage/paths.js +17 -4
  157. package/dist/storage/projection-lock.js +73 -0
  158. package/dist/storage/work-references.js +23 -0
  159. package/dist/storage/writer-contract.js +77 -0
  160. package/dist/storage/writer-migration.js +85 -0
  161. package/package.json +24 -13
  162. package/tools/repair-paused-run-status.mjs +84 -0
@@ -0,0 +1,577 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { isDeepStrictEqual } from "node:util";
5
+ import { createContext } from "../runtime/context.js";
6
+ import { AppError } from "../shared/errors.js";
7
+ import { getResourceDatabase } from "../storage/database.js";
8
+ import { managedProcessBinding, processIsAlive, processTreeIsAlive, resourceHome } from "./managed-processes.js";
9
+ import { runtimeBudgetStatus, scopeLocalProcessRole, validateRetainedRuntimeScopeManifest, prepareRuntimeScopeFence } from "./runtime-budget.js";
10
+ import { nativeInventoryVersion, retainedDaemonOperations } from "./run-control.js";
11
+ import { adapterSessionId, runHarnessAdapter } from "./harness-adapter.js";
12
+ import { prepareControllerResume, resumeRunController } from "./run-controller-recovery.js";
13
+ import { readNativeDaemonState, requireManagedDaemonBinding } from "./managed-daemon-binding.js";
14
+ import { assertHistoricalDaemonSettlement } from "./native-daemon-history.js";
15
+ import { cleanupScopeWriters } from "./scope-writer-cleanup.js";
16
+ const hash = (value) => crypto.createHash("sha256").update(value).digest("hex");
17
+ const nativeLaunchEnvironmentKeys = ["CODEX_HOME", "DD_FLOW_CONFIG_HOME", "DD_FLOW_HOME", "DD_FLOW_BIN", "DD_FLOW_RESOURCE_HOME"];
18
+ function nativeLaunchEnvironment(context, saved) {
19
+ const env = { ...context.env };
20
+ 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"])
21
+ delete env[name];
22
+ if (!saved || Object.keys(saved).some(name => !nativeLaunchEnvironmentKeys.includes(name)))
23
+ throw new AppError("runtime_scope_native_launch_required", "Native launch has an unsupported environment", 1);
24
+ for (const name of nativeLaunchEnvironmentKeys) {
25
+ if (saved[name] === null)
26
+ delete env[name];
27
+ else if (typeof saved[name] === "string")
28
+ env[name] = saved[name];
29
+ else
30
+ throw new AppError("runtime_scope_native_launch_required", "Native launch must retain explicit configuration presence", 1);
31
+ }
32
+ return env;
33
+ }
34
+ /** The existing detached scope owner drives preparation; clients never own
35
+ * the lifecycle. All-role release remains distinct from these RUN receipts. */
36
+ export async function prepareRuntimeScopeResume(context, scopeId, generation, releaseRequestId) {
37
+ const db = getResourceDatabase(resourceHome(context));
38
+ const assertCurrent = () => {
39
+ const latest = runtimeBudgetStatus(context, scopeId);
40
+ if (latest.fence || latest.control?.generation !== generation)
41
+ throw new AppError("runtime_scope_control_superseded", "Scope resume preparation lost its generation", 1);
42
+ };
43
+ const row = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-resume' AND resource_key = ?", [scopeId]);
44
+ if (!row)
45
+ return null;
46
+ const intent = JSON.parse(row.metadata_json);
47
+ if (intent.generation !== generation)
48
+ return null;
49
+ if (releaseRequestId !== undefined && releaseRequestId !== intent.request_id)
50
+ throw new AppError("runtime_scope_resume_conflict", "Release belongs to another resume request", 1);
51
+ if (releaseRequestId !== undefined && intent.status === "released") {
52
+ const prior = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-resume-release' AND resource_key = ?", [hash(JSON.stringify([scopeId, generation]))]);
53
+ if (!prior)
54
+ throw new AppError("runtime_scope_release_unproven", "Released scope lost its retained grant", 1);
55
+ const latest = runtimeBudgetStatus(context, scopeId);
56
+ const last = db.get("SELECT MAX(json_extract(metadata_json, '$.generation')) AS generation FROM managed_resources WHERE resource_kind = 'scope-control-request' AND owner_id = ?", [scopeId]);
57
+ return { ...JSON.parse(prior.metadata_json), status: "released", dispatch_blocked: latest.dispatch_blocked, current: !latest.fence && !latest.control && last?.generation === generation, settled: false, reused: true };
58
+ }
59
+ assertCurrent();
60
+ const runs = [];
61
+ for (const source of intent.runs) {
62
+ let target;
63
+ try {
64
+ target = createContext({ ...context.env, DD_FLOW_HOME: source.runtime_home, DD_FLOW_RESOURCE_HOME: resourceHome(context), DD_FLOW_ENGINE_MODE: "1" });
65
+ const project = target.db.get("SELECT root FROM projects WHERE id = ?", [source.project_id]);
66
+ const controller = target.db.get("SELECT manifest_json FROM run_controllers WHERE project_id = ? AND run_id = ? AND status NOT IN ('completed','cancelled','superseded')", [source.project_id, source.run_id]);
67
+ const manifest = controller ? JSON.parse(controller.manifest_json) : null;
68
+ if (!project || manifest?.budget?.scope_id !== scopeId || manifest.resource_home !== resourceHome(context))
69
+ throw new AppError("runtime_scope_owner_unknown", "Resume RUN no longer belongs to this retained scope", 1);
70
+ const input = { projectRoot: project.root, runId: source.run_id, requestId: source.resume_request_id, controlId: source.control_id };
71
+ const commitResume = prepareControllerResume(target, input);
72
+ db.writeTransaction(() => { assertCurrent(); commitResume(); });
73
+ assertCurrent();
74
+ const receipt = await resumeRunController(target, { ...input, waitMs: 0 });
75
+ assertCurrent();
76
+ runs.push({ ...source, receipt });
77
+ }
78
+ catch (error) {
79
+ runs.push({ ...source, pending: true, reason: error instanceof AppError ? error.code : "runtime_scope_resume_failed", message: String(error) });
80
+ }
81
+ finally {
82
+ target?.db.close?.();
83
+ }
84
+ }
85
+ const native = [];
86
+ for (const source of intent.native) {
87
+ try {
88
+ const key = hash(JSON.stringify([scopeId, generation, source.process_id]));
89
+ const saved = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-resume-native' AND resource_key = ?", [key]);
90
+ const restarting = saved && ["starting", "restarted"].includes(JSON.parse(saved.metadata_json).status);
91
+ const retired = restarting ? null : await retireScopeNative(context, intent, source, assertCurrent);
92
+ native.push(restarting || retired?.status === "retired" ? await restartScopeNative(context, intent, source, assertCurrent) : retired);
93
+ }
94
+ catch (error) {
95
+ native.push({ ...source, pending: true, reason: error instanceof AppError ? error.code : "runtime_scope_resume_failed", message: String(error) });
96
+ }
97
+ }
98
+ assertCurrent();
99
+ // Recheck all RUNs together after asynchronous preparation of every role.
100
+ // These locks will also be the boundary for the eventual release publication.
101
+ const lockedRuns = new Map(), heldRuns = [];
102
+ let registryHeld = false, runPreparationConsistent = false;
103
+ let released = null, releaseError = null, releaseMessage = null;
104
+ let cleanupErrors = [];
105
+ try {
106
+ for (const home of [...new Set(intent.runs.map((source) => source.runtime_home))].sort())
107
+ lockedRuns.set(home, createContext({ ...context.env, DD_FLOW_HOME: home, DD_FLOW_RESOURCE_HOME: resourceHome(context), DD_FLOW_ENGINE_MODE: "1" }));
108
+ const checks = [];
109
+ for (const [index, source] of intent.runs.entries()) {
110
+ const target = lockedRuns.get(source.runtime_home);
111
+ const prepared = preparedScopeRun(target, { scopeId, requestId: source.resume_request_id, projectId: source.project_id, runId: source.run_id, controlId: source.control_id, recoveryId: source.recovery_id }, assertCurrent);
112
+ const proof = prepared.proof;
113
+ checks.push(prepared.assertUnchanged);
114
+ runs[index] = { ...runs[index], ...proof, ...(proof.prepared === true ? { pending: false, reason: null } : {}) };
115
+ }
116
+ const commitRelease = releaseRequestId === undefined ? null : prepareScopeRelease(context, intent, runs, native);
117
+ db.beginWriteTransaction();
118
+ registryHeld = true;
119
+ for (const target of lockedRuns.values()) {
120
+ target.db.beginWriteTransaction();
121
+ heldRuns.push(target);
122
+ }
123
+ assertCurrent();
124
+ for (const check of checks)
125
+ check();
126
+ runPreparationConsistent = true;
127
+ if (commitRelease) {
128
+ const receipt = commitRelease();
129
+ db.exec("COMMIT");
130
+ registryHeld = false;
131
+ released = receipt;
132
+ }
133
+ }
134
+ catch (error) {
135
+ releaseError = error instanceof AppError ? error.code : "scope_writer_boundary_unavailable";
136
+ releaseMessage = error instanceof Error ? error.message : String(error);
137
+ for (const [index, run] of runs.entries())
138
+ runs[index] = { ...run, prepared: false, pending: true, reason: error instanceof AppError ? error.code : "scope_writer_boundary_unavailable" };
139
+ }
140
+ finally {
141
+ cleanupErrors = cleanupScopeWriters([
142
+ ...[...lockedRuns.values()].reverse().map(target => ({ name: target.ddFlowHome, db: target.db, rollback: heldRuns.includes(target), close: "always" })),
143
+ { name: resourceHome(context), db, rollback: registryHeld, close: "on_error" }
144
+ ]);
145
+ }
146
+ if (released)
147
+ return { ...released, status: "released", dispatch_blocked: false, current: true, settled: false, reused: false, ...(cleanupErrors.length ? { cleanup_errors: cleanupErrors } : {}) };
148
+ if (cleanupErrors.length)
149
+ throw new AppError(releaseError ?? "scope_writer_cleanup_failed", releaseMessage ?? "Scope preparation cleanup failed", 1, { scope_cleanup_errors: cleanupErrors });
150
+ assertCurrent();
151
+ return { scope_id: scopeId, generation, request_id: intent.request_id, status: "preparing", dispatch_blocked: true, settled: false, release_error: releaseError ? { code: releaseError, message: releaseMessage } : null, run_preparation_consistent: runPreparationConsistent, runs, native, probes: intent.probes, pending_reasons: [...new Set(["scope_release_required", ...(releaseError ? [releaseError] : []), ...(!runPreparationConsistent ? ["scope_writer_boundary_unavailable"] : []), ...(runs.some(item => item.prepared !== true) ? ["run_recovery_required"] : []), ...(native.some(item => item?.prepared !== true) ? ["non_work_recovery_required"] : []), ...(intent.probes.length ? ["probe_reconciliation_required"] : [])])] };
152
+ }
153
+ /** Read filesystem/native evidence before reserving writers. The returned
154
+ * SQL-only commit is called with registry -> all-RUN barriers held. */
155
+ function prepareScopeRelease(context, intent, runs, native) {
156
+ const db = getResourceDatabase(resourceHome(context));
157
+ if (runs.some(run => run.prepared !== true) || native.some(owner => owner?.prepared !== true))
158
+ throw new AppError("runtime_scope_preparation_required", "Every owned RUN and native Session must be prepared", 1);
159
+ const manifest = validateRetainedRuntimeScopeManifest(intent.manifest, intent.scope_id);
160
+ const artifactRow = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control-capture' AND resource_key = ?", [intent.capture_key]);
161
+ const artifact = artifactRow ? JSON.parse(artifactRow.metadata_json) : null;
162
+ const journal = path.join(path.dirname(manifest.manifest_path), "events.jsonl");
163
+ if (artifact?.scope_id !== intent.scope_id || artifact.generation !== intent.generation || artifact.journal_source !== journal || typeof artifact.journal_bytes !== "string" || !fs.readFileSync(journal).equals(Buffer.from(artifact.journal_bytes)))
164
+ throw new AppError("runtime_scope_journal_changed", "Release source changed after capture", 1);
165
+ const operations = new Map();
166
+ const events = artifact.journal_bytes.trimEnd().split("\n").map((line) => JSON.parse(line));
167
+ for (const event of events) {
168
+ if (!event.type.startsWith("dev.dd.eval.operation."))
169
+ continue;
170
+ const phase = event.type.slice("dev.dd.eval.operation.".length), id = event.data.operation_id;
171
+ const prior = operations.get(id);
172
+ if (typeof id !== "string" || !id || typeof event.data.operation !== "string" || !event.data.operation || !["requested", "started", "suspended", "observation_lost", "completed", "failed", "cancelled"].includes(phase) || !["requested", "started"].includes(phase) && !prior?.started || prior?.terminal && ["requested", "started"].includes(phase))
173
+ throw new AppError("runtime_scope_journal_invalid", "Release operation has invalid dispatch history", 1);
174
+ if (prior && (prior.operation !== event.data.operation || prior.execution !== (event.executionid ?? null)))
175
+ throw new AppError("runtime_scope_journal_invalid", "Release operation changed its ownership", 1);
176
+ const terminal = ["completed", "failed", "cancelled"].includes(phase) ? phase : null;
177
+ const terminalHash = terminal ? hash(JSON.stringify({ terminal, result: terminal === "completed" ? event.data.result : { error: event.data.error ?? null } })) : null;
178
+ if (terminal && prior?.terminal && (prior.terminal !== terminal || prior.terminal_hash !== terminalHash))
179
+ throw new AppError("runtime_scope_journal_invalid", "Release operation has conflicting terminal outcomes", 1);
180
+ operations.set(id, { operation: event.data.operation, execution: event.executionid ?? null, started: prior?.started === true || phase === "started", terminal: terminal ?? prior?.terminal ?? null, terminal_hash: terminalHash ?? prior?.terminal_hash ?? null });
181
+ }
182
+ const probes = intent.probes.map(raw => {
183
+ const probe = raw;
184
+ const record = db.get("SELECT * FROM managed_processes WHERE id = ?", [probe?.process_id ?? ""]);
185
+ if (record?.kind === "eval-observer") {
186
+ if (scopeLocalProcessRole(record) !== "observer" || record.owner_id !== intent.scope_id || probe.physical_settled !== true || processTreeIsAlive(record))
187
+ throw new AppError("scope_operation_reconciliation_required", "EVAL observer requires its retired physical owner", 1);
188
+ // This proves only local writer retirement. The operation loop below
189
+ // still requires every interrupted experiment operation to reconcile.
190
+ return { process_id: record.id, operation_id: record.operation_id, role: "observer", outcome: "retired", reconciliation: "captured_journal" };
191
+ }
192
+ const execution = manifest.execution_ids.find(id => record?.operation_id?.startsWith(`${intent.scope_id}:${id}:launch:`));
193
+ const operationId = execution ? `${intent.scope_id}:${execution}:launch` : "", checkId = record?.operation_id?.slice(operationId.length + 1);
194
+ const parent = operations.get(operationId);
195
+ const references = events.filter((event) => event.type === "dev.dd.eval.execution.context_prepared" && event.executionid === execution).map((event) => event.data.baseline_admission);
196
+ const reference = references[0], file = execution ? path.join(path.dirname(manifest.manifest_path), "executions", execution, "baseline-admission", "receipt.json") : "";
197
+ if (!record || record.kind !== "eval-baseline" || record.owner_id !== intent.scope_id || JSON.parse(record.metadata_json).role !== "probe" || probe.physical_settled !== true || processTreeIsAlive(record) || !execution || !checkId || !/^[a-z0-9-]+$/.test(checkId) || !parent?.started || parent.operation !== `execution.${execution}.launch` || parent.execution !== execution || !reference || reference.file !== file || !/^[a-f0-9]{64}$/.test(reference.sha256 ?? "") || references.some((item) => JSON.stringify(item) !== JSON.stringify(reference)))
198
+ throw new AppError("probe_reconciliation_required", "Local probe has no retained baseline admission outcome", 1);
199
+ const bytes = fs.readFileSync(file), receipt = JSON.parse(bytes.toString("utf8"));
200
+ const source = JSON.parse(artifact.manifest_bytes);
201
+ if (hash(bytes) !== reference.sha256 || receipt.schema_id !== "dd-eval/baseline-admission@1" || receipt.status !== "passed" || receipt.checkpoint_id !== source.input_checkpoint?.id || receipt.checkpoint_sha256 !== source.input_checkpoint?.sha256 || !receipt.checkpoint_id || !receipt.checkpoint_sha256 || receipt.policy_sha256 !== reference.policy_sha256 || !/^[a-f0-9]{64}$/.test(receipt.policy_sha256 ?? "") || !Array.isArray(receipt.checks) || receipt.checks.filter((check) => check.id === checkId).length !== 1 || receipt.checks.some((check) => check.exit_code !== 0 || check.timed_out !== false))
202
+ throw new AppError("probe_reconciliation_required", "Local probe baseline evidence is incomplete or changed", 1);
203
+ return { process_id: record.id, operation_id: record.operation_id, parent_operation_id: operationId, outcome: "completed", receipt_file: file, receipt_sha256: reference.sha256 };
204
+ });
205
+ for (const [id, operation] of operations) {
206
+ if (!operation.started || operation.terminal === "completed" || operation.terminal === "cancelled")
207
+ continue;
208
+ if (operation.terminal || !operation.execution || !manifest.execution_ids.includes(operation.execution) || operation.operation !== `execution.${operation.execution}.launch` || id !== `${intent.scope_id}:${operation.execution}:launch`)
209
+ throw new AppError("scope_operation_reconciliation_required", "Release has an unresolved EVAL operation", 1);
210
+ const retained = JSON.parse(fs.readFileSync(path.join(path.dirname(manifest.manifest_path), "executions", operation.execution, "managed-runtime.json"), "utf8"));
211
+ if (retained.schema_id !== "dd-eval/managed-runtime@1" || !runs.some(run => run.runtime_home === retained.runtime_root && run.run_id === retained.run_id))
212
+ throw new AppError("scope_operation_reconciliation_required", "Unresolved execution has no prepared retained RUN", 1);
213
+ }
214
+ const current = runtimeBudgetStatus(context, intent.scope_id);
215
+ if (current.fence || current.control?.generation !== intent.generation || current.provider_turns.length)
216
+ throw new AppError("runtime_scope_inventory_changed", "Release lost its scope generation or provider settlement", 1);
217
+ const bindings = [...intent.process_bindings];
218
+ for (const run of runs)
219
+ bindings.push(...run.process_bindings);
220
+ for (const owner of native)
221
+ bindings.push(owner.replacement_binding);
222
+ for (const proof of [...runs.map(run => run.native), ...native]) {
223
+ if (nativeInventoryVersion(proof.state_dir) !== proof.inventory_version)
224
+ throw new AppError("runtime_scope_inventory_changed", "Native preparation changed before release", 1);
225
+ const state = readNativeDaemonState(proof.state_dir);
226
+ if (state.provider_process) {
227
+ const daemon = db.get("SELECT * FROM managed_processes WHERE id = ?", [state.resource_process?.id ?? ""]);
228
+ const provider = db.get("SELECT * FROM managed_processes WHERE id = ?", [state.provider_process.id ?? ""]);
229
+ const daemonMetadata = daemon ? JSON.parse(daemon.metadata_json) : null, providerMetadata = provider ? JSON.parse(provider.metadata_json) : null;
230
+ if (!daemon || !provider || !bindings.some(binding => JSON.stringify(binding) === JSON.stringify(managedProcessBinding(daemon, resourceHome(context))))
231
+ || daemon.pid !== state.pid || daemon.lease_token !== state.resource_process?.lease_token
232
+ || provider.kind !== daemon.kind.replace(/-daemon$/, "-provider") || provider.pid !== state.provider_pid || !provider.pid || !provider.pid_started_at || provider.lease_token !== state.provider_process.lease_token
233
+ || provider.owner_id !== daemon.owner_id || provider.project_id !== daemon.project_id || provider.run_id !== daemon.run_id || provider.work_id !== daemon.work_id
234
+ || providerMetadata.owner_pid !== daemon.pid || providerMetadata.owner_pid_started_at !== daemon.pid_started_at
235
+ || providerMetadata.state_dir !== proof.state_dir || providerMetadata.dd_flow_home !== daemonMetadata.dd_flow_home || providerMetadata.role !== daemonMetadata.role || JSON.stringify(providerMetadata.budget) !== JSON.stringify(daemonMetadata.budget))
236
+ throw new AppError("runtime_scope_owner_unknown", "Native provider has no exact prepared daemon ownership", 1);
237
+ bindings.push(managedProcessBinding(provider, resourceHome(context)));
238
+ }
239
+ if (nativeInventoryVersion(proof.state_dir) !== proof.inventory_version)
240
+ throw new AppError("runtime_scope_inventory_changed", "Native preparation changed while checking its provider", 1);
241
+ }
242
+ const actual = current.processes.map(record => managedProcessBinding(record, resourceHome(context)));
243
+ if (actual.some(binding => !bindings.some(expected => JSON.stringify(binding) === JSON.stringify(expected))) || bindings.some(expected => !actual.some(binding => JSON.stringify(binding) === JSON.stringify(expected))))
244
+ throw new AppError("runtime_scope_inventory_changed", "Release has an unverified process registration", 1);
245
+ const key = hash(JSON.stringify([intent.scope_id, intent.generation])), now = context.now();
246
+ const receipt = { scope_id: intent.scope_id, generation: intent.generation, request_id: intent.request_id, source_request_id: intent.source_request_id, capture_key: intent.capture_key, journal_sha256: hash(artifact.journal_bytes), run_operations: runs.map(run => run.run_operation), native, probes, process_bindings: actual, released_at: now };
247
+ const resumeRow = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-resume' AND resource_key = ?", [intent.scope_id]);
248
+ return () => {
249
+ const latest = runtimeBudgetStatus(context, intent.scope_id);
250
+ const latestBindings = latest.processes.map(record => managedProcessBinding(record, resourceHome(context)));
251
+ if (latest.fence || latest.control?.generation !== intent.generation || latest.provider_turns.length
252
+ || !isDeepStrictEqual(latestBindings, actual)
253
+ || db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control-capture' AND resource_key = ?", [intent.capture_key])?.metadata_json !== artifactRow.metadata_json
254
+ || db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-resume' AND resource_key = ?", [intent.scope_id])?.metadata_json !== resumeRow?.metadata_json)
255
+ throw new AppError("runtime_scope_inventory_changed", "Scope changed while release evidence was prepared", 1);
256
+ db.run("INSERT INTO managed_resources (resource_kind,resource_key,owner_id,lease_token,lease_expires_at,process_id,metadata_json,created_at,updated_at) VALUES ('scope-resume-release',?,?,?,?,NULL,?,?,?)", [key, intent.scope_id, key, now, JSON.stringify(receipt), now, now]);
257
+ if (db.run("DELETE FROM managed_resources WHERE resource_kind = 'scope-control' AND resource_key = ? AND json_extract(metadata_json, '$.generation') = ?", [intent.scope_id, intent.generation]).changes !== 1)
258
+ throw new AppError("runtime_scope_control_superseded", "Release lost its control generation", 1);
259
+ db.run("UPDATE managed_resources SET metadata_json = json_set(metadata_json, '$.status', 'released'), updated_at = ? WHERE resource_kind = 'scope-resume' AND resource_key = ?", [now, intent.scope_id]);
260
+ return receipt;
261
+ };
262
+ }
263
+ /** A preparation snapshot, not an unfence. The release arbiter must repeat
264
+ * these reads while holding every participating RUN writer boundary. */
265
+ function preparedScopeRun(context, input, assertCurrent) {
266
+ const registry = getResourceDatabase(resourceHome(context));
267
+ const checks = [];
268
+ const read = (sql, params) => {
269
+ const value = context.db.get(sql, params);
270
+ checks.push(() => { if (!isDeepStrictEqual(context.db.get(sql, params), value))
271
+ throw new AppError("runtime_scope_inventory_changed", "RUN preparation changed before release", 1); });
272
+ return value;
273
+ };
274
+ const finish = (proof) => ({ proof, assertUnchanged: () => { assertCurrent(); for (const check of checks)
275
+ check(); } });
276
+ return registry.readSnapshot(() => context.db.readSnapshot(() => {
277
+ assertCurrent();
278
+ const controller = read("SELECT * FROM run_controllers WHERE project_id = ? AND run_id = ? AND status NOT IN ('completed','cancelled','superseded')", [input.projectId, input.runId]);
279
+ if (!controller)
280
+ throw new AppError("runtime_scope_owner_unknown", "Prepared RUN lost its controller", 1);
281
+ const state = JSON.parse(controller.state_json), manifest = JSON.parse(controller.manifest_json);
282
+ const operation = read(`SELECT o.operation_id, o.receipt_json FROM run_controller_operations o
283
+ JOIN run_controls ctl ON ctl.control_id = ? AND ctl.project_id = ? AND ctl.run_id = ?
284
+ JOIN run_recovery_guards g ON g.recovery_id = ctl.recovery_id
285
+ WHERE o.controller_id = ? AND o.generation = ? AND o.status = 'accepted'
286
+ AND ctl.status = 'resuming' AND ctl.recovery_id = ? AND g.generation = o.generation AND g.status = 'resuming'
287
+ AND json_extract(o.input_json, '$.action') = 'control_resume' AND json_extract(o.input_json, '$.request_id') = ?`, [input.controlId, input.projectId, input.runId, controller.controller_id, controller.generation, input.recoveryId, input.requestId]);
288
+ const prepared = operation?.receipt_json ? JSON.parse(operation.receipt_json) : null;
289
+ if (prepared?.prepared !== true)
290
+ return finish({ prepared: false, pending: true, reason: "run_recovery_preparation_required" });
291
+ const session = state.current_session === null ? null : state.sessions[state.current_session];
292
+ const owner = registry.get("SELECT * FROM managed_processes WHERE id = ?", [controller.process_id]);
293
+ if (!session || manifest.budget?.scope_id !== input.scopeId || manifest.resource_home !== resourceHome(context) || state.recovery?.operation_id !== operation.operation_id || state.recovery?.control_id !== input.controlId || state.recovery?.recovery_id !== input.recoveryId || prepared.recovery_id !== input.recoveryId || prepared.session_id !== session.id || !owner || owner.lease_token !== controller.process_lease_token || owner.pid !== controller.owner_pid || owner.owner_id !== controller.controller_id || !processIsAlive(owner))
294
+ throw new AppError("runtime_scope_owner_unknown", "RUN preparation lost its current owner or Session", 1);
295
+ const native = requireManagedDaemonBinding(context, { stateDir: session.state_dir, projectId: input.projectId, runId: input.runId, ownerId: session.owner.owner_id });
296
+ const loaded = read("SELECT receipt_json FROM run_controller_operations WHERE operation_id = ? AND controller_id = ? AND generation = ? AND status = 'completed'", [prepared.resume_operation_id, controller.controller_id, controller.generation]);
297
+ const loadReceipt = loaded?.receipt_json ? JSON.parse(loaded.receipt_json) : null;
298
+ if (!processIsAlive(native.process) || native.process.id !== prepared.process_id || native.state.daemon_id !== prepared.daemon_id || loadReceipt?.settled !== true || adapterSessionId(loadReceipt) !== session.id)
299
+ throw new AppError("runtime_scope_owner_unknown", "RUN preparation has no current native load acknowledgement", 1);
300
+ for (const record of [owner, native.process]) {
301
+ const expected = managedProcessBinding(record, resourceHome(context));
302
+ checks.push(() => {
303
+ const latest = registry.get("SELECT * FROM managed_processes WHERE id = ?", [record.id]);
304
+ if (!latest || !isDeepStrictEqual(managedProcessBinding(latest, resourceHome(context)), expected))
305
+ throw new AppError("runtime_scope_owner_unknown", "Prepared RUN lost its process binding", 1);
306
+ });
307
+ }
308
+ return finish({ prepared: true, run_operation: { runtime_home: context.ddFlowHome, controller_id: controller.controller_id, operation_id: operation.operation_id }, process_bindings: [managedProcessBinding(owner, resourceHome(context)), managedProcessBinding(native.process, resourceHome(context))], native: { state_dir: session.state_dir, daemon_id: prepared.daemon_id, session_id: session.id, inventory_version: nativeInventoryVersion(session.state_dir) } });
309
+ }));
310
+ }
311
+ async function restartScopeNative(context, intent, source, assertCurrent) {
312
+ const db = getResourceDatabase(resourceHome(context)), key = hash(JSON.stringify([intent.scope_id, intent.generation, source.process_id]));
313
+ const saved = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-resume-native' AND resource_key = ?", [key]);
314
+ let operation = JSON.parse(saved.metadata_json);
315
+ const startup = source.operations.find(item => item.operation === "daemon.start" && item.result?.daemon_id === source.daemon_id);
316
+ const launch = startup.launch;
317
+ const state = () => JSON.parse(fs.readFileSync(path.join(source.state_dir, "daemon.json"), "utf8"));
318
+ const assertRetired = (physical = true) => {
319
+ assertCurrent();
320
+ const old = db.get("SELECT * FROM managed_processes WHERE id = ?", [source.process_id]);
321
+ if (!old || physical && processTreeIsAlive(old) || JSON.stringify(managedProcessBinding(old, resourceHome(context))) !== JSON.stringify(intent.process_bindings.find(item => item.process_id === source.process_id)))
322
+ throw new AppError("runtime_scope_owner_unknown", "The retired native tree changed or is still alive", 1);
323
+ };
324
+ assertRetired();
325
+ const predecessor = operation.status === "retired" ? state() : null;
326
+ if (predecessor && (predecessor.daemon_id !== source.daemon_id || predecessor.shutdown_state !== "clean"))
327
+ throw new AppError("runtime_scope_owner_unknown", "Native restart lost its clean predecessor", 1);
328
+ let dispatch = false;
329
+ db.writeTransaction(() => {
330
+ assertRetired(false);
331
+ operation = JSON.parse(db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-resume-native' AND resource_key = ?", [key]).metadata_json);
332
+ if (operation.status === "retired") {
333
+ if (!predecessor)
334
+ throw new AppError("runtime_scope_inventory_changed", "Native retirement changed during restart preparation", 1);
335
+ operation.status = "starting";
336
+ operation.restart_operation_id = `scope-restart:${key}`;
337
+ dispatch = true;
338
+ 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]);
339
+ }
340
+ });
341
+ if (!operation.restart_receipt) {
342
+ if (dispatch) {
343
+ const env = nativeLaunchEnvironment(context, launch.env);
344
+ assertRetired();
345
+ operation.restart_receipt = await runHarnessAdapter({ executable: launch.executable, args: launch.args, cwd: launch.cwd, env, runtimeOwner: source.runtime_owner, operationId: operation.restart_operation_id, timeoutMs: 30_000 });
346
+ }
347
+ else {
348
+ const retained = retainedDaemonOperations(source.state_dir).find(item => item.operation_id === operation.restart_operation_id && item.operation === "daemon.start" && item.predecessor_daemon_id === source.daemon_id && item.state === "completed");
349
+ if (!retained)
350
+ return { ...source, ...operation, pending: true, reason: "native_restart_outcome_unknown" };
351
+ operation.restart_receipt = retained.result;
352
+ }
353
+ }
354
+ assertRetired();
355
+ const daemon = state(), replacement = db.get("SELECT * FROM managed_processes WHERE id = ?", [daemon.resource_process?.id ?? ""]);
356
+ const metadata = replacement ? JSON.parse(replacement.metadata_json) : null;
357
+ if (!replacement || !replacement.kind.endsWith("-daemon") || replacement.id === source.process_id || !processIsAlive(replacement) || replacement.pid !== daemon.pid || replacement.lease_token !== daemon.resource_process?.lease_token || replacement.owner_id !== source.runtime_owner.owner_id || replacement.run_id || replacement.work_id || replacement.project_id !== (source.runtime_owner.project_id ?? null) || metadata?.role !== source.role || metadata?.state_dir !== source.state_dir || metadata?.dd_flow_home !== source.runtime_owner.dd_flow_home || metadata?.adapter_executable !== launch.executable || JSON.stringify(metadata?.budget) !== JSON.stringify(source.runtime_owner.budget) || daemon.daemon_id === source.daemon_id || daemon.daemon_id !== operation.restart_receipt.daemon_id || operation.restart_receipt.already_running === true || JSON.stringify(daemon.config?.runtime_owner) !== JSON.stringify(source.runtime_owner))
358
+ throw new AppError("runtime_scope_owner_unknown", "Native restart has no fresh bound physical owner", 1);
359
+ const binding = managedProcessBinding(replacement, resourceHome(context));
360
+ if (operation.replacement_binding && JSON.stringify(operation.replacement_binding) !== JSON.stringify(binding))
361
+ throw new AppError("runtime_scope_owner_unknown", "Native replacement registration changed", 1);
362
+ operation.status = "restarted";
363
+ operation.replacement_binding = binding;
364
+ 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]);
365
+ const assertReplacement = (physical = true) => {
366
+ assertRetired(physical);
367
+ const current = db.get("SELECT * FROM managed_processes WHERE id = ?", [replacement.id]), latest = physical ? state() : null;
368
+ if (!current || JSON.stringify(managedProcessBinding(current, resourceHome(context))) !== JSON.stringify(binding)
369
+ || physical && (!processIsAlive(current) || latest.daemon_id !== daemon.daemon_id || latest.resource_process?.id !== current.id || latest.resource_process?.lease_token !== current.lease_token || JSON.stringify(latest.config?.runtime_owner) !== JSON.stringify(source.runtime_owner)))
370
+ throw new AppError("runtime_scope_owner_unknown", "Session loading lost its native replacement", 1);
371
+ };
372
+ if (!Array.isArray(source.sessions) || !source.sessions.length || source.sessions.some(item => typeof item.session_id !== "string" || !item.session_id) || new Set(source.sessions.map(item => item.session_id)).size !== source.sessions.length)
373
+ throw new AppError("runtime_scope_owner_unknown", "Native recovery has no exact Session inventory", 1);
374
+ const sessions = [];
375
+ for (const session of source.sessions) {
376
+ const operationId = `scope-load:${hash(JSON.stringify([intent.scope_id, intent.generation, source.process_id, session.session_id]))}`;
377
+ let dispatchLoad = false;
378
+ assertReplacement();
379
+ const load = db.writeTransaction(() => {
380
+ let load;
381
+ assertReplacement(false);
382
+ const row = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-resume-native-load' AND resource_key = ?", [operationId]);
383
+ if (row)
384
+ load = JSON.parse(row.metadata_json);
385
+ else {
386
+ load = { scope_id: intent.scope_id, generation: intent.generation, request_id: intent.request_id, source_process_id: source.process_id, process_id: replacement.id, daemon_id: daemon.daemon_id, session_id: session.session_id, status: "dispatching" };
387
+ const now = context.now();
388
+ db.run("INSERT INTO managed_resources (resource_kind,resource_key,owner_id,lease_token,lease_expires_at,process_id,metadata_json,created_at,updated_at) VALUES ('scope-resume-native-load',?,?,?,?,NULL,?,?,?)", [operationId, intent.scope_id, operationId, now, JSON.stringify(load), now, now]);
389
+ dispatchLoad = true;
390
+ }
391
+ if (load.scope_id !== intent.scope_id || load.generation !== intent.generation || load.request_id !== intent.request_id || load.source_process_id !== source.process_id || load.process_id !== replacement.id || load.daemon_id !== daemon.daemon_id || load.session_id !== session.session_id)
392
+ throw new AppError("runtime_scope_owner_unknown", "Retained Session load belongs to another physical owner", 1);
393
+ return load;
394
+ });
395
+ if (!load.receipt) {
396
+ if (dispatchLoad) {
397
+ assertReplacement();
398
+ const args = launch.args.slice(2).filter((value, index, all) => value !== "--session-id" && all[index - 1] !== "--session-id");
399
+ load.receipt = await runHarnessAdapter({ executable: launch.executable, args: ["session", "resume", ...args, "--session-id", session.session_id, "--json"], cwd: launch.cwd, env: nativeLaunchEnvironment(context, launch.env), runtimeOwner: source.runtime_owner, operationId, timeoutMs: 30_000 });
400
+ }
401
+ else {
402
+ const retained = retainedDaemonOperations(source.state_dir).find(item => item.operation_id === operationId && item.operation === "session.resume" && item.daemon_id === daemon.daemon_id && item.session_id === session.session_id && item.state === "completed");
403
+ if (!retained)
404
+ return { ...source, ...operation, sessions, pending: true, reason: "native_session_load_outcome_unknown" };
405
+ load.receipt = retained.result;
406
+ }
407
+ }
408
+ assertReplacement();
409
+ if (adapterSessionId(load.receipt) !== session.session_id || load.receipt.settled !== true)
410
+ throw new AppError("runtime_scope_session_unsettled", "Native load did not confirm the same settled Session", 1);
411
+ load.status = "prepared";
412
+ db.run("UPDATE managed_resources SET metadata_json = ?, updated_at = ? WHERE resource_kind = 'scope-resume-native-load' AND resource_key = ?", [JSON.stringify(load), context.now(), operationId]);
413
+ sessions.push({ session_id: session.session_id, operation_id: operationId, receipt: load.receipt, prepared: true });
414
+ }
415
+ return { ...source, ...operation, inventory_version: nativeInventoryVersion(source.state_dir), sessions, prepared: true, pending: true, reason: "scope_release_required" };
416
+ }
417
+ async function retireScopeNative(context, intent, source, assertCurrent) {
418
+ const db = getResourceDatabase(resourceHome(context)), key = hash(JSON.stringify([intent.scope_id, intent.generation, source.process_id]));
419
+ const record = db.get("SELECT * FROM managed_processes WHERE id = ?", [source.process_id]);
420
+ const frozen = intent.process_bindings.find((binding) => binding.process_id === source.process_id);
421
+ const state = () => JSON.parse(fs.readFileSync(path.join(source.state_dir, "daemon.json"), "utf8"));
422
+ const assertOwner = (physical = true) => {
423
+ assertCurrent();
424
+ const current = db.get("SELECT * FROM managed_processes WHERE id = ?", [source.process_id]), daemon = physical ? state() : null;
425
+ if (!current || current.run_id || !["judge", "probe"].includes(source.role) || JSON.stringify(managedProcessBinding(current, resourceHome(context))) !== JSON.stringify(frozen)
426
+ || physical && (daemon.daemon_id !== source.daemon_id || daemon.resource_process?.id !== current.id || daemon.resource_process?.lease_token !== current.lease_token || JSON.stringify(daemon.config?.runtime_owner) !== JSON.stringify(source.runtime_owner)))
427
+ throw new AppError("runtime_scope_owner_unknown", "Native retirement lost its frozen physical owner", 1);
428
+ };
429
+ assertOwner();
430
+ const startup = source.operations?.filter(operation => operation.operation === "daemon.start" && operation.state === "completed" && operation.result?.daemon_id === source.daemon_id);
431
+ const launch = (startup?.length === 1 ? startup[0].launch : null);
432
+ let launchOwner;
433
+ try {
434
+ launchOwner = JSON.parse(launch?.runtime_owner ?? "null");
435
+ }
436
+ catch {
437
+ launchOwner = null;
438
+ }
439
+ 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))
440
+ throw new AppError("runtime_scope_native_launch_required", "Native recovery requires the original owned launch contract", 1);
441
+ 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"))
442
+ throw new AppError("runtime_scope_native_launch_required", "Native recovery requires explicit configuration presence before retiring its owner", 1);
443
+ let dispatch = false;
444
+ const preparedInventory = db.get("SELECT 1 FROM managed_resources WHERE resource_kind = 'scope-resume-native' AND resource_key = ?", [key]) ? null : nativeInventoryVersion(source.state_dir);
445
+ const operation = db.writeTransaction(() => {
446
+ let operation;
447
+ assertOwner(false);
448
+ const saved = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-resume-native' AND resource_key = ?", [key]);
449
+ if (saved)
450
+ operation = JSON.parse(saved.metadata_json);
451
+ else {
452
+ if (preparedInventory !== source.inventory_version)
453
+ throw new AppError("runtime_scope_inventory_changed", "Native owner changed before retirement", 1);
454
+ operation = { scope_id: intent.scope_id, generation: intent.generation, request_id: intent.request_id, process_id: source.process_id, daemon_id: source.daemon_id, operation_id: `scope-retire:${key}`, status: "retiring" };
455
+ const now = context.now();
456
+ db.run("INSERT INTO managed_resources (resource_kind,resource_key,owner_id,lease_token,lease_expires_at,process_id,metadata_json,created_at,updated_at) VALUES ('scope-resume-native',?,?,?,?,NULL,?,?,?)", [key, intent.scope_id, key, now, JSON.stringify(operation), now, now]);
457
+ dispatch = true;
458
+ }
459
+ return operation;
460
+ });
461
+ if (!operation.receipt) {
462
+ if (dispatch) {
463
+ assertOwner();
464
+ operation.receipt = await runHarnessAdapter({ executable: launch.executable, args: ["daemon", "stop", "--state-dir", source.state_dir, "--json"], env: { ...context.env, DD_FLOW_HOME: source.runtime_owner.dd_flow_home, DD_FLOW_RESOURCE_HOME: resourceHome(context) }, operationId: operation.operation_id, timeoutMs: 30_000 });
465
+ }
466
+ else {
467
+ const retained = retainedDaemonOperations(source.state_dir).find(item => item.operation_id === operation.operation_id && item.operation === "daemon.stop" && item.daemon_id === source.daemon_id && item.state === "completed");
468
+ if (!retained)
469
+ return { ...source, ...operation, pending: true, reason: "native_retirement_outcome_unknown" };
470
+ operation.receipt = retained.result;
471
+ }
472
+ assertOwner();
473
+ 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]);
474
+ }
475
+ if (operation.receipt.stopped !== true || operation.receipt.clean !== true || state().shutdown_state !== "clean" || processTreeIsAlive(record))
476
+ return { ...source, ...operation, pending: true, reason: "native_retirement_settlement_required" };
477
+ assertOwner();
478
+ operation.status = "retired";
479
+ 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]);
480
+ return { ...source, ...operation, pending: true, reason: "native_restart_required" };
481
+ }
482
+ /** Freeze the preparation plan under the same arbiter as stop. This accepts
483
+ * ownership work, not productive dispatch; only all-role release may unfence. */
484
+ export function prepareRuntimeScopeResumeInput(input) {
485
+ prepareRuntimeScopeFence(input.scopeId, input.requestId);
486
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(input.requestId) || !Number.isSafeInteger(input.generation) || input.generation < 1 || typeof input.captureKey !== "string" || !/^[a-f0-9]{64}$/.test(input.captureKey))
487
+ throw new AppError("runtime_scope_resume_invalid", "Scope resume requires its exact generation, capture and request identity", 2);
488
+ }
489
+ export function prepareRuntimeScopeResumeRequest(context, input) {
490
+ prepareRuntimeScopeResumeInput(input);
491
+ const db = getResourceDatabase(resourceHome(context), "read_existing"), key = hash(JSON.stringify([input.scopeId, input.requestId]));
492
+ try {
493
+ const latest = runtimeBudgetStatus(context, input.scopeId);
494
+ const control = latest.control;
495
+ const prior = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-resume-request' AND resource_key = ?", [key]);
496
+ if (prior) {
497
+ const receipt = JSON.parse(prior.metadata_json);
498
+ if (receipt.generation !== input.generation || receipt.capture_key !== input.captureKey)
499
+ throw new AppError("runtime_scope_resume_conflict", "Resume request identity was used for another source", 1);
500
+ const active = latest.resume;
501
+ const sameRequest = active?.generation === input.generation && active.request_id === input.requestId;
502
+ return { ...receipt, ...(sameRequest ? active : {}), ...(sameRequest && active.status === "released" ? { release: latest.release } : {}), current: sameRequest && active.current === true, reused: true };
503
+ }
504
+ if (latest.fence || control?.generation !== input.generation)
505
+ throw new AppError("runtime_scope_control_superseded", "Resume source is not the current recoverable scope control", 1);
506
+ const existing = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-resume' AND resource_key = ?", [input.scopeId]);
507
+ if (existing && JSON.parse(existing.metadata_json).generation === input.generation)
508
+ throw new AppError("runtime_scope_resume_conflict", "This scope generation already has a resume owner", 1);
509
+ const drainRow = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control-drain' AND resource_key = ?", [hash(JSON.stringify([input.scopeId, input.generation]))]);
510
+ const drain = drainRow ? JSON.parse(drainRow.metadata_json) : null;
511
+ if (drain?.scope_id !== input.scopeId || drain.generation !== input.generation || drain.requested_mode !== control.requested_mode || drain.physical_settled !== true || drain.capture?.journal?.artifact_key !== input.captureKey || drain.capture.journal.trailing_bytes !== 0 || !Array.isArray(drain.capture.run_captures) || drain.capture.run_captures.some((run) => run.status !== "verified"))
512
+ throw new AppError("runtime_scope_capture_required", "Resume requires a complete all-role drain and verified RUN captures", 1);
513
+ const manifest = validateRetainedRuntimeScopeManifest(drain.capture.manifest, input.scopeId);
514
+ if (JSON.stringify(manifest) !== JSON.stringify(latest.manifest))
515
+ throw new AppError("runtime_scope_manifest_changed", "Resume scope manifest changed after capture", 1);
516
+ const bindings = latest.processes.map(record => managedProcessBinding(record, resourceHome(context)));
517
+ const ordered = (items) => JSON.stringify([...items].sort((a, b) => a.process_id.localeCompare(b.process_id)));
518
+ if (!Array.isArray(drain.process_bindings) || ordered(bindings) !== ordered(drain.process_bindings) || latest.provider_turns.length)
519
+ throw new AppError("runtime_scope_inventory_changed", "Owned process or provider inventory changed after capture", 1);
520
+ for (const owner of drain.native ?? []) {
521
+ if (owner.historical === true) {
522
+ assertHistoricalDaemonSettlement(context, input.scopeId, owner);
523
+ continue;
524
+ }
525
+ if (typeof owner.state_dir !== "string" || owner.native_settled !== true || nativeInventoryVersion(owner.state_dir) !== owner.inventory_version)
526
+ throw new AppError("runtime_scope_inventory_changed", "Native owner changed after capture", 1);
527
+ }
528
+ const artifactRow = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control-capture' AND resource_key = ?", [input.captureKey]);
529
+ const artifact = artifactRow ? JSON.parse(artifactRow.metadata_json) : null;
530
+ if (artifact?.scope_id !== input.scopeId || artifact.generation !== input.generation || typeof artifact.manifest_bytes !== "string" || typeof artifact.journal_bytes !== "string" || artifact.journal_source !== path.join(path.dirname(manifest.manifest_path), "events.jsonl") || hash(artifact.manifest_bytes) !== manifest.manifest_sha256 || hash(JSON.stringify([input.scopeId, input.generation, manifest.manifest_sha256, hash(artifact.journal_bytes)])) !== input.captureKey || !fs.readFileSync(artifact.journal_source).equals(Buffer.from(artifact.journal_bytes)))
531
+ throw new AppError("runtime_scope_journal_changed", "Resume requires the current immutable EVAL journal capture", 1);
532
+ const runs = drain.capture.run_captures.map((run) => ({ ...run, resume_request_id: `scope-resume:${hash(JSON.stringify([input.scopeId, input.generation, input.requestId, run.runtime_home, run.project_id, run.run_id]))}` }));
533
+ const receipt = { scope_id: input.scopeId, generation: input.generation, request_id: input.requestId, source_request_id: control.request_id, capture_key: input.captureKey, status: "preparing", dispatch_blocked: true, manifest, process_bindings: bindings, runs, native: drain.capture.native_bindings, probes: drain.capture.probe_operations, requested_at: context.now() };
534
+ return { ...receipt, current: true, reused: false };
535
+ }
536
+ finally {
537
+ db.close?.();
538
+ }
539
+ }
540
+ export function recordRuntimeScopeResume(context, input) {
541
+ prepareRuntimeScopeResumeInput(input);
542
+ let db = getResourceDatabase(resourceHome(context), "read_existing");
543
+ const key = hash(JSON.stringify([input.scopeId, input.requestId]));
544
+ const authority = () => {
545
+ const status = runtimeBudgetStatus(context, input.scopeId);
546
+ return {
547
+ records: db.all("SELECT resource_kind, resource_key, metadata_json FROM managed_resources WHERE (resource_kind = 'scope-control' AND resource_key = ?) OR (resource_kind = 'scope-resume' AND resource_key = ?) OR (resource_kind = 'scope-control-drain' AND resource_key = ?) OR (resource_kind = 'scope-control-capture' AND resource_key = ?) ORDER BY resource_kind, resource_key", [input.scopeId, input.scopeId, hash(JSON.stringify([input.scopeId, input.generation])), input.captureKey]),
548
+ fence: status.fence, manifest: status.manifest, turns: status.provider_turns,
549
+ processes: status.processes.map(record => managedProcessBinding(record, resourceHome(context)))
550
+ };
551
+ };
552
+ let before, prepared;
553
+ try {
554
+ before = authority();
555
+ prepared = prepareRuntimeScopeResumeRequest(context, input);
556
+ }
557
+ finally {
558
+ db.close?.();
559
+ }
560
+ if (prepared.reused)
561
+ return prepared;
562
+ db = getResourceDatabase(resourceHome(context));
563
+ return db.writeTransaction(() => {
564
+ // This branch is SQL-only: an existing request bypasses evidence reads.
565
+ if (db.get("SELECT 1 FROM managed_resources WHERE resource_kind = 'scope-resume-request' AND resource_key = ?", [key]))
566
+ return prepareRuntimeScopeResumeRequest(context, input);
567
+ if (!isDeepStrictEqual(authority(), before))
568
+ throw new AppError("runtime_scope_inventory_changed", "Resume authority changed while evidence was prepared", 1);
569
+ const receipt = { ...prepared };
570
+ delete receipt.current;
571
+ delete receipt.reused;
572
+ const payload = JSON.stringify(receipt), now = context.now();
573
+ db.run("INSERT INTO managed_resources (resource_kind,resource_key,owner_id,lease_token,lease_expires_at,process_id,metadata_json,created_at,updated_at) VALUES ('scope-resume-request',?,?,?,?,NULL,?,?,?)", [key, input.scopeId, key, now, payload, now, now]);
574
+ db.run("INSERT INTO managed_resources (resource_kind,resource_key,owner_id,lease_token,lease_expires_at,process_id,metadata_json,created_at,updated_at) VALUES ('scope-resume',?,?,?,?,NULL,?,?,?) ON CONFLICT(resource_kind,resource_key) DO UPDATE SET lease_token = excluded.lease_token, metadata_json = excluded.metadata_json, updated_at = excluded.updated_at", [input.scopeId, input.scopeId, key, now, payload, now, now]);
575
+ return prepared;
576
+ });
577
+ }