@deksden-com/dd-flow-cli 0.9.0-beta.124 → 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 (113) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +10 -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/harness-runtime/bin/dd-agy.mjs +3 -2
  14. package/dist/harness-runtime/bin/dd-codex.mjs +3 -2
  15. package/dist/harness-runtime/bin/dd-droid.mjs +3 -2
  16. package/dist/harness-runtime/bin/dd-grok.mjs +16 -6
  17. package/dist/harness-runtime/bin/dd-opencode.mjs +3 -2
  18. package/dist/harness-runtime/bin/dd-zcode.mjs +20 -16
  19. package/dist/harness-runtime/lib/adapter-timeouts.d.mts +5 -0
  20. package/dist/harness-runtime/lib/adapter-timeouts.mjs +34 -0
  21. package/dist/harness-runtime/lib/daemon-observation.d.mts +2 -0
  22. package/dist/harness-runtime/lib/daemon-observation.mjs +163 -0
  23. package/dist/harness-runtime/lib/daemon-operations.mjs +60 -13
  24. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +151 -72
  25. package/dist/harness-runtime/lib/dd-agy.mjs +11 -23
  26. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +9 -14
  27. package/dist/harness-runtime/lib/dd-codex.mjs +252 -80
  28. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +14 -19
  29. package/dist/harness-runtime/lib/dd-droid.mjs +117 -31
  30. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +56 -30
  31. package/dist/harness-runtime/lib/dd-grok.mjs +15 -13
  32. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +18 -27
  33. package/dist/harness-runtime/lib/dd-opencode.mjs +44 -14
  34. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +56 -53
  35. package/dist/harness-runtime/lib/dd-zcode.mjs +200 -111
  36. package/dist/harness-runtime/lib/lease-renewal.d.mts +8 -0
  37. package/dist/harness-runtime/lib/lease-renewal.mjs +23 -0
  38. package/dist/harness-runtime/lib/lifecycle-command.d.mts +3 -3
  39. package/dist/harness-runtime/lib/lifecycle-command.mjs +74 -20
  40. package/dist/harness-runtime/lib/managed-daemon.d.mts +10 -1
  41. package/dist/harness-runtime/lib/managed-daemon.mjs +228 -40
  42. package/dist/harness-runtime/lib/native-children.d.mts +13 -0
  43. package/dist/harness-runtime/lib/native-children.mjs +78 -0
  44. package/dist/harness-runtime/lib/native-hook-command.d.mts +4 -0
  45. package/dist/harness-runtime/lib/native-hook-command.mjs +115 -11
  46. package/dist/harness-runtime/lib/native-streams.mjs +45 -0
  47. package/dist/harness-runtime/lib/observation-clock.d.mts +18 -0
  48. package/dist/harness-runtime/lib/observation-clock.mjs +73 -17
  49. package/dist/harness-runtime/lib/operation-errors.d.mts +14 -0
  50. package/dist/harness-runtime/lib/operation-errors.mjs +105 -8
  51. package/dist/harness-runtime/lib/operation-progress.d.mts +7 -0
  52. package/dist/harness-runtime/lib/operation-progress.mjs +112 -0
  53. package/dist/harness-runtime/lib/process-json.mjs +136 -38
  54. package/dist/harness-runtime/lib/tool-observations.d.mts +1 -0
  55. package/dist/harness-runtime/lib/tool-observations.mjs +5 -2
  56. package/dist/harness-runtime/lib/zcode-usage.mjs +3 -2
  57. package/dist/runtime/context.js +8 -4
  58. package/dist/schemas/run-control-receipt.schema.json +19 -0
  59. package/dist/services/code-checks.js +79 -9
  60. package/dist/services/codex-capacity.js +117 -41
  61. package/dist/services/codex-hook-delivery.js +2 -2
  62. package/dist/services/compatibility-preflight.js +4 -1
  63. package/dist/services/controller-fanout.js +9 -76
  64. package/dist/services/engines.js +70 -20
  65. package/dist/services/eval-snapshots.js +179 -40
  66. package/dist/services/external-work-launch.js +13 -8
  67. package/dist/services/harness-adapter.js +175 -11
  68. package/dist/services/hooks.js +65 -21
  69. package/dist/services/lifecycle-invocations.js +404 -32
  70. package/dist/services/managed-lease-monitor.js +128 -0
  71. package/dist/services/managed-processes.js +103 -47
  72. package/dist/services/merge-server.js +58 -21
  73. package/dist/services/recovery-observation-budget.js +67 -7
  74. package/dist/services/recovery-snapshot-database.js +7 -4
  75. package/dist/services/run-control-receipt.js +1 -1
  76. package/dist/services/run-control-worker.js +177 -118
  77. package/dist/services/run-control.js +99 -8
  78. package/dist/services/run-controller-adapter.js +169 -46
  79. package/dist/services/run-controller-capture.js +14 -2
  80. package/dist/services/run-controller-process.js +11 -4
  81. package/dist/services/run-controller-state.js +68 -7
  82. package/dist/services/run-controller.js +166 -94
  83. package/dist/services/run-fork.js +8 -3
  84. package/dist/services/run-recovery-guard.js +75 -0
  85. package/dist/services/run-recovery-runtime.js +1 -56
  86. package/dist/services/run-recovery.js +17 -13
  87. package/dist/services/runs.js +39 -6
  88. package/dist/services/runtime-budget.js +32 -15
  89. package/dist/services/runtime-scope-control.js +8 -0
  90. package/dist/services/runtime-scope-resume.js +30 -8
  91. package/dist/services/runtime-scope-worker.js +118 -73
  92. package/dist/services/runtime-service.js +6 -7
  93. package/dist/services/scope-writer-cleanup.js +3 -2
  94. package/dist/services/stage-context.js +135 -0
  95. package/dist/services/stage-lifecycle.js +3 -15
  96. package/dist/services/stage-pause.js +3 -2
  97. package/dist/services/usage.js +14 -3
  98. package/dist/services/vnext-code-review.js +10 -9
  99. package/dist/services/vnext-code.js +10 -8
  100. package/dist/services/vnext-contracts.js +10 -1
  101. package/dist/services/vnext-execution-profile.js +75 -2
  102. package/dist/services/vnext-fanout.js +5 -0
  103. package/dist/services/vnext-merge.js +10 -9
  104. package/dist/services/vnext-plan-review.js +21 -17
  105. package/dist/services/vnext-plan.js +9 -7
  106. package/dist/services/vnext-protocolize.js +15 -9
  107. package/dist/services/vnext-specify.js +6 -6
  108. package/dist/services/work-registry.js +34 -4
  109. package/dist/services/workspace-bootstrap.js +14 -3
  110. package/dist/shared/errors.js +11 -6
  111. package/dist/storage/agent-output.js +26 -0
  112. package/dist/storage/database.js +80 -21
  113. 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
+ }
@@ -8,6 +8,13 @@ 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
  }
@@ -433,7 +489,7 @@ export async function reservePorts(context, input) {
433
489
  }
434
490
  return { ports, release: () => releasePortClaims(db, claims) };
435
491
  }
436
- function registry(context) { return getResourceDatabase(resourceHome(context)); }
492
+ function registry(context) { return getResourceDatabase(resourceHome(context), context.resourceDatabaseMode, context.databaseDeadlineAt); }
437
493
  function requireProcess(db, id) { const row = db.get("SELECT * FROM managed_processes WHERE id = ?", [id]); if (!row)
438
494
  throw new AppError("not_found", `Managed process is missing: ${id}`, 1); return row; }
439
495
  export function prepareManagedProcessLease(context, leaseMs) { return leaseExpiry(context.now(), leaseMs); }
@@ -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";
@@ -109,27 +110,58 @@ async function dispatch(context, input) {
109
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)
110
111
  throw new AppError("capacity_continuation_blocked", "MERGE dispatch owner changed before durable receipt retention", 1); };
111
112
  retain();
112
- const leaseHeartbeat = setInterval(() => { if (guarded(context, input.request) || (recoveryGuard(context, input.request.project_id, input.request.run_id)?.generation ?? 0) !== generation)
113
- 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);
114
132
  leaseHeartbeat.unref();
115
133
  try {
116
- 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
+ };
117
150
  await adapterCall(["daemon", "start", ...common]);
118
- const created = await adapterCall(["session", "create", ...common], runtimeHarness(input.profile) === "zcode-acp" ? 180_000 : undefined);
151
+ const created = await adapterCall(["session", "create", ...common]);
119
152
  const sessionId = adapterSessionId(created);
120
153
  if (!sessionId)
121
154
  throw new AppError("merge_adapter_invalid", "Harness adapter did not return a Session id", 1, { harness: input.profile.harness, response: created });
122
- Object.assign(retained, { session_id: sessionId, create: created });
155
+ Object.assign(retained, { session_id: sessionId, create: created, settlement_pending: false });
123
156
  retain();
124
157
  runtimeContext = managedInvocationContext(runtimeContext, { harness: runtimeHarness(input.profile), stateDir, sessionId, projectRoot: run.project_root, runId: input.request.run_id, generation });
125
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`);
126
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`);
127
160
  let prompted;
128
- const promptDeadline = Date.now() + 45 * 60_000;
129
- retained.prompt_deadline = promptDeadline;
161
+ retained.progress_policy = "native-inactivity@1";
130
162
  retain();
131
163
  try {
132
- 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`);
133
165
  }
134
166
  catch (error) {
135
167
  if (runtimeHarness(input.profile) !== "codex-desktop")
@@ -137,7 +169,7 @@ async function dispatch(context, input) {
137
169
  const capacityIntents = (retained.capacity_intents ?? []);
138
170
  const capacityFailures = (retained.capacity_failures ?? []);
139
171
  const resumed = await continueCodexCapacity({ error, sessionId, operationId: String(retained.prompt_operation_id), allowSettledDescendants: true,
140
- deadline: promptDeadline,
172
+ signal: ownership.signal,
141
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.`,
142
174
  retainedIntents: () => capacityIntents,
143
175
  retainedFailures: () => capacityFailures,
@@ -169,7 +201,7 @@ async function dispatch(context, input) {
169
201
  }
170
202
  else
171
203
  fs.writeFileSync(file, text, { flag: "wx", mode: 0o600 });
172
- 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}`, 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);
173
205
  } });
174
206
  if (!resumed)
175
207
  throw error;
@@ -181,7 +213,7 @@ async function dispatch(context, input) {
181
213
  const request = context.db.get("SELECT status FROM merge_requests WHERE merge_request_id = ?", [input.request.merge_request_id]);
182
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() });
183
215
  if (request?.status === "dispatching")
184
- 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]);
185
217
  }
186
218
  catch (error) {
187
219
  if (error instanceof AppError && ["run_recovery_guarded", "run_recovery_generation_stale"].includes(error.code)) {
@@ -189,24 +221,29 @@ async function dispatch(context, input) {
189
221
  throw error;
190
222
  }
191
223
  const request = context.db.get("SELECT status FROM merge_requests WHERE merge_request_id = ?", [input.request.merge_request_id]);
192
- if (request?.status === "dispatching")
193
- 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]);
194
- else if (request?.status === "active")
195
- 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]);
196
226
  throw error;
197
227
  }
198
228
  finally {
229
+ lease.close();
199
230
  clearInterval(leaseHeartbeat);
200
231
  }
201
232
  }
202
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]))
203
234
  return false; selected.add(request.project_id); return true; }).slice(0, limit); }
204
- 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) {
205
- if (guarded(context, request))
206
- continue;
207
- const work = context.db.get("SELECT status FROM works WHERE work_id = ?", [request.executor_work_id]);
208
- 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]);
209
- } }
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
+ }
210
247
  function loadProfile(context, id) { return loadAgentProfile(context.ddFlowHome, id); }
211
248
  function event(file, value) { fs.appendFileSync(file, `${JSON.stringify(value)}\n`); }
212
249
  function guarded(context, request) { const guard = recoveryGuard(context, request.project_id, request.run_id); return Boolean(guard && ["draining", "sealed", "resuming"].includes(guard.status)); }