@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
@@ -9,9 +9,10 @@ import { managedProcessBinding, processIsAlive, processTreeIsAlive, resourceHome
9
9
  import { runtimeBudgetStatus, scopeLocalProcessRole, validateRetainedRuntimeScopeManifest, prepareRuntimeScopeFence } from "./runtime-budget.js";
10
10
  import { nativeInventoryVersion, retainedDaemonOperations } from "./run-control.js";
11
11
  import { adapterSessionId, runHarnessAdapter } from "./harness-adapter.js";
12
- import { recordControllerResume, resumeRunController } from "./run-controller-recovery.js";
12
+ import { prepareControllerResume, resumeRunController } from "./run-controller-recovery.js";
13
13
  import { readNativeDaemonState, requireManagedDaemonBinding } from "./managed-daemon-binding.js";
14
14
  import { assertHistoricalDaemonSettlement } from "./native-daemon-history.js";
15
+ import { cleanupScopeWriters } from "./scope-writer-cleanup.js";
15
16
  const hash = (value) => crypto.createHash("sha256").update(value).digest("hex");
16
17
  const nativeLaunchEnvironmentKeys = ["CODEX_HOME", "DD_FLOW_CONFIG_HOME", "DD_FLOW_HOME", "DD_FLOW_BIN", "DD_FLOW_RESOURCE_HOME"];
17
18
  function nativeLaunchEnvironment(context, saved) {
@@ -67,16 +68,8 @@ export async function prepareRuntimeScopeResume(context, scopeId, generation, re
67
68
  if (!project || manifest?.budget?.scope_id !== scopeId || manifest.resource_home !== resourceHome(context))
68
69
  throw new AppError("runtime_scope_owner_unknown", "Resume RUN no longer belongs to this retained scope", 1);
69
70
  const input = { projectRoot: project.root, runId: source.run_id, requestId: source.resume_request_id, controlId: source.control_id };
70
- db.beginWriteTransaction();
71
- try {
72
- assertCurrent();
73
- recordControllerResume(target, input);
74
- db.exec("COMMIT");
75
- }
76
- catch (error) {
77
- db.exec("ROLLBACK");
78
- throw error;
79
- }
71
+ const commitResume = prepareControllerResume(target, input);
72
+ db.writeTransaction(() => { assertCurrent(); commitResume(); });
80
73
  assertCurrent();
81
74
  const receipt = await resumeRunController(target, { ...input, waitMs: 0 });
82
75
  assertCurrent();
@@ -108,9 +101,19 @@ export async function prepareRuntimeScopeResume(context, scopeId, generation, re
108
101
  const lockedRuns = new Map(), heldRuns = [];
109
102
  let registryHeld = false, runPreparationConsistent = false;
110
103
  let released = null, releaseError = null, releaseMessage = null;
104
+ let cleanupErrors = [];
111
105
  try {
112
106
  for (const home of [...new Set(intent.runs.map((source) => source.runtime_home))].sort())
113
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);
114
117
  db.beginWriteTransaction();
115
118
  registryHeld = true;
116
119
  for (const target of lockedRuns.values()) {
@@ -118,14 +121,11 @@ export async function prepareRuntimeScopeResume(context, scopeId, generation, re
118
121
  heldRuns.push(target);
119
122
  }
120
123
  assertCurrent();
121
- for (const [index, source] of intent.runs.entries()) {
122
- const target = lockedRuns.get(source.runtime_home);
123
- const proof = 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);
124
- runs[index] = { ...runs[index], ...proof, ...(proof.prepared === true ? { pending: false, reason: null } : {}) };
125
- }
124
+ for (const check of checks)
125
+ check();
126
126
  runPreparationConsistent = true;
127
- if (releaseRequestId !== undefined) {
128
- const receipt = publishScopeRelease(context, intent, runs, native);
127
+ if (commitRelease) {
128
+ const receipt = commitRelease();
129
129
  db.exec("COMMIT");
130
130
  registryHeld = false;
131
131
  released = receipt;
@@ -138,28 +138,21 @@ export async function prepareRuntimeScopeResume(context, scopeId, generation, re
138
138
  runs[index] = { ...run, prepared: false, pending: true, reason: error instanceof AppError ? error.code : "scope_writer_boundary_unavailable" };
139
139
  }
140
140
  finally {
141
- try {
142
- for (const target of heldRuns.reverse())
143
- target.db.exec("ROLLBACK");
144
- }
145
- finally {
146
- try {
147
- if (registryHeld)
148
- db.exec("ROLLBACK");
149
- }
150
- finally {
151
- for (const target of lockedRuns.values())
152
- target.db.close?.();
153
- }
154
- }
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
+ ]);
155
145
  }
156
146
  if (released)
157
- return { ...released, status: "released", dispatch_blocked: false, current: true, settled: false, reused: false };
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 });
158
150
  assertCurrent();
159
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"] : [])])] };
160
152
  }
161
- /** Called only inside the registry + all-RUN writer boundary above. */
162
- function publishScopeRelease(context, intent, runs, native) {
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) {
163
156
  const db = getResourceDatabase(resourceHome(context));
164
157
  if (runs.some(run => run.prepared !== true) || native.some(owner => owner?.prepared !== true))
165
158
  throw new AppError("runtime_scope_preparation_required", "Every owned RUN and native Session must be prepared", 1);
@@ -251,27 +244,42 @@ function publishScopeRelease(context, intent, runs, native) {
251
244
  throw new AppError("runtime_scope_inventory_changed", "Release has an unverified process registration", 1);
252
245
  const key = hash(JSON.stringify([intent.scope_id, intent.generation])), now = context.now();
253
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 };
254
- 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]);
255
- 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)
256
- throw new AppError("runtime_scope_control_superseded", "Release lost its control generation", 1);
257
- 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]);
258
- return receipt;
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
+ };
259
262
  }
260
263
  /** A preparation snapshot, not an unfence. The release arbiter must repeat
261
264
  * these reads while holding every participating RUN writer boundary. */
262
265
  function preparedScopeRun(context, input, assertCurrent) {
263
266
  const registry = getResourceDatabase(resourceHome(context));
264
- registry.exec("SAVEPOINT scope_run_registry");
265
- let readingRun = false;
266
- try {
267
- context.db.exec("SAVEPOINT scope_run_preparation");
268
- readingRun = true;
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(() => {
269
277
  assertCurrent();
270
- const controller = context.db.get("SELECT * FROM run_controllers WHERE project_id = ? AND run_id = ? AND status NOT IN ('completed','cancelled','superseded')", [input.projectId, input.runId]);
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]);
271
279
  if (!controller)
272
280
  throw new AppError("runtime_scope_owner_unknown", "Prepared RUN lost its controller", 1);
273
281
  const state = JSON.parse(controller.state_json), manifest = JSON.parse(controller.manifest_json);
274
- const operation = context.db.get(`SELECT o.operation_id, o.receipt_json FROM run_controller_operations o
282
+ const operation = read(`SELECT o.operation_id, o.receipt_json FROM run_controller_operations o
275
283
  JOIN run_controls ctl ON ctl.control_id = ? AND ctl.project_id = ? AND ctl.run_id = ?
276
284
  JOIN run_recovery_guards g ON g.recovery_id = ctl.recovery_id
277
285
  WHERE o.controller_id = ? AND o.generation = ? AND o.status = 'accepted'
@@ -279,61 +287,57 @@ function preparedScopeRun(context, input, assertCurrent) {
279
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]);
280
288
  const prepared = operation?.receipt_json ? JSON.parse(operation.receipt_json) : null;
281
289
  if (prepared?.prepared !== true)
282
- return { prepared: false, pending: true, reason: "run_recovery_preparation_required" };
290
+ return finish({ prepared: false, pending: true, reason: "run_recovery_preparation_required" });
283
291
  const session = state.current_session === null ? null : state.sessions[state.current_session];
284
292
  const owner = registry.get("SELECT * FROM managed_processes WHERE id = ?", [controller.process_id]);
285
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))
286
294
  throw new AppError("runtime_scope_owner_unknown", "RUN preparation lost its current owner or Session", 1);
287
295
  const native = requireManagedDaemonBinding(context, { stateDir: session.state_dir, projectId: input.projectId, runId: input.runId, ownerId: session.owner.owner_id });
288
- const loaded = context.db.get("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]);
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]);
289
297
  const loadReceipt = loaded?.receipt_json ? JSON.parse(loaded.receipt_json) : null;
290
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)
291
299
  throw new AppError("runtime_scope_owner_unknown", "RUN preparation has no current native load acknowledgement", 1);
292
- return { 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) } };
293
- }
294
- finally {
295
- try {
296
- if (readingRun)
297
- context.db.exec("RELEASE scope_run_preparation");
298
- }
299
- finally {
300
- registry.exec("RELEASE scope_run_registry");
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
+ });
301
307
  }
302
- }
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
+ }));
303
310
  }
304
311
  async function restartScopeNative(context, intent, source, assertCurrent) {
305
312
  const db = getResourceDatabase(resourceHome(context)), key = hash(JSON.stringify([intent.scope_id, intent.generation, source.process_id]));
306
313
  const saved = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-resume-native' AND resource_key = ?", [key]);
307
- const operation = JSON.parse(saved.metadata_json);
314
+ let operation = JSON.parse(saved.metadata_json);
308
315
  const startup = source.operations.find(item => item.operation === "daemon.start" && item.result?.daemon_id === source.daemon_id);
309
316
  const launch = startup.launch;
310
317
  const state = () => JSON.parse(fs.readFileSync(path.join(source.state_dir, "daemon.json"), "utf8"));
311
- const assertRetired = () => {
318
+ const assertRetired = (physical = true) => {
312
319
  assertCurrent();
313
320
  const old = db.get("SELECT * FROM managed_processes WHERE id = ?", [source.process_id]);
314
- if (!old || processTreeIsAlive(old) || JSON.stringify(managedProcessBinding(old, resourceHome(context))) !== JSON.stringify(intent.process_bindings.find(item => item.process_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)))
315
322
  throw new AppError("runtime_scope_owner_unknown", "The retired native tree changed or is still alive", 1);
316
323
  };
317
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);
318
328
  let dispatch = false;
319
- db.beginWriteTransaction();
320
- try {
321
- assertRetired();
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);
322
332
  if (operation.status === "retired") {
323
- const old = state();
324
- if (old.daemon_id !== source.daemon_id || old.shutdown_state !== "clean")
325
- throw new AppError("runtime_scope_owner_unknown", "Native restart lost its clean predecessor", 1);
333
+ if (!predecessor)
334
+ throw new AppError("runtime_scope_inventory_changed", "Native retirement changed during restart preparation", 1);
326
335
  operation.status = "starting";
327
336
  operation.restart_operation_id = `scope-restart:${key}`;
328
337
  dispatch = true;
329
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]);
330
339
  }
331
- db.exec("COMMIT");
332
- }
333
- catch (error) {
334
- db.exec("ROLLBACK");
335
- throw error;
336
- }
340
+ });
337
341
  if (!operation.restart_receipt) {
338
342
  if (dispatch) {
339
343
  const env = nativeLaunchEnvironment(context, launch.env);
@@ -358,10 +362,11 @@ async function restartScopeNative(context, intent, source, assertCurrent) {
358
362
  operation.status = "restarted";
359
363
  operation.replacement_binding = binding;
360
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]);
361
- const assertReplacement = () => {
362
- assertRetired();
363
- const current = db.get("SELECT * FROM managed_processes WHERE id = ?", [replacement.id]), latest = state();
364
- if (!current || !processIsAlive(current) || JSON.stringify(managedProcessBinding(current, resourceHome(context))) !== JSON.stringify(binding) || 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))
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)))
365
370
  throw new AppError("runtime_scope_owner_unknown", "Session loading lost its native replacement", 1);
366
371
  };
367
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)
@@ -369,10 +374,11 @@ async function restartScopeNative(context, intent, source, assertCurrent) {
369
374
  const sessions = [];
370
375
  for (const session of source.sessions) {
371
376
  const operationId = `scope-load:${hash(JSON.stringify([intent.scope_id, intent.generation, source.process_id, session.session_id]))}`;
372
- let load, dispatchLoad = false;
373
- db.beginWriteTransaction();
374
- try {
375
- assertReplacement();
377
+ let dispatchLoad = false;
378
+ assertReplacement();
379
+ const load = db.writeTransaction(() => {
380
+ let load;
381
+ assertReplacement(false);
376
382
  const row = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-resume-native-load' AND resource_key = ?", [operationId]);
377
383
  if (row)
378
384
  load = JSON.parse(row.metadata_json);
@@ -384,12 +390,8 @@ async function restartScopeNative(context, intent, source, assertCurrent) {
384
390
  }
385
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)
386
392
  throw new AppError("runtime_scope_owner_unknown", "Retained Session load belongs to another physical owner", 1);
387
- db.exec("COMMIT");
388
- }
389
- catch (error) {
390
- db.exec("ROLLBACK");
391
- throw error;
392
- }
393
+ return load;
394
+ });
393
395
  if (!load.receipt) {
394
396
  if (dispatchLoad) {
395
397
  assertReplacement();
@@ -417,10 +419,11 @@ async function retireScopeNative(context, intent, source, assertCurrent) {
417
419
  const record = db.get("SELECT * FROM managed_processes WHERE id = ?", [source.process_id]);
418
420
  const frozen = intent.process_bindings.find((binding) => binding.process_id === source.process_id);
419
421
  const state = () => JSON.parse(fs.readFileSync(path.join(source.state_dir, "daemon.json"), "utf8"));
420
- const assertOwner = () => {
422
+ const assertOwner = (physical = true) => {
421
423
  assertCurrent();
422
- const current = db.get("SELECT * FROM managed_processes WHERE id = ?", [source.process_id]), daemon = state();
423
- if (!current || current.run_id || !["judge", "probe"].includes(source.role) || JSON.stringify(managedProcessBinding(current, resourceHome(context))) !== JSON.stringify(frozen) || 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))
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)))
424
427
  throw new AppError("runtime_scope_owner_unknown", "Native retirement lost its frozen physical owner", 1);
425
428
  };
426
429
  assertOwner();
@@ -437,28 +440,24 @@ async function retireScopeNative(context, intent, source, assertCurrent) {
437
440
  throw new AppError("runtime_scope_native_launch_required", "Native recovery requires the original owned launch contract", 1);
438
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"))
439
442
  throw new AppError("runtime_scope_native_launch_required", "Native recovery requires explicit configuration presence before retiring its owner", 1);
440
- let operation;
441
443
  let dispatch = false;
442
- db.beginWriteTransaction();
443
- try {
444
- assertOwner();
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);
445
448
  const saved = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-resume-native' AND resource_key = ?", [key]);
446
449
  if (saved)
447
450
  operation = JSON.parse(saved.metadata_json);
448
451
  else {
449
- if (nativeInventoryVersion(source.state_dir) !== source.inventory_version)
452
+ if (preparedInventory !== source.inventory_version)
450
453
  throw new AppError("runtime_scope_inventory_changed", "Native owner changed before retirement", 1);
451
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" };
452
455
  const now = context.now();
453
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]);
454
457
  dispatch = true;
455
458
  }
456
- db.exec("COMMIT");
457
- }
458
- catch (error) {
459
- db.exec("ROLLBACK");
460
- throw error;
461
- }
459
+ return operation;
460
+ });
462
461
  if (!operation.receipt) {
463
462
  if (dispatch) {
464
463
  assertOwner();
@@ -539,27 +538,40 @@ export function prepareRuntimeScopeResumeRequest(context, input) {
539
538
  }
540
539
  }
541
540
  export function recordRuntimeScopeResume(context, input) {
542
- const prepared = prepareRuntimeScopeResumeRequest(context, input);
543
- if (prepared.reused)
544
- return prepared;
545
- const db = getResourceDatabase(resourceHome(context)), key = hash(JSON.stringify([input.scopeId, input.requestId]));
546
- db.beginWriteTransaction();
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;
547
553
  try {
548
- // Recheck mutable capture/ownership authority under the existing arbiter.
549
- const result = prepareRuntimeScopeResumeRequest(context, input);
550
- if (!result.reused) {
551
- const receipt = { ...result };
552
- delete receipt.current;
553
- delete receipt.reused;
554
- const payload = JSON.stringify(receipt), now = context.now();
555
- 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]);
556
- 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]);
557
- }
558
- db.exec("COMMIT");
559
- return result;
554
+ before = authority();
555
+ prepared = prepareRuntimeScopeResumeRequest(context, input);
560
556
  }
561
- catch (error) {
562
- db.exec("ROLLBACK");
563
- throw error;
557
+ finally {
558
+ db.close?.();
564
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
+ });
565
577
  }
@@ -53,45 +53,49 @@ export async function reconcileRuntimeScopeControl(context, scopeId, generation,
53
53
  }
54
54
  export async function launchRuntimeScopeControl(context, scopeId, requestId, mode, manifest, reconcileId, resumeId, prepared) {
55
55
  const control = requestRuntimeScopeControl(context, scopeId, requestId, mode, manifest, prepared), db = getResourceDatabase(resourceHome(context)), key = keyFor(scopeId, control.generation);
56
- let worker;
57
- db.beginWriteTransaction();
58
- try {
59
- const current = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control' AND resource_key = ?", [scopeId]);
60
- const prior = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control-worker' AND resource_key = ?", [key]);
61
- const existing = prior ? JSON.parse(prior.metadata_json) : null;
62
- if (existing) {
63
- const identity = existing.owner ?? existing.launcher;
64
- if (!identity || !Number.isSafeInteger(identity.pid) || identity.pid < 1 || typeof identity.pid_started_at !== "string" || !identity.pid_started_at || !["starting", "running", "completed", "failed", "superseded"].includes(existing.status))
65
- throw new AppError("runtime_scope_owner_unknown", "Retained scope owner has no verifiable birth identity", 1);
66
- }
67
- if (!current || JSON.parse(current.metadata_json).generation !== control.generation || db.get("SELECT 1 FROM managed_resources WHERE resource_kind = 'provider-fence' AND resource_key = ?", [scopeId])) {
68
- db.exec("COMMIT");
69
- return { scope_id: scopeId, control, current: false, settled: false, worker: existing ? publicRuntimeScopeWorker(existing) : null };
70
- }
71
- if (existing && (existing.owner ? processIsAlive(existing.owner) : existing.status !== "failed" && processIsAlive(existing.launcher))) {
72
- db.exec("COMMIT");
73
- return { scope_id: scopeId, control, current: true, settled: false, worker: publicRuntimeScopeWorker(existing) };
74
- }
75
- if (existing?.status === "completed" && !reconcileId && !resumeId) {
76
- db.exec("COMMIT");
77
- return { scope_id: scopeId, control, current: true, settled: false, worker: publicRuntimeScopeWorker(existing) };
78
- }
79
- const renewal = resumeId ? `resume:${resumeId}` : reconcileId;
80
- const snapshot = renewal ? renewRecoveryObservation(existing?.snapshot ?? {}, renewal) : existing?.snapshot;
81
- if (existing && recoveryObservationBudget(snapshot).state().remaining_ms === 0) {
82
- db.exec("COMMIT");
83
- return { scope_id: scopeId, control, current: true, settled: false, worker: publicRuntimeScopeWorker(existing) };
56
+ const launcher = birth();
57
+ const publication = (() => {
58
+ for (let attempt = 0;; attempt++) {
59
+ const prior = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control-worker' AND resource_key = ?", [key]);
60
+ const existing = prior ? JSON.parse(prior.metadata_json) : null;
61
+ if (existing) {
62
+ const identity = existing.owner ?? existing.launcher;
63
+ if (!identity || !Number.isSafeInteger(identity.pid) || identity.pid < 1 || typeof identity.pid_started_at !== "string" || !identity.pid_started_at || !["starting", "running", "completed", "failed", "superseded"].includes(existing.status))
64
+ throw new AppError("runtime_scope_owner_unknown", "Retained scope owner has no verifiable birth identity", 1);
65
+ }
66
+ const existingAlive = !!existing && (existing.owner ? processIsAlive(existing.owner) : existing.status !== "failed" && processIsAlive(existing.launcher));
67
+ try {
68
+ return db.writeTransaction(() => {
69
+ const current = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control' AND resource_key = ?", [scopeId]);
70
+ if (db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control-worker' AND resource_key = ?", [key])?.metadata_json !== prior?.metadata_json)
71
+ throw new AppError("runtime_scope_owner_changed", "Scope owner changed while its physical identity was checked", 1);
72
+ if (!current || JSON.parse(current.metadata_json).generation !== control.generation || db.get("SELECT 1 FROM managed_resources WHERE resource_kind = 'provider-fence' AND resource_key = ?", [scopeId]))
73
+ return { current: false, existing };
74
+ if (existing && existingAlive)
75
+ return { current: true, existing };
76
+ if (existing?.status === "completed" && !reconcileId && !resumeId)
77
+ return { current: true, existing };
78
+ const renewal = resumeId ? `resume:${resumeId}` : reconcileId;
79
+ const snapshot = renewal ? renewRecoveryObservation(existing?.snapshot ?? {}, renewal) : existing?.snapshot;
80
+ if (existing && recoveryObservationBudget(snapshot).state().remaining_ms === 0)
81
+ return { current: true, existing };
82
+ const cleanupRequest = resumeId ? undefined : reconcileId ?? existing?.reconcile_id;
83
+ const worker = { scope_id: scopeId, generation: control.generation, request_id: requestId, mode, token: crypto.randomUUID(), status: "starting", launcher, ...(snapshot ? { snapshot } : {}), ...(cleanupRequest ? { reconcile_id: cleanupRequest } : {}) };
84
+ const now = context.now();
85
+ 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-worker',?,?,?,?,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", [key, scopeId, worker.token, now, JSON.stringify(worker), now, now]);
86
+ return { worker };
87
+ }, "runtimeScope.workerReserve");
88
+ }
89
+ catch (error) {
90
+ // No worker has spawned yet. Recheck physical identity after a racing update.
91
+ if (!(error instanceof AppError) || error.code !== "runtime_scope_owner_changed" || attempt >= 3)
92
+ throw error;
93
+ }
84
94
  }
85
- const cleanupRequest = resumeId ? undefined : reconcileId ?? existing?.reconcile_id;
86
- worker = { scope_id: scopeId, generation: control.generation, request_id: requestId, mode, token: crypto.randomUUID(), status: "starting", launcher: birth(), ...(snapshot ? { snapshot } : {}), ...(cleanupRequest ? { reconcile_id: cleanupRequest } : {}) };
87
- const now = context.now();
88
- 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-worker',?,?,?,?,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", [key, scopeId, worker.token, now, JSON.stringify(worker), now, now]);
89
- db.exec("COMMIT");
90
- }
91
- catch (error) {
92
- db.exec("ROLLBACK");
93
- throw error;
94
- }
95
+ })();
96
+ if (!publication.worker)
97
+ return { scope_id: scopeId, control, current: publication.current, settled: false, worker: publication.existing ? publicRuntimeScopeWorker(publication.existing) : null };
98
+ const worker = publication.worker;
95
99
  try {
96
100
  const entry = fileURLToPath(new URL("../cli.js", import.meta.url));
97
101
  if (!fs.existsSync(entry))
@@ -145,19 +149,14 @@ function requireStartingScopeWorker(db, key, token) {
145
149
  export async function serveRuntimeScopeControl(context, scopeId, generation) {
146
150
  const { key, token } = prepareRuntimeScopeServing(context, scopeId, generation);
147
151
  const db = getResourceDatabase(resourceHome(context));
148
- let worker;
149
- db.beginWriteTransaction();
150
- try {
151
- worker = requireStartingScopeWorker(db, key, token);
152
- worker.owner = birth();
152
+ const owner = birth();
153
+ const worker = db.writeTransaction(() => {
154
+ const worker = requireStartingScopeWorker(db, key, token);
155
+ worker.owner = owner;
153
156
  worker.status = "running";
154
157
  db.run("UPDATE managed_resources SET metadata_json = ?, updated_at = ? WHERE resource_kind = 'scope-control-worker' AND resource_key = ? AND lease_token = ?", [JSON.stringify(worker), context.now(), key, token]);
155
- db.exec("COMMIT");
156
- }
157
- catch (error) {
158
- db.exec("ROLLBACK");
159
- throw error;
160
- }
158
+ return worker;
159
+ });
161
160
  let heartbeat;
162
161
  // A fence must never be held indefinitely by an observer that cannot obtain
163
162
  // fresh provider evidence. A later operator request continues the retained
@@ -0,0 +1,26 @@
1
+ /** Cleanup is secondary to the arbiter's known commit outcome. Always visit
2
+ * every connection, including after a failed rollback or close. */
3
+ export function cleanupScopeWriters(targets) {
4
+ const errors = [];
5
+ for (const target of targets) {
6
+ let failed = false;
7
+ if (target.rollback) {
8
+ try {
9
+ target.db.exec("ROLLBACK");
10
+ }
11
+ catch (error) {
12
+ failed = true;
13
+ errors.push({ target: target.name, phase: "rollback", message: String(error) });
14
+ }
15
+ }
16
+ if (target.close === "always" || failed) {
17
+ try {
18
+ target.db.close?.();
19
+ }
20
+ catch (error) {
21
+ errors.push({ target: target.name, phase: "close", message: String(error) });
22
+ }
23
+ }
24
+ }
25
+ return errors;
26
+ }
@@ -3,6 +3,8 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { AppError } from "../shared/errors.js";
5
5
  import { prepareFileBytes, prepareJsonText, prepareOutputFile } from "../cli/input-preparation.js";
6
+ /** One precedence rule for coordinator and delegated Work packets. */
7
+ export const acceptedDecisionPrecedence = "Read the accepted RUN decisions before applying older general project rules. An accepted, explicitly scoped user decision supersedes a conflicting general rule only within that scope; preserve the rule elsewhere. Do not ask the user again about a decision already settled in those artifacts or invent a broader exception.";
6
8
  export function loadExternalStageContext(input) {
7
9
  const { file: sourcePath, bytes } = prepareFileBytes(input.file, "Stage context");
8
10
  const materializedSha256 = sha256(bytes);
@@ -81,7 +83,10 @@ export function renderExternalStageContext(input) {
81
83
  "</project_context>", "",
82
84
  "<accepted_decisions>",
83
85
  ...(decisions.length ? decisions.map((item) => `- ${typeof item === "string" ? item : `${item.id ? `${item.id}: ` : ""}${item.statement ?? ""}`}`) : ["- No additional accepted decisions."]),
84
- "</accepted_decisions>"
86
+ "</accepted_decisions>", "",
87
+ "<source_precedence>",
88
+ acceptedDecisionPrecedence,
89
+ "</source_precedence>"
85
90
  ].join("\n");
86
91
  }
87
92
  function validateContext(value, expectedStage) {
@@ -172,7 +172,7 @@ export function startStage(context, input, prepared = prepareLegacyStageStart(co
172
172
  continuation_policy: "go_router",
173
173
  session_kind: "orchestrator",
174
174
  current_stage: input.stage,
175
- cwd: attached.run.workspace_root
175
+ cwd: identity?.cwd ?? null
176
176
  })
177
177
  });
178
178
  }