@deksden-com/dd-flow-cli 0.9.0-beta.104 → 0.9.0-beta.105

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 (68) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/build-info.json +3 -3
  3. package/dist/cli/run-cli.js +23 -8
  4. package/dist/harness-runtime/bin/dd-agy.mjs +1 -1
  5. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +2 -2
  6. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +4 -3
  7. package/dist/harness-runtime/lib/delegation-instructions.d.mts +2 -0
  8. package/dist/harness-runtime/lib/delegation-instructions.mjs +8 -1
  9. package/dist/protocol/local-files.js +18 -9
  10. package/dist/runtime/context.js +2 -2
  11. package/dist/services/cleanup.js +81 -32
  12. package/dist/services/code-checks.js +18 -19
  13. package/dist/services/continuation-outcome.js +1 -1
  14. package/dist/services/controller-fanout.js +3 -3
  15. package/dist/services/eval-snapshots.js +69 -41
  16. package/dist/services/execution-policy.js +12 -1
  17. package/dist/services/hooks.js +150 -42
  18. package/dist/services/lanes.js +23 -18
  19. package/dist/services/lifecycle-command.js +11 -2
  20. package/dist/services/lifecycle-invocations.js +291 -54
  21. package/dist/services/managed-processes.js +35 -23
  22. package/dist/services/merge-queue.js +71 -67
  23. package/dist/services/merge-server.js +3 -11
  24. package/dist/services/migrations.js +28 -12
  25. package/dist/services/plan-runtime.js +8 -4
  26. package/dist/services/projects.js +3 -11
  27. package/dist/services/protocols.js +98 -68
  28. package/dist/services/recovery-snapshot-database.js +43 -8
  29. package/dist/services/run-control-worker.js +15 -32
  30. package/dist/services/run-control.js +3 -17
  31. package/dist/services/run-controller-capture.js +58 -27
  32. package/dist/services/run-controller-process.js +21 -30
  33. package/dist/services/run-controller-recovery.js +36 -37
  34. package/dist/services/run-controller.js +81 -86
  35. package/dist/services/run-fork.js +10 -6
  36. package/dist/services/run-projection.js +19 -64
  37. package/dist/services/run-recovery-runtime.js +24 -14
  38. package/dist/services/run-recovery.js +56 -54
  39. package/dist/services/runs.js +218 -160
  40. package/dist/services/runtime-budget.js +19 -45
  41. package/dist/services/runtime-scope-capture.js +8 -6
  42. package/dist/services/runtime-scope-control.js +49 -39
  43. package/dist/services/runtime-scope-resume.js +134 -122
  44. package/dist/services/runtime-scope-worker.js +48 -49
  45. package/dist/services/scope-writer-cleanup.js +26 -0
  46. package/dist/services/stage-context.js +6 -1
  47. package/dist/services/stage-lifecycle.js +1 -1
  48. package/dist/services/stage-pause.js +13 -29
  49. package/dist/services/stage-report-renderer.js +116 -6
  50. package/dist/services/usage.js +2 -9
  51. package/dist/services/vnext-code-review.js +10 -7
  52. package/dist/services/vnext-code.js +9 -17
  53. package/dist/services/vnext-merge.js +64 -21
  54. package/dist/services/vnext-plan-review.js +43 -30
  55. package/dist/services/vnext-plan.js +28 -24
  56. package/dist/services/vnext-protocolize.js +183 -20
  57. package/dist/services/vnext-specify.js +27 -19
  58. package/dist/services/work-registry.js +229 -150
  59. package/dist/storage/database.js +286 -56
  60. package/dist/storage/foreign-key-transaction.js +39 -0
  61. package/dist/storage/immutable-input.js +45 -0
  62. package/dist/storage/import-guard.js +102 -0
  63. package/dist/storage/native-cleanup.js +52 -0
  64. package/dist/storage/projection-lock.js +73 -0
  65. package/dist/storage/writer-contract.js +3 -3
  66. package/dist/storage/writer-migration.js +24 -41
  67. package/package.json +1 -1
  68. package/tools/repair-paused-run-status.mjs +48 -23
@@ -50,20 +50,14 @@ export async function waitForRuntimeScopeRecovery(context, input, assertOwner) {
50
50
  const db = getResourceDatabase(resourceHome(context));
51
51
  for (;;) {
52
52
  assertOwner();
53
- const snapshot = (() => {
54
- db.exec("SAVEPOINT scope_recovery_release");
55
- try {
56
- return {
57
- fence: db.get("SELECT 1 FROM managed_resources WHERE resource_kind = 'provider-fence' AND resource_key = ?", [input.scopeId]),
58
- latest: db.get("SELECT MAX(json_extract(metadata_json, '$.generation')) AS generation FROM managed_resources WHERE resource_kind = 'scope-control-request' AND owner_id = ?", [input.scopeId]),
59
- control: db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control' AND resource_key = ?", [input.scopeId]),
60
- release: db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-resume-release' AND resource_key = ?", [digest(JSON.stringify([input.scopeId, input.generation]))])
61
- };
62
- }
63
- finally {
64
- db.exec("RELEASE scope_recovery_release");
65
- }
66
- })();
53
+ const snapshot = db.readSnapshot(() => {
54
+ return {
55
+ fence: db.get("SELECT 1 FROM managed_resources WHERE resource_kind = 'provider-fence' AND resource_key = ?", [input.scopeId]),
56
+ latest: db.get("SELECT MAX(json_extract(metadata_json, '$.generation')) AS generation FROM managed_resources WHERE resource_kind = 'scope-control-request' AND owner_id = ?", [input.scopeId]),
57
+ control: db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control' AND resource_key = ?", [input.scopeId]),
58
+ release: db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-resume-release' AND resource_key = ?", [digest(JSON.stringify([input.scopeId, input.generation]))])
59
+ };
60
+ });
67
61
  if (snapshot.fence)
68
62
  throw new AppError("runtime_scope_stopped", "Terminal cancellation superseded scope recovery", 1);
69
63
  const { latest, control: row, release } = snapshot;
@@ -118,12 +112,10 @@ export function requestRuntimeScopeControl(context, scopeId, requestId, mode, ma
118
112
  const declaration = prepared.manifest;
119
113
  const db = getResourceDatabase(resourceHome(context));
120
114
  const requestKey = digest(JSON.stringify([scopeId, requestId]));
121
- db.beginWriteTransaction();
122
- try {
115
+ return db.writeTransaction(() => {
123
116
  const prior = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control-request' AND resource_key = ?", [requestKey]);
124
117
  if (prior) {
125
118
  const receipt = scopeControlReceipt(prior.metadata_json, mode);
126
- db.exec("COMMIT");
127
119
  return receipt;
128
120
  }
129
121
  if (declaration !== undefined) {
@@ -149,13 +141,8 @@ export function requestRuntimeScopeControl(context, scopeId, requestId, mode, ma
149
141
  const payload = JSON.stringify(receipt);
150
142
  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-control-request',?,?,?,?,NULL,?,?,?)", [requestKey, scopeId, requestKey, now, payload, now, now]);
151
143
  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-control',?,?,?,?,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", [scopeId, scopeId, requestKey, now, payload, now, now]);
152
- db.exec("COMMIT");
153
144
  return receipt;
154
- }
155
- catch (error) {
156
- db.exec("ROLLBACK");
157
- throw error;
158
- }
145
+ });
159
146
  }
160
147
  function assertScopeAdmission(context, scopeId) {
161
148
  const db = getResourceDatabase(resourceHome(context));
@@ -245,21 +232,15 @@ export function prepareRuntimeScopeFence(scopeId, requestId) {
245
232
  export function fenceRuntimeBudget(context, scopeId, requestId) {
246
233
  prepareRuntimeScopeFence(scopeId, requestId);
247
234
  const db = getResourceDatabase(resourceHome(context));
248
- db.beginWriteTransaction();
249
- try {
235
+ return db.writeTransaction(() => {
250
236
  const existing = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'provider-fence' AND resource_key = ?", [scopeId]);
251
237
  const now = context.now();
252
238
  const receipt = existing ? JSON.parse(existing.metadata_json)
253
239
  : { scope_id: scopeId, request_id: requestId, dispatch_blocked: true, fenced_at: now };
254
240
  if (!existing)
255
241
  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 ('provider-fence',?,?,?,?,NULL,?,?,?)", [scopeId, scopeId, digest(requestId), now, JSON.stringify(receipt), now, now]);
256
- db.exec("COMMIT");
257
242
  return receipt;
258
- }
259
- catch (error) {
260
- db.exec("ROLLBACK");
261
- throw error;
262
- }
243
+ });
263
244
  }
264
245
  export function runtimeBudgetForRun(context, projectId, runId) {
265
246
  const retained = context.db.get("SELECT 1 FROM sqlite_schema WHERE name = 'run_controllers'")
@@ -304,16 +285,16 @@ export function reserveProviderTurn(context, record, input) {
304
285
  const db = getResourceDatabase(resourceHome(context));
305
286
  const key = digest(JSON.stringify([record.id, input.operationId]));
306
287
  const policy = JSON.stringify(budget), now = context.now();
307
- db.beginWriteTransaction();
308
- try {
288
+ const recoveryOwnerAlive = input.operation === "session.resume" && processIsAlive(record);
289
+ return db.writeTransaction(() => {
309
290
  const current = db.get("SELECT * FROM managed_processes WHERE id = ?", [record.id]);
310
- if (current?.lease_token !== record.lease_token || current.state !== "running")
291
+ if (current?.lease_token !== record.lease_token || current.state !== "running" || current.lease_expires_at <= context.now() || managedProcessBinding(current, resourceHome(context)).registration_sha256 !== managedProcessBinding(record, resourceHome(context)).registration_sha256)
311
292
  throw new AppError("process_ownership_unknown", "Budget owner changed before admission", 1);
312
293
  try {
313
294
  assertScopeAdmission(context, budget.scope_id);
314
295
  }
315
296
  catch (error) {
316
- if (!(error instanceof AppError) || error.code !== "runtime_scope_controlled" || !scopeRecoveryLoadAllowed(context, budget.scope_id, record, input))
297
+ if (!(error instanceof AppError) || error.code !== "runtime_scope_controlled" || !scopeRecoveryLoadAllowed(context, budget.scope_id, record, input, recoveryOwnerAlive))
317
298
  throw error;
318
299
  }
319
300
  const frozen = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'provider-budget' AND resource_key = ?", [budget.scope_id]);
@@ -325,7 +306,6 @@ export function reserveProviderTurn(context, record, input) {
325
306
  if (prior) {
326
307
  if (prior.lease_token !== record.lease_token)
327
308
  throw new AppError("process_ownership_unknown", "Retained budget claim belongs to another physical owner", 1);
328
- db.exec("COMMIT");
329
309
  return { admitted: true, budget_scope: budget.scope_id, reused: true };
330
310
  }
331
311
  const activeTurns = db.all("SELECT process_id, metadata_json FROM managed_resources WHERE resource_kind = 'provider-turn' AND json_extract(metadata_json, '$.scope_id') = ? AND json_extract(metadata_json, '$.harness') = ? ORDER BY created_at, resource_key", [budget.scope_id, harness]);
@@ -335,21 +315,15 @@ export function reserveProviderTurn(context, record, input) {
335
315
  const claim = JSON.parse(turn.metadata_json);
336
316
  return { process_id: turn.process_id, operation_id: claim.operation_id ?? null, operation: claim.operation ?? null, session_id: claim.session_id ?? null };
337
317
  });
338
- db.exec("COMMIT");
339
318
  return { admitted: false, reason: "provider_capacity", budget_scope: budget.scope_id, harness, limit, active, blocking_turns };
340
319
  }
341
320
  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 ('provider-turn', ?, ?, ?, ?, ?, ?, ?, ?)", [key, record.owner_id, record.lease_token, record.lease_expires_at, record.id, JSON.stringify({ scope_id: budget.scope_id, harness, operation_id: input.operationId, operation: input.operation, session_id: input.sessionId ?? null, state_dir: metadata.state_dir }), now, now]);
342
- db.exec("COMMIT");
343
321
  return { admitted: true, budget_scope: budget.scope_id, harness, limit, active: active + 1 };
344
- }
345
- catch (error) {
346
- db.exec("ROLLBACK");
347
- throw error;
348
- }
322
+ });
349
323
  }
350
- function scopeRecoveryLoadAllowed(context, scopeId, record, input) {
324
+ function scopeRecoveryLoadAllowed(context, scopeId, record, input, physicalOwnerAlive) {
351
325
  const metadata = JSON.parse(record.metadata_json);
352
- if (input.operation !== "session.resume" || !input.daemonId || !metadata.state_dir || metadata.dd_flow_home !== context.ddFlowHome || !processIsAlive(record))
326
+ if (input.operation !== "session.resume" || !input.daemonId || !metadata.state_dir || metadata.dd_flow_home !== context.ddFlowHome || !physicalOwnerAlive)
353
327
  return false;
354
328
  const db = getResourceDatabase(resourceHome(context));
355
329
  const control = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control' AND resource_key = ?", [scopeId]);
@@ -6,7 +6,7 @@ import { getResourceDatabase } from "../storage/database.js";
6
6
  import { resourceHome } from "./managed-processes.js";
7
7
  import { validateRetainedRuntimeScopeManifest } from "./runtime-budget.js";
8
8
  /** Archive a complete append-only prefix, not a claim that no later evidence
9
- * can arrive. The caller holds the registry transaction; never take the EVAL
9
+ * can arrive. Prepare before taking the registry writer; never take the EVAL
10
10
  * journal lock here (its writer may itself be waiting for the registry).
11
11
  */
12
12
  export function captureScopeJournal(context, control, manifest) {
@@ -41,10 +41,12 @@ export function captureScopeJournal(context, control, manifest) {
41
41
  const sha256 = hash(prefix), key = hash(JSON.stringify([control.scope_id, control.generation, manifest.manifest_sha256, sha256]));
42
42
  const payload = JSON.stringify({ scope_id: control.scope_id, generation: control.generation, manifest, manifest_bytes: manifestBytes, journal_source: source, journal_bytes: text });
43
43
  const registry = getResourceDatabase(resourceHome(context));
44
- const prior = registry.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control-capture' AND resource_key = ?", [key]);
45
- if (prior && prior.metadata_json !== payload)
46
- throw new AppError("runtime_scope_capture_changed", "Retained EVAL capture no longer matches its content identity", 1);
47
- if (!prior)
48
- registry.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-control-capture',?,?,?,?,NULL,?,?,?)", [key, control.scope_id, key, context.now(), payload, context.now(), context.now()]);
44
+ registry.writeTransaction(() => {
45
+ const prior = registry.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control-capture' AND resource_key = ?", [key]);
46
+ if (prior && prior.metadata_json !== payload)
47
+ throw new AppError("runtime_scope_capture_changed", "Retained EVAL capture no longer matches its content identity", 1);
48
+ if (!prior)
49
+ registry.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-control-capture',?,?,?,?,NULL,?,?,?)", [key, control.scope_id, key, context.now(), payload, context.now(), context.now()]);
50
+ });
49
51
  return { artifact_key: key, source, sha256, byte_length: prefix.length, event_count: events.length, request_sequence: request.data.sequence, consistency: "append_only_prefix", trailing_bytes: bytes.length - prefix.length };
50
52
  }
@@ -2,7 +2,7 @@ import crypto from "node:crypto";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { createContext } from "../runtime/context.js";
5
- import { getResourceDatabase } from "../storage/database.js";
5
+ import { getResourceDatabase, withDeferredPublications } from "../storage/database.js";
6
6
  import { AppError } from "../shared/errors.js";
7
7
  import { finishManagedProcess, managedLeaseReleased, managedProcessBinding, managedProcessStatus, processTreeIsAlive, resourceHome, stopManagedProcess } from "./managed-processes.js";
8
8
  import { requestRuntimeScopeControl, runtimeBudgetStatus, scopeLocalProcessRole, validateRetainedRuntimeScopeManifest } from "./runtime-budget.js";
@@ -14,6 +14,7 @@ import { verifyRecoverySnapshot } from "./eval-snapshots.js";
14
14
  import { captureScopeJournal } from "./runtime-scope-capture.js";
15
15
  import { historicalDaemonSettlement, assertHistoricalDaemonSettlement } from "./native-daemon-history.js";
16
16
  import { managedProviderBelongsToDaemon } from "./managed-daemon-binding.js";
17
+ import { cleanupScopeWriters } from "./scope-writer-cleanup.js";
17
18
  const scopeInventoryIdentity = (records) => JSON.stringify(records.map(record => managedProcessBinding(record, "")).sort((a, b) => a.process_id.localeCompare(b.process_id)));
18
19
  /** Persist the all-role drain boundary, separately from recovery readiness.
19
20
  * Registry -> RUN lock order matches scope dispatch. No await/provider call is
@@ -22,21 +23,17 @@ const scopeInventoryIdentity = (records) => JSON.stringify(records.map(record =>
22
23
  export function retainScopeDrain(context, control, processes, runs, native, probes) {
23
24
  const registry = getResourceDatabase(resourceHome(context));
24
25
  const targets = new Map();
26
+ const heldTargets = new Set();
27
+ const workspaces = new Map();
25
28
  const captures = [];
26
- registry.beginWriteTransaction();
29
+ let registryHeld = false, primaryFailure;
30
+ let committedReceipt;
27
31
  try {
28
32
  for (const run of [...runs].sort((a, b) => String(a.runtime_home).localeCompare(String(b.runtime_home)))) {
29
33
  const home = String(run.runtime_home);
30
34
  let target = targets.get(home);
31
35
  if (!target) {
32
36
  target = createContext({ ...context.env, DD_FLOW_HOME: home, DD_FLOW_RESOURCE_HOME: resourceHome(context), DD_FLOW_ENGINE_MODE: "1" });
33
- try {
34
- target.db.beginWriteTransaction();
35
- }
36
- catch (error) {
37
- target.db.close?.();
38
- throw error;
39
- }
40
37
  targets.set(home, target);
41
38
  }
42
39
  const project = target.db.get("SELECT root FROM projects WHERE id = ?", [run.project_id]);
@@ -51,6 +48,7 @@ export function retainScopeDrain(context, control, processes, runs, native, prob
51
48
  throw new AppError("runtime_scope_inventory_changed", "RUN no longer proves its captured process binding", 1);
52
49
  }
53
50
  run.receipt = fresh;
51
+ workspaces.set(run, target.db.get("SELECT workspace_root FROM runs WHERE project_id = ? AND id = ?", [run.project_id, run.run_id]).workspace_root);
54
52
  // Capture failure does not undo physical drain. Retain the failure beside
55
53
  // the stop evidence, and keep resume fenced until capture is reconciled.
56
54
  try {
@@ -104,22 +102,52 @@ export function retainScopeDrain(context, control, processes, runs, native, prob
104
102
  const capture = { manifest, journal, run_captures: captures, native_bindings: resumableNative.map(owner => ({ process_id: owner.process_id, role: owner.role, state_dir: owner.state_dir, daemon_id: owner.daemon_id, runtime_owner: owner.runtime_owner, inventory_version: owner.inventory_version, sessions: owner.sessions, operations: owner.operations })), probe_operations: probes, pending_reasons: [...new Set([manifestReason, ...captures.filter(item => item.status !== "verified").map(item => String(item.reason)), ...(resumableNative.length ? ["non_work_recovery_required"] : []), ...(probes.length ? ["probe_reconciliation_required"] : [])])] };
105
103
  const receipt = { scope_id: control.scope_id, generation: control.generation, requested_mode: control.requested_mode, observed_at: context.now(), physical_settled: true, recovery_ready: false, process_bindings: processes.map(record => managedProcessBinding(record, resourceHome(context))), runs, native, probes, capture };
106
104
  const key = crypto.createHash("sha256").update(JSON.stringify([control.scope_id, control.generation])).digest("hex");
105
+ // Captures are immutable evidence, not release authority. Only this short
106
+ // registry -> RUN boundary publishes the all-role drain grant.
107
+ registry.beginWriteTransaction();
108
+ registryHeld = true;
109
+ for (const target of targets.values()) {
110
+ target.db.beginWriteTransaction();
111
+ heldTargets.add(target);
112
+ }
113
+ const current = runtimeBudgetStatus(context, control.scope_id);
114
+ if (current.fence || current.control?.generation !== control.generation
115
+ || scopeInventoryIdentity(processes) !== scopeInventoryIdentity(current.processes) || current.provider_turns.length
116
+ || JSON.stringify(current.manifest) !== JSON.stringify(latest.manifest))
117
+ throw new AppError("runtime_scope_inventory_changed", "Scope changed while its drain evidence was prepared", 1);
118
+ for (const run of runs) {
119
+ const target = targets.get(String(run.runtime_home));
120
+ const project = target.db.get("SELECT root FROM projects WHERE id = ?", [run.project_id]);
121
+ const saved = target.db.get("SELECT workspace_root FROM runs WHERE project_id = ? AND id = ?", [run.project_id, run.run_id]);
122
+ const previous = run.receipt;
123
+ if (!project || saved?.workspace_root !== workspaces.get(run)
124
+ || JSON.stringify(runControlStatus(target, { projectRoot: project.root, runId: String(run.run_id), controlId: previous.control.control_id })) !== JSON.stringify(run.receipt))
125
+ throw new AppError("runtime_scope_inventory_changed", "RUN changed while its drain evidence was prepared", 1);
126
+ }
107
127
  registry.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-control-drain',?,?,?,?,NULL,?,?,?) ON CONFLICT(resource_kind,resource_key) DO UPDATE SET metadata_json = excluded.metadata_json, updated_at = excluded.updated_at", [key, control.scope_id, key, context.now(), JSON.stringify(receipt), context.now(), context.now()]);
108
128
  registry.exec("COMMIT");
129
+ registryHeld = false;
130
+ committedReceipt = receipt;
109
131
  return receipt;
110
132
  }
111
133
  catch (error) {
112
- registry.exec("ROLLBACK");
134
+ primaryFailure = error;
113
135
  throw error;
114
136
  }
115
137
  finally {
116
- for (const target of targets.values()) {
138
+ const errors = cleanupScopeWriters([
139
+ ...[...targets.values()].reverse().map(target => ({ name: target.ddFlowHome, db: target.db, rollback: heldTargets.has(target), close: "always" })),
140
+ { name: resourceHome(context), db: registry, rollback: registryHeld, close: "on_error" }
141
+ ]);
142
+ if (errors.length && committedReceipt)
143
+ committedReceipt.cleanup_errors = errors;
144
+ else if (errors.length && primaryFailure instanceof AppError)
145
+ primaryFailure.details.scope_cleanup_errors = errors;
146
+ else if (errors.length) {
117
147
  try {
118
- target.db.exec("ROLLBACK");
119
- }
120
- finally {
121
- target.db.close?.();
148
+ process.stderr.write(`${JSON.stringify({ phase: "scope_drain_cleanup", primary: String(primaryFailure), errors })}\n`);
122
149
  }
150
+ catch { /* Cleanup must not replace the primary outcome. */ }
123
151
  }
124
152
  }
125
153
  }
@@ -157,16 +185,10 @@ export async function controlRuntimeScopeLocalProbes(context, scopeId, generatio
157
185
  if (mode === "pause" && processTreeIsAlive(current))
158
186
  return { process_id: record.id, physical_settled: false, reason: "local_probe_running" };
159
187
  const operationId = `scope-probe:${crypto.createHash("sha256").update(JSON.stringify([scopeId, generation, record.id, record.lease_token, record.pid_started_at])).digest("hex")}`;
160
- registry.beginWriteTransaction();
161
- try {
188
+ registry.writeTransaction(() => {
162
189
  current = validate();
163
190
  registry.run("INSERT OR IGNORE INTO managed_resources (resource_kind,resource_key,owner_id,lease_token,lease_expires_at,process_id,metadata_json,created_at,updated_at) VALUES ('scope-control-operation',?,?,?,?,NULL,?,?,?)", [operationId, scopeId, record.lease_token, context.now(), JSON.stringify({ scope_id: scopeId, generation, process_id: record.id, operation: mode === "stop" ? "probe.stop" : "probe.observe", status: "dispatching" }), context.now(), context.now()]);
164
- registry.exec("COMMIT");
165
- }
166
- catch (error) {
167
- registry.exec("ROLLBACK");
168
- throw error;
169
- }
191
+ });
170
192
  // A prior owner may have died after recording intent but before signalling.
171
193
  // Repeating an identity-checked stop never redispatches productive work.
172
194
  if (mode === "stop" && processTreeIsAlive(current))
@@ -240,16 +262,10 @@ export async function controlRuntimeScopeNativeOwners(context, scopeId, generati
240
262
  const operationId = `scope-cancel:${crypto.createHash("sha256").update(JSON.stringify([scopeId, generation, record.id, state.daemon_id, id])).digest("hex")}`;
241
263
  const now = context.now();
242
264
  let inserted = false;
243
- registry.beginWriteTransaction();
244
- try {
265
+ registry.writeTransaction(() => {
245
266
  assertOwner();
246
267
  inserted = registry.run("INSERT OR IGNORE INTO managed_resources (resource_kind,resource_key,owner_id,lease_token,lease_expires_at,process_id,metadata_json,created_at,updated_at) VALUES ('scope-control-operation',?,?,?,?,NULL,?,?,?)", [operationId, scopeId, record.lease_token, now, JSON.stringify({ scope_id: scopeId, generation, process_id: record.id, daemon_id: state.daemon_id, session_id: id, status: "dispatching" }), now, now]).changes === 1;
247
- registry.exec("COMMIT");
248
- }
249
- catch (error) {
250
- registry.exec("ROLLBACK");
251
- throw error;
252
- }
268
+ });
253
269
  try {
254
270
  const saved = JSON.parse(registry.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control-operation' AND resource_key = ?", [operationId]).metadata_json);
255
271
  if (saved.status === "completed") {
@@ -331,8 +347,7 @@ export async function controlRuntimeScopeRuns(context, scopeId, requestId, mode,
331
347
  let input = { projectRoot: project.root, runId: record.run_id, requestId: runRequestId, action: mode };
332
348
  // Lock order is resource registry then RUN database. No provider or await
333
349
  // occurs here; a newer scope request cannot interleave RUN intent creation.
334
- registry.beginWriteTransaction();
335
- try {
350
+ withDeferredPublications(() => registry.writeTransaction(() => {
336
351
  const current = registry.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control' AND resource_key = ?", [scopeId]);
337
352
  if (!current || JSON.parse(current.metadata_json).generation !== control.generation || registry.get("SELECT 1 FROM managed_resources WHERE resource_kind = 'provider-fence' AND resource_key = ?", [scopeId]))
338
353
  throw new AppError("runtime_scope_control_superseded", "Scope request no longer owns admission", 1);
@@ -346,12 +361,7 @@ export async function controlRuntimeScopeRuns(context, scopeId, requestId, mode,
346
361
  ...(active.requested_mode === "stop" ? { force: active.request.force ?? false } : { graceMs: active.request.grace_ms ?? 30_000, ...(active.request.escalate ? { escalate: active.request.escalate } : {}) }) };
347
362
  }
348
363
  recordRunControlIntent(target, input);
349
- registry.exec("COMMIT");
350
- }
351
- catch (error) {
352
- registry.exec("ROLLBACK");
353
- throw error;
354
- }
364
+ }));
355
365
  seen.add(key);
356
366
  const receipt = await requestRunControl(target, { ...input, waitMs: 0, ...(reconcileId ? { reconcileId: crypto.createHash("sha256").update(JSON.stringify([scopeId, control.generation, reconcileId])).digest("hex") } : {}) });
357
367
  runs.push({ runtime_home: metadata.dd_flow_home, project_id: record.project_id, run_id: record.run_id, request_id: input.requestId, receipt });