@deksden-com/dd-flow-cli 0.9.0-beta.123 → 0.9.0-beta.127

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 (122) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +33 -1
  3. package/dist/build-info.json +7 -7
  4. package/dist/cli/argument-values.js +38 -0
  5. package/dist/cli/command-inputs.js +4 -3
  6. package/dist/cli/help.js +1 -0
  7. package/dist/cli/hook-ingress.js +1 -1
  8. package/dist/cli/run-cli.js +126 -193
  9. package/dist/cli/runtime-maintenance-diagnostics.js +46 -0
  10. package/dist/cli/runtime-maintenance-ingress.js +51 -0
  11. package/dist/cli/runtime-process.js +128 -0
  12. package/dist/cli.js +8 -5
  13. package/dist/domain/stage-catalog.js +1 -1
  14. package/dist/harness-runtime/bin/dd-agy.mjs +3 -2
  15. package/dist/harness-runtime/bin/dd-codex.mjs +3 -2
  16. package/dist/harness-runtime/bin/dd-droid.mjs +3 -2
  17. package/dist/harness-runtime/bin/dd-grok.mjs +16 -6
  18. package/dist/harness-runtime/bin/dd-opencode.mjs +3 -2
  19. package/dist/harness-runtime/bin/dd-zcode.mjs +20 -16
  20. package/dist/harness-runtime/lib/adapter-timeouts.d.mts +5 -0
  21. package/dist/harness-runtime/lib/adapter-timeouts.mjs +34 -0
  22. package/dist/harness-runtime/lib/codex-capacity-policy.d.mts +10 -0
  23. package/dist/harness-runtime/lib/codex-capacity-policy.mjs +89 -0
  24. package/dist/harness-runtime/lib/daemon-observation.d.mts +2 -0
  25. package/dist/harness-runtime/lib/daemon-observation.mjs +163 -0
  26. package/dist/harness-runtime/lib/daemon-operations.mjs +159 -15
  27. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +215 -86
  28. package/dist/harness-runtime/lib/dd-agy.mjs +11 -13
  29. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +47 -23
  30. package/dist/harness-runtime/lib/dd-codex.mjs +257 -99
  31. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +32 -30
  32. package/dist/harness-runtime/lib/dd-droid.mjs +117 -31
  33. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +96 -38
  34. package/dist/harness-runtime/lib/dd-grok.mjs +15 -13
  35. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +54 -20
  36. package/dist/harness-runtime/lib/dd-opencode.mjs +44 -14
  37. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +81 -76
  38. package/dist/harness-runtime/lib/dd-zcode.mjs +204 -116
  39. package/dist/harness-runtime/lib/driver-recovery.mjs +12 -2
  40. package/dist/harness-runtime/lib/lease-renewal.d.mts +8 -0
  41. package/dist/harness-runtime/lib/lease-renewal.mjs +23 -0
  42. package/dist/harness-runtime/lib/lifecycle-command.d.mts +3 -3
  43. package/dist/harness-runtime/lib/lifecycle-command.mjs +74 -20
  44. package/dist/harness-runtime/lib/managed-daemon.d.mts +11 -0
  45. package/dist/harness-runtime/lib/managed-daemon.mjs +321 -65
  46. package/dist/harness-runtime/lib/native-children.d.mts +13 -0
  47. package/dist/harness-runtime/lib/native-children.mjs +78 -0
  48. package/dist/harness-runtime/lib/native-hook-command.d.mts +4 -0
  49. package/dist/harness-runtime/lib/native-hook-command.mjs +115 -11
  50. package/dist/harness-runtime/lib/native-streams.mjs +45 -0
  51. package/dist/harness-runtime/lib/observation-clock.d.mts +18 -0
  52. package/dist/harness-runtime/lib/observation-clock.mjs +73 -17
  53. package/dist/harness-runtime/lib/operation-errors.d.mts +14 -0
  54. package/dist/harness-runtime/lib/operation-errors.mjs +105 -8
  55. package/dist/harness-runtime/lib/operation-progress.d.mts +7 -0
  56. package/dist/harness-runtime/lib/operation-progress.mjs +112 -0
  57. package/dist/harness-runtime/lib/process-json.mjs +136 -38
  58. package/dist/harness-runtime/lib/tool-observations.d.mts +1 -0
  59. package/dist/harness-runtime/lib/tool-observations.mjs +5 -2
  60. package/dist/harness-runtime/lib/zcode-usage.mjs +3 -2
  61. package/dist/runtime/context.js +8 -4
  62. package/dist/schemas/flow-run.schema.json +1 -0
  63. package/dist/schemas/plan-review-decision.schema.json +3 -3
  64. package/dist/schemas/run-control-receipt.schema.json +19 -0
  65. package/dist/services/code-checks.js +93 -27
  66. package/dist/services/codex-capacity.js +251 -74
  67. package/dist/services/codex-hook-delivery.js +2 -2
  68. package/dist/services/compatibility-preflight.js +4 -1
  69. package/dist/services/controller-fanout.js +9 -75
  70. package/dist/services/engines.js +70 -20
  71. package/dist/services/eval-snapshots.js +179 -40
  72. package/dist/services/external-work-launch.js +21 -9
  73. package/dist/services/harness-adapter.js +195 -25
  74. package/dist/services/hooks.js +65 -21
  75. package/dist/services/lifecycle-invocations.js +404 -32
  76. package/dist/services/managed-lease-monitor.js +128 -0
  77. package/dist/services/managed-processes.js +117 -53
  78. package/dist/services/merge-server.js +66 -23
  79. package/dist/services/recovery-observation-budget.js +67 -7
  80. package/dist/services/recovery-snapshot-database.js +7 -4
  81. package/dist/services/review-deferral.js +23 -0
  82. package/dist/services/run-control-receipt.js +1 -1
  83. package/dist/services/run-control-worker.js +177 -118
  84. package/dist/services/run-control.js +99 -8
  85. package/dist/services/run-controller-adapter.js +177 -51
  86. package/dist/services/run-controller-capture.js +14 -2
  87. package/dist/services/run-controller-process.js +11 -4
  88. package/dist/services/run-controller-recovery.js +17 -0
  89. package/dist/services/run-controller-state.js +68 -7
  90. package/dist/services/run-controller.js +207 -107
  91. package/dist/services/run-fork.js +8 -3
  92. package/dist/services/run-recovery-guard.js +75 -0
  93. package/dist/services/run-recovery-runtime.js +21 -30
  94. package/dist/services/run-recovery.js +17 -13
  95. package/dist/services/runs.js +40 -7
  96. package/dist/services/runtime-budget.js +32 -15
  97. package/dist/services/runtime-scope-control.js +8 -0
  98. package/dist/services/runtime-scope-resume.js +30 -8
  99. package/dist/services/runtime-scope-worker.js +118 -73
  100. package/dist/services/runtime-service.js +6 -7
  101. package/dist/services/scope-writer-cleanup.js +3 -2
  102. package/dist/services/stage-context.js +135 -0
  103. package/dist/services/stage-lifecycle.js +3 -15
  104. package/dist/services/stage-pause.js +3 -2
  105. package/dist/services/usage.js +14 -3
  106. package/dist/services/verification-matrix.js +30 -1
  107. package/dist/services/vnext-code-review.js +26 -13
  108. package/dist/services/vnext-code.js +10 -8
  109. package/dist/services/vnext-contracts.js +10 -1
  110. package/dist/services/vnext-execution-profile.js +75 -2
  111. package/dist/services/vnext-fanout.js +5 -0
  112. package/dist/services/vnext-merge.js +30 -11
  113. package/dist/services/vnext-plan-review.js +102 -35
  114. package/dist/services/vnext-plan.js +9 -7
  115. package/dist/services/vnext-protocolize.js +15 -9
  116. package/dist/services/vnext-specify.js +6 -6
  117. package/dist/services/work-registry.js +43 -6
  118. package/dist/services/workspace-bootstrap.js +14 -3
  119. package/dist/shared/errors.js +11 -6
  120. package/dist/storage/agent-output.js +26 -0
  121. package/dist/storage/database.js +80 -21
  122. package/package.json +7 -4
@@ -0,0 +1,128 @@
1
+ import { setTimeout as delay } from "node:timers/promises";
2
+ import { AppError } from "../shared/errors.js";
3
+ import { createRenewalState, maintenanceRetryable, renewalConfirmed, renewalDelay, renewalFailure, renewalRemaining, RENEWAL_POLICY } from "../harness-runtime/lib/lease-renewal.mjs";
4
+ /** Registry renewals happen outside RUN writers. Assertions never renew. */
5
+ export function managedLeaseMonitor(renew) {
6
+ const episode = createRenewalState();
7
+ let expiry = 0;
8
+ let failure = null;
9
+ let lost = false;
10
+ let fatal = null;
11
+ let closing = false;
12
+ let pending;
13
+ const shutdown = new AbortController();
14
+ const uncertain = () => new AppError("controller_lease_unconfirmed", "Managed lease requires confirmation outside the writer", 1, { cause: failure, exhausted: episode.exhausted, closing });
15
+ const assertNoRenewal = () => {
16
+ if (closing)
17
+ throw uncertain();
18
+ if (fatal)
19
+ throw fatal;
20
+ if (lost)
21
+ throw new AppError("controller_lease_lost", "Managed owner was replaced or retired", 1);
22
+ if (failure || Date.now() >= expiry)
23
+ throw uncertain();
24
+ };
25
+ const tick = () => {
26
+ if (closing)
27
+ throw uncertain();
28
+ if (fatal)
29
+ throw fatal;
30
+ if (lost || failure && !renewalRemaining(episode) || episode.exhausted)
31
+ return;
32
+ try {
33
+ episode.uncertainSince ??= performance.now();
34
+ const remaining = renewalRemaining(episode);
35
+ if (!remaining) {
36
+ failure = uncertain();
37
+ return;
38
+ }
39
+ const receipt = renew(Math.min(RENEWAL_POLICY.attemptMs, remaining));
40
+ if (!receipt.owned) {
41
+ lost = true;
42
+ return;
43
+ }
44
+ const value = Date.parse(receipt.lease_expires_at);
45
+ if (!Number.isFinite(value))
46
+ throw new AppError("managed_lease_receipt_invalid", "Renewal has no valid persisted expiry", 1);
47
+ if (value <= Date.now())
48
+ throw new AppError("controller_lease_unconfirmed", "Renewal receipt has already expired", 1);
49
+ if ((!failure || renewalRemaining(episode)) && renewalConfirmed(episode)) {
50
+ expiry = value;
51
+ failure = null;
52
+ }
53
+ else
54
+ failure = uncertain();
55
+ }
56
+ catch (error) {
57
+ failure = error;
58
+ if (maintenanceRetryable(error) || error instanceof AppError && error.code === "controller_lease_unconfirmed")
59
+ renewalFailure(episode);
60
+ else {
61
+ fatal = error;
62
+ throw error;
63
+ }
64
+ }
65
+ };
66
+ const assertConfirmed = () => { tick(); assertNoRenewal(); };
67
+ const runConfirmation = async (assertCurrent) => {
68
+ for (;;) {
69
+ if (closing)
70
+ throw uncertain();
71
+ assertCurrent();
72
+ tick();
73
+ try {
74
+ assertNoRenewal();
75
+ return;
76
+ }
77
+ catch (error) {
78
+ if (lost || episode.exhausted || !renewalRemaining(episode))
79
+ throw error;
80
+ try {
81
+ await delay(Math.min(renewalDelay(episode), renewalRemaining(episode)), undefined, { signal: shutdown.signal });
82
+ }
83
+ catch (cause) {
84
+ if (closing)
85
+ throw uncertain();
86
+ throw cause;
87
+ }
88
+ }
89
+ }
90
+ };
91
+ const ensureConfirmed = async (assertCurrent = () => { }) => {
92
+ if (closing)
93
+ throw uncertain();
94
+ if (pending)
95
+ assertCurrent();
96
+ pending ??= runConfirmation(assertCurrent).finally(() => { pending = undefined; });
97
+ await pending;
98
+ if (closing)
99
+ throw uncertain();
100
+ assertCurrent();
101
+ assertNoRenewal();
102
+ };
103
+ const background = (assertCurrent, onFailure, onConfirmed = () => { }) => {
104
+ void ensureConfirmed(assertCurrent).then(() => {
105
+ if (closing)
106
+ return;
107
+ try {
108
+ onConfirmed();
109
+ }
110
+ catch (error) {
111
+ fatal = failure = error;
112
+ throw error;
113
+ }
114
+ }).catch(error => {
115
+ if (closing)
116
+ return;
117
+ try {
118
+ onFailure(error);
119
+ }
120
+ catch (reporting) {
121
+ fatal = failure = reporting;
122
+ }
123
+ });
124
+ };
125
+ return { tick, assertNoRenewal, assertConfirmed, ensureConfirmed, background,
126
+ close: () => { closing = true; shutdown.abort(); },
127
+ isConfirmed: () => !closing && !lost && !failure && Date.now() < expiry };
128
+ }
@@ -4,10 +4,17 @@ import path from "node:path";
4
4
  import { spawnSync } from "node:child_process";
5
5
  import net from "node:net";
6
6
  import { getResourceDatabase } from "../storage/database.js";
7
- import { AppError } from "../shared/errors.js";
7
+ import { AppError, errorRecord } from "../shared/errors.js";
8
8
  export function managedProcessBinding(record, home) {
9
9
  return { resource_home: home, process_id: record.id, registration_sha256: crypto.createHash("sha256").update(JSON.stringify([home, record.id, record.kind, record.owner_id, record.lease_token, record.pid, record.pid_started_at, record.project_id, record.run_id, record.work_id, record.check_id, record.operation_id, record.metadata_json])).digest("hex") };
10
10
  }
11
+ export function assertManagedProcessAdmission(context, record) {
12
+ const expiry = Date.parse(record.lease_expires_at);
13
+ if (!Number.isFinite(expiry))
14
+ throw new AppError("runtime_artifact_invalid", "Managed lease expiry is malformed", 1, { process_id: record.id });
15
+ if (expiry <= Date.parse(context.now()))
16
+ throw new AppError("process_ownership_unknown", "Managed owner needs fresh renewal before admission", 1, { process_id: record.id, reason: "renewal_required", lease_expires_at: record.lease_expires_at });
17
+ }
11
18
  const defaultLeaseMs = 15 * 60_000;
12
19
  export function resourceHome(context) {
13
20
  return context.env?.DD_FLOW_RESOURCE_HOME ?? context.ddFlowHome ?? "/tmp/dd-flow-runtime";
@@ -53,8 +60,10 @@ export function prepareManagedProcessRegistration(context, input) {
53
60
  const observer = input.kind === "eval-observer";
54
61
  const observerBirth = observer && input.pid ? processStartedAt(input.pid) : null;
55
62
  if (observer) {
56
- if (!Number.isSafeInteger(input.pid) || !input.pid || input.pid < 1 || !observerBirth || input.ownerPid !== undefined && input.ownerPid !== input.pid || input.processGroupId !== undefined && input.processGroupId !== input.pid)
63
+ if (!Number.isSafeInteger(input.pid) || !input.pid || input.pid < 1 || input.ownerPid !== undefined && input.ownerPid !== input.pid || input.processGroupId !== undefined && input.processGroupId !== input.pid)
57
64
  throw new AppError("process_ownership_unknown", "EVAL observer registration requires its live self-owned process identity", 1);
65
+ if (!observerBirth)
66
+ throw new AppError("process_ownership_unknown", "EVAL observer physical identity observation is unavailable", 1, { process_id: input.id, reason: "physical_identity_unavailable", phase: "observer_registration", effect: "no_effect", native_dispatch_started: false });
58
67
  input = { ...input, ownerPid: input.pid, uniqueActiveOperation: true };
59
68
  }
60
69
  if (input.uniqueActiveOperation && !input.operationId)
@@ -71,7 +80,7 @@ export function prepareManagedProcessRegistration(context, input) {
71
80
  catch {
72
81
  throw new AppError("validation", "metadata must be JSON-serializable", 2);
73
82
  }
74
- const reader = getResourceDatabase(resourceHome(context), "read_existing");
83
+ const reader = getResourceDatabase(resourceHome(context), "read_existing", context.databaseDeadlineAt);
75
84
  try {
76
85
  if (input.id && reader.get("SELECT 1 FROM managed_processes WHERE id = ?", [input.id]))
77
86
  throw new AppError("validation", "Managed process ID already exists; use its retained registration", 2, { process_id: input.id });
@@ -89,11 +98,14 @@ export function prepareManagedProcessRegistration(context, input) {
89
98
  export function registerManagedProcess(context, rawInput) {
90
99
  const { input, observer, observerBirth, metadataJson } = prepareManagedProcessRegistration(context, rawInput);
91
100
  if (!input.uniqueActiveOperation)
92
- return insertManagedProcess(context, input, metadataJson);
93
- const db = registry(context);
101
+ return registry(context).writeTransaction(() => insertManagedProcess(context, input, metadataJson));
94
102
  const observerProcessBirth = observer ? stableProcessBirth(input.pid) : null;
103
+ const observerOwner = observer ? parseMetadata(metadataJson) : null;
104
+ if (observer && (!observerProcessBirth || !observerOwner?.owner_pid_started_at || !parseBirth(observerOwner.owner_birth)))
105
+ throw new AppError("process_ownership_unknown", "EVAL observer physical identity observation is unavailable", 1, { process_id: input.id, reason: "physical_identity_unavailable", phase: "observer_registration", effect: "no_effect", native_dispatch_started: false });
95
106
  if (observer && !processIsAlive({ pid: input.pid, pid_started_at: observerBirth, metadata_json: JSON.stringify({ process_birth: observerProcessBirth }) }))
96
107
  throw new AppError("process_ownership_unknown", "EVAL observer changed before registration", 1);
108
+ const db = registry(context);
97
109
  return db.writeTransaction(() => {
98
110
  const existing = db.get("SELECT * FROM managed_processes WHERE operation_id = ? AND state IN ('starting','running','stopping','orphaned')", [input.operationId]);
99
111
  if (existing)
@@ -133,29 +145,53 @@ export function prepareManagedProcessConfirmation(context, input) {
133
145
  const current = readManagedProcess(context, input.id);
134
146
  if (current.lease_token !== input.leaseToken)
135
147
  throw new AppError("process_lease_lost", "Managed process lease does not match", 1);
136
- if (current.state !== "starting")
137
- throw new AppError("process_confirmation_rejected", "Only a starting managed process can be confirmed", 1, { process_id: current.id, state: current.state });
148
+ if (current.state !== "starting" && !(current.state === "running" && current.pid === input.pid && processIsAlive(current) && (!input.processGroupId || parseMetadata(current.metadata_json).process_group_id === input.processGroupId) && (!input.ownerPid || parseMetadata(current.metadata_json).owner_pid === input.ownerPid)))
149
+ throw new AppError("process_confirmation_rejected", "Only the retained physical process may be confirmed", 1, { process_id: current.id, state: current.state });
138
150
  return current;
139
151
  }
140
152
  export function confirmManagedProcess(context, input) {
141
- const current = prepareManagedProcessConfirmation(context, input);
153
+ const prepared = prepareManagedProcessConfirmation(context, input);
154
+ const retainedIdentity = parseMetadata(prepared.metadata_json);
155
+ const retainedOwner = retainedIdentity.owner_pid;
156
+ const validRetainedOwner = typeof retainedOwner === "number" && Number.isSafeInteger(retainedOwner) && retainedOwner > 0;
157
+ const ownerUnknown = Object.hasOwn(retainedIdentity, "owner_pid") && (!validRetainedOwner || typeof retainedIdentity.owner_pid_started_at !== "string" || !retainedIdentity.owner_pid_started_at.trim() || Object.hasOwn(retainedIdentity, "owner_birth") && !parseBirth(retainedIdentity.owner_birth));
158
+ if (prepared.state === "running" && (!prepared.pid_started_at || Object.hasOwn(retainedIdentity, "process_birth") && !parseBirth(retainedIdentity.process_birth) || ownerUnknown) || input.ownerPid === undefined && Object.hasOwn(retainedIdentity, "owner_pid") && !validRetainedOwner)
159
+ throw new AppError("process_ownership_unknown", "Retained managed physical identity is unconfirmed", 1, { process_id: input.id, reason: "stored_identity_unconfirmed", phase: "process_confirmation", effect: "no_effect", native_dispatch_started: false });
160
+ // Initial confirmation must observe the registered owner too, even when a
161
+ // bootstrap caller omits its optional override. Running bindings never repair.
162
+ const ownerPid = input.ownerPid ?? (validRetainedOwner ? retainedOwner : undefined);
163
+ const pidStartedAt = processStartedAt(input.pid), processBirth = stableProcessBirth(input.pid);
164
+ const ownerStartedAt = ownerPid ? processStartedAt(ownerPid) : null, ownerBirth = ownerPid ? stableProcessBirth(ownerPid) : null;
165
+ if (!pidStartedAt || !processBirth || ownerPid && (!ownerStartedAt || !ownerBirth))
166
+ throw new AppError("process_ownership_unknown", "Managed physical identity observation is unavailable", 1, { process_id: input.id, reason: "physical_identity_unavailable", phase: "process_confirmation", effect: "no_effect", native_dispatch_started: false });
142
167
  const db = registry(context);
143
- const now = context.now();
144
- const metadata = parseMetadata(current.metadata_json);
145
- if (input.processGroupId)
146
- metadata.process_group_id = input.processGroupId;
147
- metadata.process_birth = stableProcessBirth(input.pid);
148
- if (input.ownerPid) {
149
- metadata.owner_pid = input.ownerPid;
150
- metadata.owner_pid_started_at = processStartedAt(input.ownerPid);
151
- metadata.owner_birth = stableProcessBirth(input.ownerPid);
152
- }
153
- const result = db.run(`UPDATE managed_processes
168
+ return db.writeTransaction(() => {
169
+ const current = requireProcess(db, input.id);
170
+ if (current.lease_token !== input.leaseToken || !["starting", "running"].includes(current.state))
171
+ throw new AppError("process_confirmation_rejected", "Managed process changed before confirmation", 1);
172
+ if (current.state === "running") {
173
+ if (prepared.state !== "running" || managedProcessBinding(current, resourceHome(context)).registration_sha256 !== managedProcessBinding(prepared, resourceHome(context)).registration_sha256 || current.pid !== input.pid || input.processGroupId && parseMetadata(current.metadata_json).process_group_id !== input.processGroupId || input.ownerPid && parseMetadata(current.metadata_json).owner_pid !== input.ownerPid)
174
+ throw new AppError("process_confirmation_rejected", "Confirmed process binding changed", 1);
175
+ return { ...current, registration_sha256: managedProcessBinding(current, resourceHome(context)).registration_sha256 };
176
+ }
177
+ const now = context.now();
178
+ const metadata = parseMetadata(current.metadata_json);
179
+ if (input.processGroupId)
180
+ metadata.process_group_id = input.processGroupId;
181
+ metadata.process_birth = processBirth;
182
+ if (ownerPid) {
183
+ metadata.owner_pid = ownerPid;
184
+ metadata.owner_pid_started_at = ownerStartedAt;
185
+ metadata.owner_birth = ownerBirth;
186
+ }
187
+ const result = db.run(`UPDATE managed_processes
154
188
  SET pid = ?, pid_started_at = ?, state = 'running', lease_expires_at = ?, updated_at = ?, metadata_json = ?
155
- WHERE id = ? AND lease_token = ? AND state = 'starting'`, [input.pid, processStartedAt(input.pid), leaseExpiry(now, input.leaseMs), now, JSON.stringify(metadata), input.id, input.leaseToken]);
156
- if (result.changes !== 1)
157
- throw new Error(`Managed process cannot be confirmed: ${input.id}`);
158
- return requireProcess(db, input.id);
189
+ WHERE id = ? AND lease_token = ? AND state = 'starting'`, [input.pid, pidStartedAt, leaseExpiry(now, input.leaseMs), now, JSON.stringify(metadata), input.id, input.leaseToken]);
190
+ if (result.changes !== 1)
191
+ throw new Error(`Managed process cannot be confirmed: ${input.id}`);
192
+ const confirmed = requireProcess(db, input.id);
193
+ return { ...confirmed, registration_sha256: managedProcessBinding(confirmed, resourceHome(context)).registration_sha256 };
194
+ });
159
195
  }
160
196
  export function prepareManagedProcessHeartbeat(context, input) {
161
197
  requireProcessText(input.id, "id");
@@ -163,28 +199,43 @@ export function prepareManagedProcessHeartbeat(context, input) {
163
199
  prepareManagedProcessLease(context, input.leaseMs);
164
200
  }
165
201
  export function heartbeatManagedProcess(context, input) {
202
+ return renewManagedProcess(context, input).owned;
203
+ }
204
+ export function renewManagedProcess(context, input) {
166
205
  prepareManagedProcessHeartbeat(context, input);
167
- const reader = getResourceDatabase(resourceHome(context), "read_existing");
206
+ if (context.resourceDatabaseMode === "prepared_existing")
207
+ registry(context);
208
+ const reader = getResourceDatabase(resourceHome(context), "read_existing", context.databaseDeadlineAt);
168
209
  try {
169
- if (!reader.get("SELECT 1 FROM managed_processes WHERE id = ? AND lease_token = ? AND state IN ('starting','running','stopping')", [input.id, input.leaseToken]))
170
- return false;
210
+ if (!reader.get("SELECT 1 FROM managed_processes WHERE id=? AND lease_token=? AND state IN ('starting','running','stopping')", [input.id, input.leaseToken]))
211
+ return { owned: false, process_id: input.id };
171
212
  }
172
213
  finally {
173
214
  reader.close?.();
174
215
  }
175
216
  const db = registry(context);
176
- return db.run("UPDATE managed_processes SET lease_expires_at = ?, updated_at = ? WHERE id = ? AND lease_token = ? AND state IN ('starting','running','stopping')", [leaseExpiry(context.now(), input.leaseMs), context.now(), input.id, input.leaseToken]).changes === 1;
217
+ return db.writeTransaction(() => {
218
+ const now = context.now();
219
+ if (!db.get("SELECT 1 FROM managed_processes WHERE id=? AND lease_token=? AND state IN ('starting','running','stopping')", [input.id, input.leaseToken]))
220
+ return { owned: false, process_id: input.id };
221
+ const changed = db.run("UPDATE managed_processes SET lease_expires_at = ?, updated_at = ? WHERE id = ? AND lease_token = ? AND state IN ('starting','running','stopping')", [leaseExpiry(now, input.leaseMs), now, input.id, input.leaseToken]).changes === 1;
222
+ if (!changed)
223
+ return { owned: false, process_id: input.id };
224
+ const record = requireProcess(db, input.id);
225
+ return { owned: true, process_id: record.id, lease_expires_at: record.lease_expires_at, registration_sha256: managedProcessBinding(record, resourceHome(context)).registration_sha256 };
226
+ });
177
227
  }
178
228
  /** Read-only lease check for a caller already holding a RUN writer. Renewal
179
229
  * must happen before entering that writer, never from this assertion. */
180
230
  export function assertManagedProcessLease(context, input) {
181
- const reader = getResourceDatabase(resourceHome(context), "read_isolated");
231
+ const reader = getResourceDatabase(resourceHome(context), "read_isolated", context.databaseDeadlineAt);
182
232
  try {
183
233
  const current = reader.get("SELECT pid, lease_expires_at, state FROM managed_processes WHERE id = ? AND lease_token = ?", [input.id, input.leaseToken]);
184
- if (!current || current.pid !== input.pid || !["starting", "running"].includes(current.state)
185
- || Date.parse(current.lease_expires_at) <= Date.now()) {
186
- throw new AppError("controller_lease_lost", "Managed owner lease changed or expired before productive dispatch", 1, { process_id: input.id });
234
+ if (!current || current.pid !== input.pid || !["starting", "running"].includes(current.state)) {
235
+ throw new AppError("controller_lease_lost", "Managed owner lease changed before productive dispatch", 1, { process_id: input.id });
187
236
  }
237
+ if (!Number.isFinite(Date.parse(current.lease_expires_at)) || Date.parse(current.lease_expires_at) <= Date.now())
238
+ throw new AppError("controller_lease_unconfirmed", "Managed owner requires renewal before productive dispatch", 1, { process_id: input.id, reason: "renewal_required" });
188
239
  }
189
240
  finally {
190
241
  reader.close?.();
@@ -200,21 +251,26 @@ export function prepareManagedProcessFinish(context, input) {
200
251
  return readManagedProcess(context, input.id);
201
252
  }
202
253
  export function finishManagedProcess(context, input) {
203
- const existing = prepareManagedProcessFinish(context, input);
204
- if (existing.lease_token === input.leaseToken && existing.state === input.state)
205
- return true;
206
- if (existing.lease_token !== input.leaseToken || !["starting", "running", "stopping", "orphaned"].includes(existing.state))
207
- return false;
254
+ prepareManagedProcessFinish(context, input);
208
255
  const db = registry(context);
209
- const now = context.now();
210
- const updated = db.run("UPDATE managed_processes SET state = ?, termination_reason = ?, finished_at = ?, updated_at = ?, lease_expires_at = ? WHERE id = ? AND lease_token = ? AND state IN ('starting','running','stopping','orphaned')", [input.state, input.reason ?? null, now, now, now, input.id, input.leaseToken]).changes === 1;
211
- if (updated)
212
- db.run("DELETE FROM managed_resources WHERE process_id = ?", [input.id]);
213
- if (updated)
214
- return true;
215
- // Another client may have completed the same lease after our initial read.
216
- const current = requireProcess(db, input.id);
217
- return current.lease_token === input.leaseToken && current.state === input.state;
256
+ return db.writeTransaction(() => {
257
+ const existing = requireProcess(db, input.id);
258
+ if (existing.lease_token !== input.leaseToken || existing.state !== input.state && !["starting", "running", "stopping", "orphaned"].includes(existing.state))
259
+ return false;
260
+ if (existing.state === input.state) {
261
+ db.run("DELETE FROM managed_resources WHERE process_id = ?", [input.id]);
262
+ return true;
263
+ }
264
+ const now = context.now();
265
+ const updated = db.run("UPDATE managed_processes SET state = ?, termination_reason = ?, finished_at = ?, updated_at = ?, lease_expires_at = ? WHERE id = ? AND lease_token = ? AND state IN ('starting','running','stopping','orphaned')", [input.state, input.reason ?? null, now, now, now, input.id, input.leaseToken]).changes === 1;
266
+ if (updated)
267
+ db.run("DELETE FROM managed_resources WHERE process_id = ?", [input.id]);
268
+ if (updated)
269
+ return true;
270
+ // Another client may have completed the same lease after our initial read.
271
+ const current = requireProcess(db, input.id);
272
+ return current.lease_token === input.leaseToken && current.state === input.state;
273
+ });
218
274
  }
219
275
  export function processIsAlive(record) {
220
276
  if (!record.pid)
@@ -300,7 +356,7 @@ export function managedLeaseReleased(record) {
300
356
  && ["stopped", "failed"].includes(record.state) && Boolean(record.finished_at);
301
357
  }
302
358
  export function managedProcessStatus(context) {
303
- const db = getResourceDatabase(resourceHome(context), "read_existing");
359
+ const db = getResourceDatabase(resourceHome(context), "read_existing", context.databaseDeadlineAt);
304
360
  try {
305
361
  return db.all("SELECT * FROM managed_processes ORDER BY updated_at DESC, id");
306
362
  }
@@ -315,7 +371,7 @@ export function prepareManagedProcessStopOperation(context, input) {
315
371
  return { record, graceMs };
316
372
  }
317
373
  function readManagedProcess(context, id) {
318
- const db = getResourceDatabase(resourceHome(context), "read_existing");
374
+ const db = getResourceDatabase(resourceHome(context), "read_existing", context.databaseDeadlineAt);
319
375
  try {
320
376
  return requireProcess(db, id);
321
377
  }
@@ -339,14 +395,19 @@ export async function stopManagedProcess(context, input) {
339
395
  return record;
340
396
  db.run("UPDATE managed_processes SET state = 'stopping', updated_at = ? WHERE id = ? AND lease_token = ? AND state IN ('starting','running','orphaned')", [context.now(), record.id, record.lease_token]);
341
397
  if (processTreeIsAlive(record)) {
342
- terminateOwnedProcess(record, "SIGTERM");
398
+ const signalErrors = [];
399
+ const termError = terminateOwnedProcess(record, "SIGTERM");
400
+ if (termError)
401
+ signalErrors.push(errorRecord(termError));
343
402
  await delay(graceMs);
344
- if (input.force !== false && processTreeIsAlive(record)) {
345
- terminateOwnedProcess(record, "SIGKILL");
403
+ if (!termError && input.force !== false && processTreeIsAlive(record)) {
404
+ const killError = terminateOwnedProcess(record, "SIGKILL");
405
+ if (killError)
406
+ signalErrors.push(errorRecord(killError));
346
407
  await delay(250);
347
408
  }
348
409
  if (processTreeIsAlive(record))
349
- throw Object.assign(new Error("Owned process tree could not be safely stopped; resources retained"), { code: "process_tree_not_settled", details: { process_id: record.id } });
410
+ throw Object.assign(new Error("Owned process tree could not be safely stopped; resources retained"), { code: "process_tree_not_settled", details: { process_id: record.id, signal_errors: signalErrors } });
350
411
  }
351
412
  if (!finishManagedProcess(context, { id: record.id, leaseToken: record.lease_token, state: "stopped", reason: input.reason ?? "requested_stop" }))
352
413
  throw Object.assign(new Error("Managed process ownership changed before settlement"), { code: "process_lease_lost" });
@@ -428,7 +489,7 @@ export async function reservePorts(context, input) {
428
489
  }
429
490
  return { ports, release: () => releasePortClaims(db, claims) };
430
491
  }
431
- function registry(context) { return getResourceDatabase(resourceHome(context)); }
492
+ function registry(context) { return getResourceDatabase(resourceHome(context), context.resourceDatabaseMode, context.databaseDeadlineAt); }
432
493
  function requireProcess(db, id) { const row = db.get("SELECT * FROM managed_processes WHERE id = ?", [id]); if (!row)
433
494
  throw new AppError("not_found", `Managed process is missing: ${id}`, 1); return row; }
434
495
  export function prepareManagedProcessLease(context, leaseMs) { return leaseExpiry(context.now(), leaseMs); }
@@ -460,7 +521,10 @@ function terminateOwnedProcess(record, signal) {
460
521
  try {
461
522
  process.kill(process.platform === "win32" || group !== record.pid ? record.pid : -group, signal);
462
523
  }
463
- catch { /* inspect again below */ }
524
+ catch (error) {
525
+ if (error.code !== "ESRCH")
526
+ return Object.assign(error, { details: { process_id: record.id, pid: record.pid, pgid: group ?? null, signal, phase: "owned_process_stop" } });
527
+ }
464
528
  }
465
529
  function parseMetadata(value) { try {
466
530
  const parsed = JSON.parse(value);
@@ -7,6 +7,7 @@ import fs from "node:fs";
7
7
  import path from "node:path";
8
8
  import { AppError } from "../shared/errors.js";
9
9
  import { runtimeBudgetForRun } from "./runtime-budget.js";
10
+ import { managedLeaseMonitor } from "./managed-lease-monitor.js";
10
11
  import { flowCommand } from "./stage-pause.js";
11
12
  import { adapterSessionId, prepareHarnessFlowExecutable, runHarnessAdapter } from "./harness-adapter.js";
12
13
  import { harnessRuntimeArguments, resolveHarnessCommand } from "./harness-config.js";
@@ -106,36 +107,73 @@ async function dispatch(context, input) {
106
107
  const executable = resolveHarnessCommand(context.ddFlowHome, input.profile.harness, "adapter");
107
108
  const common = ["--dd-flow-bin", flowExecutable, "--state-dir", stateDir, "--journal", adapterJournal, "--cwd", input.request.target_workspace, "--provider", input.profile.provider, "--model", input.profile.model, "--reasoning", input.profile.reasoning, "--mode", input.profile.mode, "--permission", input.profile.permission, "--project-root", run.project_root, "--dd-flow-home", context.ddFlowHome, ...harnessRuntimeArguments(context.ddFlowHome, input.profile.harness), "--json"];
108
109
  const retained = { profile: input.profile, executable, owner: runtimeOwner, session_id: null, state_dir: stateDir, create_operation_id: `${input.request.merge_request_id}:${token}:session.create`, prompt_operation_id: `${input.request.merge_request_id}:${token}:session.prompt` };
109
- const retain = () => { context.db.run("UPDATE merge_requests SET adapter_receipt_json = ?, updated_at = ? WHERE merge_request_id = ? AND dispatch_owner = ? AND dispatch_lease_token = ?", [JSON.stringify(retained), context.now(), input.request.merge_request_id, input.serverId, token]); };
110
+ const retain = () => { const changed = context.db.run("UPDATE merge_requests SET adapter_receipt_json = ?, updated_at = ? WHERE merge_request_id = ? AND dispatch_owner = ? AND dispatch_lease_token = ?", [JSON.stringify(retained), context.now(), input.request.merge_request_id, input.serverId, token]); if (changed.changes !== 1)
111
+ throw new AppError("capacity_continuation_blocked", "MERGE dispatch owner changed before durable receipt retention", 1); };
110
112
  retain();
111
- const leaseHeartbeat = setInterval(() => { if (guarded(context, input.request) || (recoveryGuard(context, input.request.project_id, input.request.run_id)?.generation ?? 0) !== generation)
112
- return; const now = context.now(); context.db.run("UPDATE merge_requests SET dispatch_lease_expires_at = ?, updated_at = ? WHERE merge_request_id = ? AND status IN ('dispatching','active') AND dispatch_owner = ? AND dispatch_lease_token = ?", [new Date(Date.now() + 120_000).toISOString(), now, input.request.merge_request_id, input.serverId, token]); }, 30_000);
113
+ const lease = managedLeaseMonitor(attemptMs => {
114
+ const db = context.db.withDeadline(Date.now() + attemptMs);
115
+ return db.writeTransaction(() => {
116
+ assertRunMutationAllowed({ ...context, db }, input.request.project_id, input.request.run_id, generation);
117
+ const expiry = new Date(Date.now() + 120_000).toISOString();
118
+ const changed = db.run("UPDATE merge_requests SET dispatch_lease_expires_at = ?, updated_at = ? WHERE merge_request_id = ? AND status IN ('dispatching','active') AND dispatch_owner = ? AND dispatch_lease_token = ?", [expiry, context.now(), input.request.merge_request_id, input.serverId, token]);
119
+ return changed.changes === 1 ? { owned: true, lease_expires_at: expiry } : { owned: false };
120
+ });
121
+ });
122
+ const ownership = new AbortController();
123
+ const leaseHeartbeat = setInterval(() => {
124
+ lease.background(() => assertRunMutationAllowed(context, input.request.project_id, input.request.run_id, generation), error => {
125
+ ownership.abort(error);
126
+ try {
127
+ event(input.journal, { type: "dispatch_lease_unconfirmed", request_id: input.request.merge_request_id, error: errorRecord(error), at: context.now() });
128
+ }
129
+ catch { /* No unhandled timer exception; the next boundary sees retained monitor failure. */ }
130
+ });
131
+ }, 30_000);
113
132
  leaseHeartbeat.unref();
114
133
  try {
115
- const adapterCall = (args, timeoutMs, progressMessage, id = `${input.request.merge_request_id}:${token}:${args[0]}.${args[1]}`) => { assertRunMutationAllowed(context, input.request.project_id, input.request.run_id, generation); return runHarnessAdapter({ executable, args, env: runtimeContext.env, runtimeOwner, operationId: id, ...(timeoutMs ? { timeoutMs } : {}), ...(input.progress ? { progress: input.progress } : {}), ...(progressMessage ? { progressMessage } : {}), onEvidence: (value) => event(input.journal, { type: "adapter_call", ...value }) }); };
134
+ const adapterCall = async (args, timeoutMs, progressMessage, id = `${input.request.merge_request_id}:${token}:${args[0]}.${args[1]}`) => {
135
+ await lease.ensureConfirmed(() => assertRunMutationAllowed(context, input.request.project_id, input.request.run_id, generation));
136
+ try {
137
+ return await runHarnessAdapter({ executable, args, harness: runtimeHarness(input.profile), env: runtimeContext.env, runtimeOwner, operationId: id, signal: ownership.signal, ...(timeoutMs !== undefined ? { timeoutMs } : {}), ...(input.progress ? { progress: input.progress } : {}), ...(progressMessage ? { progressMessage } : {}), onEvidence: (value) => event(input.journal, { type: "adapter_call", ...value }), onNativeOutcome: value => {
138
+ if (args[0] === "session" && args[1] === "create" && adapterSessionId(value)) {
139
+ Object.assign(retained, { session_id: adapterSessionId(value), create: value, settlement_pending: true });
140
+ retain();
141
+ }
142
+ } });
143
+ }
144
+ catch (error) {
145
+ if (ownership.signal.aborted)
146
+ throw ownership.signal.reason;
147
+ throw error;
148
+ }
149
+ };
116
150
  await adapterCall(["daemon", "start", ...common]);
117
- const created = await adapterCall(["session", "create", ...common], runtimeHarness(input.profile) === "zcode-acp" ? 180_000 : undefined);
151
+ const created = await adapterCall(["session", "create", ...common]);
118
152
  const sessionId = adapterSessionId(created);
119
153
  if (!sessionId)
120
154
  throw new AppError("merge_adapter_invalid", "Harness adapter did not return a Session id", 1, { harness: input.profile.harness, response: created });
121
- Object.assign(retained, { session_id: sessionId, create: created });
155
+ Object.assign(retained, { session_id: sessionId, create: created, settlement_pending: false });
122
156
  retain();
123
157
  runtimeContext = managedInvocationContext(runtimeContext, { harness: runtimeHarness(input.profile), stateDir, sessionId, projectRoot: run.project_root, runId: input.request.run_id, generation });
124
158
  const stageCommand = managedLifecycleCommand(runtimeContext, `${flowCommand(runtimeContext)} stage start ${input.request.run_id} --stage merge --project-root ${JSON.stringify(run.project_root)} --json --progress-jsonl`);
125
159
  fs.writeFileSync(promptFile, `Start the assigned MERGE stage now. Your first tool call must be this exact standalone command:\n\n${stageCommand}\n\nTrust and follow the complete stage packet it returns. Continue through merge apply and stage finish. Stop only when the stage reaches a terminal result or explicitly requests user/operator action.\n\n${stageLifecycleInstruction}\n`);
126
160
  let prompted;
127
- const promptDeadline = Date.now() + 45 * 60_000;
161
+ retained.progress_policy = "native-inactivity@1";
162
+ retain();
128
163
  try {
129
- prompted = await adapterCall(["session", "prompt", "--session-id", sessionId, "--prompt-file", promptFile, ...common], 45 * 60 * 1000, `MERGE ${input.request.merge_request_id} Session ${sessionId} is still running`);
164
+ prompted = await adapterCall(["session", "prompt", "--session-id", sessionId, "--prompt-file", promptFile, ...common], null, `MERGE ${input.request.merge_request_id} Session ${sessionId} is still running`);
130
165
  }
131
166
  catch (error) {
132
167
  if (runtimeHarness(input.profile) !== "codex-desktop")
133
168
  throw error;
134
169
  const capacityIntents = (retained.capacity_intents ?? []);
170
+ const capacityFailures = (retained.capacity_failures ?? []);
135
171
  const resumed = await continueCodexCapacity({ error, sessionId, operationId: String(retained.prompt_operation_id), allowSettledDescendants: true,
136
- deadline: promptDeadline,
172
+ signal: ownership.signal,
137
173
  continuationText: `Continue only MERGE ${input.request.merge_request_id}, Work ${input.request.executor_work_id}, RUN ${input.request.run_id} in this same Session and target ${input.request.target_workspace}. First inspect current durable Stage/Work/MERGE receipts. Never repeat Stage start, accepted answer, completed apply or checks. Follow only the current exact issued successor and stop at the original boundary.`,
138
174
  retainedIntents: () => capacityIntents,
175
+ retainedFailures: () => capacityFailures,
176
+ retainFailure: (_id, refusal) => { capacityFailures.push(refusal); retained.capacity_failures = capacityFailures; retain(); },
139
177
  inspectOperation: id => adapterCall(["daemon", "operation", "--state-dir", stateDir, "--operation-id", id, "--json"], 30_000, undefined, `${id}:capacity-observe`),
140
178
  inspectSession: turnId => adapterCall(["session", "inspect", "--session-id", sessionId, ...(turnId ? ["--turn-id", turnId] : []), ...common], 30_000, undefined, `${input.request.merge_request_id}:${token}:capacity-inspect`),
141
179
  assertCurrent: () => {
@@ -163,7 +201,7 @@ async function dispatch(context, input) {
163
201
  }
164
202
  else
165
203
  fs.writeFileSync(file, text, { flag: "wx", mode: 0o600 });
166
- return adapterCall(["session", "prompt", "--session-id", sessionId, "--prompt-file", file, ...common], Math.max(1, promptDeadline - Date.now()), `MERGE ${input.request.merge_request_id} Session ${sessionId} capacity continuation ${ordinal}/2`, id);
204
+ return adapterCall(["session", "prompt", "--session-id", sessionId, "--prompt-file", file, ...common], null, `MERGE ${input.request.merge_request_id} Session ${sessionId} capacity continuation ${ordinal}`, id);
167
205
  } });
168
206
  if (!resumed)
169
207
  throw error;
@@ -175,7 +213,7 @@ async function dispatch(context, input) {
175
213
  const request = context.db.get("SELECT status FROM merge_requests WHERE merge_request_id = ?", [input.request.merge_request_id]);
176
214
  event(input.journal, { type: "adapter_settled", request_id: input.request.merge_request_id, session_id: sessionId, request_status: request?.status ?? null, at: context.now() });
177
215
  if (request?.status === "dispatching")
178
- context.db.run("UPDATE merge_requests SET status = 'recovery_required', last_error_json = ?, updated_at = ? WHERE merge_request_id = ?", [JSON.stringify({ code: "adapter_did_not_start_stage", response: prompted }), context.now(), input.request.merge_request_id]);
216
+ context.db.run("UPDATE merge_requests SET status = 'recovery_required', last_error_json = ?, updated_at = ? WHERE merge_request_id = ? AND status = 'dispatching' AND dispatch_owner = ? AND dispatch_lease_token = ? AND ? = COALESCE((SELECT MAX(generation) FROM run_recovery_guards WHERE project_id = merge_requests.project_id AND run_id = merge_requests.run_id), 0)", [JSON.stringify({ code: "adapter_did_not_start_stage", response: prompted }), context.now(), input.request.merge_request_id, input.serverId, token, generation]);
179
217
  }
180
218
  catch (error) {
181
219
  if (error instanceof AppError && ["run_recovery_guarded", "run_recovery_generation_stale"].includes(error.code)) {
@@ -183,24 +221,29 @@ async function dispatch(context, input) {
183
221
  throw error;
184
222
  }
185
223
  const request = context.db.get("SELECT status FROM merge_requests WHERE merge_request_id = ?", [input.request.merge_request_id]);
186
- if (request?.status === "dispatching")
187
- context.db.run("UPDATE merge_requests SET status = 'recovery_required', last_error_json = ?, updated_at = ? WHERE merge_request_id = ?", [JSON.stringify({ code: "adapter_launch_failed", error: errorRecord(error) }), context.now(), input.request.merge_request_id]);
188
- else if (request?.status === "active")
189
- context.db.run("UPDATE merge_requests SET status = 'recovery_required', last_error_json = ?, updated_at = ? WHERE merge_request_id = ?", [JSON.stringify({ code: "adapter_lost_after_stage_start", error: errorRecord(error) }), context.now(), input.request.merge_request_id]);
224
+ if (request?.status === "dispatching" || request?.status === "active")
225
+ context.db.run("UPDATE merge_requests SET status = 'recovery_required', last_error_json = ?, updated_at = ? WHERE merge_request_id = ? AND status = ? AND dispatch_owner = ? AND dispatch_lease_token = ? AND ? = COALESCE((SELECT MAX(generation) FROM run_recovery_guards WHERE project_id = merge_requests.project_id AND run_id = merge_requests.run_id), 0)", [JSON.stringify({ code: request.status === "dispatching" ? "adapter_launch_failed" : "adapter_lost_after_stage_start", error: errorRecord(error) }), context.now(), input.request.merge_request_id, request.status, input.serverId, token, generation]);
190
226
  throw error;
191
227
  }
192
228
  finally {
229
+ lease.close();
193
230
  clearInterval(leaseHeartbeat);
194
231
  }
195
232
  }
196
233
  function nextServerRequests(context, limit) { const selected = new Set(); return context.db.all("SELECT merge_request_id, project_id, run_id, executor_work_id, target_workspace, execution_route, status, created_at FROM merge_requests WHERE status = 'queued' AND execution_route = 'server' ORDER BY created_at, merge_request_id").filter((request) => { if (guarded(context, request) || selected.has(request.project_id) || context.db.get("SELECT 1 FROM merge_requests WHERE project_id = ? AND status NOT IN ('completed','failed','cancelled','superseded') AND (created_at < ? OR (created_at = ? AND merge_request_id < ?)) LIMIT 1", [request.project_id, request.created_at, request.created_at, request.merge_request_id]))
197
234
  return false; selected.add(request.project_id); return true; }).slice(0, limit); }
198
- function reconcileExpiredDispatches(context) { const expired = context.db.all("SELECT merge_request_id, executor_work_id, project_id, run_id FROM merge_requests WHERE status = 'dispatching' AND dispatch_lease_expires_at < ?", [context.now()]); for (const request of expired) {
199
- if (guarded(context, request))
200
- continue;
201
- const work = context.db.get("SELECT status FROM works WHERE work_id = ?", [request.executor_work_id]);
202
- context.db.run("UPDATE merge_requests SET status = 'recovery_required', last_error_json = ?, updated_at = ? WHERE merge_request_id = ?", [JSON.stringify({ code: "expired_dispatch_outcome_unknown", work_status: work?.status ?? null }), context.now(), request.merge_request_id]);
203
- } }
235
+ export function reconcileExpiredDispatches(context) {
236
+ context.db.writeTransaction(() => {
237
+ const now = context.now();
238
+ const expired = context.db.all("SELECT merge_request_id, executor_work_id, project_id, run_id, dispatch_owner, dispatch_lease_token, dispatch_lease_expires_at FROM merge_requests WHERE status = 'dispatching' AND dispatch_lease_expires_at < ?", [now]);
239
+ for (const request of expired) {
240
+ if (guarded(context, request))
241
+ continue;
242
+ const work = context.db.get("SELECT status FROM works WHERE work_id = ?", [request.executor_work_id]);
243
+ context.db.run("UPDATE merge_requests SET status = 'recovery_required', last_error_json = ?, updated_at = ? WHERE merge_request_id = ? AND status = 'dispatching' AND dispatch_owner IS ? AND dispatch_lease_token IS ? AND dispatch_lease_expires_at = ? AND dispatch_lease_expires_at < ?", [JSON.stringify({ code: "expired_dispatch_outcome_unknown", work_status: work?.status ?? null }), now, request.merge_request_id, request.dispatch_owner, request.dispatch_lease_token, request.dispatch_lease_expires_at, now]);
244
+ }
245
+ });
246
+ }
204
247
  function loadProfile(context, id) { return loadAgentProfile(context.ddFlowHome, id); }
205
248
  function event(file, value) { fs.appendFileSync(file, `${JSON.stringify(value)}\n`); }
206
249
  function guarded(context, request) { const guard = recoveryGuard(context, request.project_id, request.run_id); return Boolean(guard && ["draining", "sealed", "resuming"].includes(guard.status)); }
@@ -210,6 +253,6 @@ function processAlive(pid) { if (!pid)
210
253
  process.kill(pid, 0);
211
254
  return true;
212
255
  }
213
- catch {
214
- return false;
256
+ catch (error) {
257
+ return error.code !== "ESRCH";
215
258
  } }