@deksden-com/dd-flow-cli 0.9.0-beta.47 → 0.9.0-beta.48

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # @deksden-com/dd-flow-cli
2
2
 
3
+ ## 0.9.0-beta.48
4
+
5
+ ### Patch Changes
6
+
7
+ - Recheck a controller lease after a transient runtime-registry write failure before
8
+ dispatching more work, while retaining the hard stop for an explicit lease loss.
9
+
3
10
  ## 0.9.0-beta.47
4
11
 
5
12
  ### Patch Changes
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "cli_package": "@deksden-com/dd-flow-cli",
3
- "cli_version": "0.9.0-beta.47",
4
- "cli_commit": "c6ad5bc48196d4f9a616667b737363c04d837e4e",
5
- "built_at": "2026-09-10T20:41:49.668Z",
3
+ "cli_version": "0.9.0-beta.48",
4
+ "cli_commit": "ee8c219ce0fbc3943b6d3b1fcc5463c190469aaa",
5
+ "built_at": "2026-09-11T05:39:36.931Z",
6
6
  "built_with_canon": {
7
7
  "version": "4.1.0",
8
8
  "commit": "ef349bf47cba1c987468e51d73a0dbadbd48dc1f",
@@ -111,6 +111,50 @@ export async function launchRunController(context, input) {
111
111
  }
112
112
  return await spawnController(context, requireController(context, controllerId));
113
113
  }
114
+ /**
115
+ * A registry write can be temporarily unavailable while another runtime client
116
+ * owns SQLite's writer lock. That is not evidence that this controller lost
117
+ * its lease. Productive dispatch still requires a successful recheck before
118
+ * proceeding, while an explicit negative renewal remains a hard ownership
119
+ * loss.
120
+ */
121
+ export function controllerLeaseMonitor(heartbeat) {
122
+ let lost = false;
123
+ let unconfirmed = null;
124
+ const lostError = () => new AppError("controller_lease_lost", "Managed owner lease could not be confirmed", 1);
125
+ return {
126
+ tick() {
127
+ try {
128
+ if (!heartbeat())
129
+ lost = true;
130
+ else
131
+ unconfirmed = null;
132
+ }
133
+ catch (error) {
134
+ unconfirmed = error;
135
+ }
136
+ },
137
+ isConfirmed() { return !lost && unconfirmed === null; },
138
+ assertConfirmed() {
139
+ if (lost)
140
+ throw lostError();
141
+ if (unconfirmed === null)
142
+ return;
143
+ try {
144
+ if (!heartbeat()) {
145
+ lost = true;
146
+ throw lostError();
147
+ }
148
+ unconfirmed = null;
149
+ }
150
+ catch (error) {
151
+ if (error instanceof AppError)
152
+ throw error;
153
+ throw new AppError("controller_lease_unconfirmed", "Managed owner lease could not be refreshed", 1, { cause: error instanceof Error ? error.message : String(error) });
154
+ }
155
+ }
156
+ };
157
+ }
114
158
  /** No initialization, provider calls or lease takeover on the observation path. */
115
159
  export function runControllerStatus(context, input) {
116
160
  if (input.after !== undefined && (!Number.isSafeInteger(input.after) || input.after < 0))
@@ -134,27 +178,23 @@ export async function serveRunController(context, input) {
134
178
  if (!row.process_id || !row.process_lease_token)
135
179
  throw new AppError("controller_owner_mismatch", "Managed owner has no resource lease", 1);
136
180
  confirmManagedProcess(context, { id: row.process_id, leaseToken: row.process_lease_token, pid: process.pid, processGroupId: process.pid, ownerPid: process.pid });
137
- let leaseLost = false;
181
+ const lease = controllerLeaseMonitor(() => heartbeatManagedProcess(context, { id: row.process_id, leaseToken: row.process_lease_token }));
138
182
  const heartbeat = setInterval(() => {
139
183
  try {
140
- if (!heartbeatManagedProcess(context, { id: row.process_id, leaseToken: row.process_lease_token }))
141
- leaseLost = true;
142
- if (!leaseLost) {
184
+ lease.tick();
185
+ if (lease.isConfirmed()) {
143
186
  const guard = recoveryGuard(context, row.project_id, row.run_id);
144
187
  if ((guard?.generation ?? 0) === row.generation && !["draining", "sealed", "resuming"].includes(guard?.status ?? ""))
145
188
  context.db.run("UPDATE merge_requests SET dispatch_lease_expires_at = ?, updated_at = ? WHERE project_id = ? AND run_id = ? AND dispatch_owner = ? AND status = 'dispatching'", [new Date(Date.now() + 120_000).toISOString(), context.now(), row.project_id, row.run_id, row.controller_id]);
146
189
  }
147
190
  }
148
- catch {
149
- leaseLost = true;
150
- }
191
+ catch { /* The lease monitor distinguishes renewal uncertainty from loss. */ }
151
192
  }, 30_000);
152
193
  heartbeat.unref();
153
194
  const manifest = JSON.parse(row.manifest_json);
154
195
  const state = JSON.parse(row.state_json);
155
196
  const assertPhysicalOwner = () => {
156
- if (leaseLost)
157
- throw new AppError("controller_lease_lost", "Managed owner lease could not be confirmed", 1);
197
+ lease.assertConfirmed();
158
198
  const current = requireController(context, row.controller_id);
159
199
  if (current.owner_pid !== process.pid || current.owner_token !== row.owner_token || !["running", "capturing", "waiting_for_user", "waiting_for_context", "waiting_for_children", "waiting_for_capacity", "waiting_for_merge"].includes(current.status))
160
200
  throw new AppError("controller_owner_mismatch", "Controller no longer owns productive dispatch", 1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deksden-com/dd-flow-cli",
3
- "version": "0.9.0-beta.47",
3
+ "version": "0.9.0-beta.48",
4
4
  "description": "Mechanical runtime CLI for dd-flow workflows.",
5
5
  "type": "module",
6
6
  "bin": {