@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 +7 -0
- package/dist/build-info.json +3 -3
- package/dist/services/run-controller.js +49 -9
- package/package.json +1 -1
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
|
package/dist/build-info.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"cli_package": "@deksden-com/dd-flow-cli",
|
|
3
|
-
"cli_version": "0.9.0-beta.
|
|
4
|
-
"cli_commit": "
|
|
5
|
-
"built_at": "2026-09-
|
|
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
|
-
|
|
181
|
+
const lease = controllerLeaseMonitor(() => heartbeatManagedProcess(context, { id: row.process_id, leaseToken: row.process_lease_token }));
|
|
138
182
|
const heartbeat = setInterval(() => {
|
|
139
183
|
try {
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
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);
|