@deksden-com/dd-flow-cli 0.9.0-beta.103 → 0.9.0-beta.105
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/build-info.json +3 -3
- package/dist/cli/run-cli.js +32 -11
- package/dist/harness-runtime/bin/dd-agy.mjs +1 -1
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +2 -2
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +4 -3
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +2 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +8 -1
- package/dist/harness-runtime/lib/managed-daemon.mjs +11 -6
- package/dist/protocol/local-files.js +18 -9
- package/dist/runtime/context.js +2 -2
- package/dist/services/cleanup.js +81 -32
- package/dist/services/code-checks.js +18 -19
- package/dist/services/continuation-outcome.js +1 -1
- package/dist/services/controller-fanout.js +3 -3
- package/dist/services/eval-snapshots.js +69 -41
- package/dist/services/execution-policy.js +12 -1
- package/dist/services/hooks.js +150 -42
- package/dist/services/lanes.js +23 -18
- package/dist/services/lifecycle-command.js +11 -2
- package/dist/services/lifecycle-invocations.js +291 -54
- package/dist/services/managed-processes.js +35 -23
- package/dist/services/merge-queue.js +71 -67
- package/dist/services/merge-server.js +3 -11
- package/dist/services/migrations.js +28 -12
- package/dist/services/plan-runtime.js +8 -4
- package/dist/services/projects.js +3 -11
- package/dist/services/protocols.js +98 -68
- package/dist/services/recovery-snapshot-database.js +43 -8
- package/dist/services/run-control-worker.js +15 -32
- package/dist/services/run-control.js +3 -17
- package/dist/services/run-controller-capture.js +58 -27
- package/dist/services/run-controller-process.js +21 -30
- package/dist/services/run-controller-recovery.js +36 -37
- package/dist/services/run-controller.js +81 -86
- package/dist/services/run-fork.js +10 -6
- package/dist/services/run-projection.js +19 -64
- package/dist/services/run-recovery-runtime.js +24 -14
- package/dist/services/run-recovery.js +56 -54
- package/dist/services/runs.js +218 -160
- package/dist/services/runtime-budget.js +19 -45
- package/dist/services/runtime-scope-capture.js +8 -6
- package/dist/services/runtime-scope-control.js +49 -39
- package/dist/services/runtime-scope-resume.js +134 -122
- package/dist/services/runtime-scope-worker.js +48 -49
- package/dist/services/scope-writer-cleanup.js +26 -0
- package/dist/services/stage-context.js +6 -1
- package/dist/services/stage-lifecycle.js +1 -1
- package/dist/services/stage-pause.js +13 -29
- package/dist/services/stage-report-renderer.js +116 -6
- package/dist/services/usage.js +2 -9
- package/dist/services/vnext-code-review.js +10 -7
- package/dist/services/vnext-code.js +9 -17
- package/dist/services/vnext-merge.js +64 -21
- package/dist/services/vnext-plan-review.js +43 -30
- package/dist/services/vnext-plan.js +28 -24
- package/dist/services/vnext-protocolize.js +183 -20
- package/dist/services/vnext-specify.js +27 -19
- package/dist/services/work-registry.js +229 -150
- package/dist/storage/database.js +286 -56
- package/dist/storage/foreign-key-transaction.js +39 -0
- package/dist/storage/immutable-input.js +45 -0
- package/dist/storage/import-guard.js +102 -0
- package/dist/storage/native-cleanup.js +52 -0
- package/dist/storage/projection-lock.js +73 -0
- package/dist/storage/writer-contract.js +3 -3
- package/dist/storage/writer-migration.js +24 -41
- package/package.json +1 -1
- package/tools/repair-paused-run-status.mjs +48 -23
|
@@ -50,20 +50,14 @@ export async function waitForRuntimeScopeRecovery(context, input, assertOwner) {
|
|
|
50
50
|
const db = getResourceDatabase(resourceHome(context));
|
|
51
51
|
for (;;) {
|
|
52
52
|
assertOwner();
|
|
53
|
-
const snapshot = (() => {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
finally {
|
|
64
|
-
db.exec("RELEASE scope_recovery_release");
|
|
65
|
-
}
|
|
66
|
-
})();
|
|
53
|
+
const snapshot = db.readSnapshot(() => {
|
|
54
|
+
return {
|
|
55
|
+
fence: db.get("SELECT 1 FROM managed_resources WHERE resource_kind = 'provider-fence' AND resource_key = ?", [input.scopeId]),
|
|
56
|
+
latest: db.get("SELECT MAX(json_extract(metadata_json, '$.generation')) AS generation FROM managed_resources WHERE resource_kind = 'scope-control-request' AND owner_id = ?", [input.scopeId]),
|
|
57
|
+
control: db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control' AND resource_key = ?", [input.scopeId]),
|
|
58
|
+
release: db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-resume-release' AND resource_key = ?", [digest(JSON.stringify([input.scopeId, input.generation]))])
|
|
59
|
+
};
|
|
60
|
+
});
|
|
67
61
|
if (snapshot.fence)
|
|
68
62
|
throw new AppError("runtime_scope_stopped", "Terminal cancellation superseded scope recovery", 1);
|
|
69
63
|
const { latest, control: row, release } = snapshot;
|
|
@@ -118,12 +112,10 @@ export function requestRuntimeScopeControl(context, scopeId, requestId, mode, ma
|
|
|
118
112
|
const declaration = prepared.manifest;
|
|
119
113
|
const db = getResourceDatabase(resourceHome(context));
|
|
120
114
|
const requestKey = digest(JSON.stringify([scopeId, requestId]));
|
|
121
|
-
db.
|
|
122
|
-
try {
|
|
115
|
+
return db.writeTransaction(() => {
|
|
123
116
|
const prior = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control-request' AND resource_key = ?", [requestKey]);
|
|
124
117
|
if (prior) {
|
|
125
118
|
const receipt = scopeControlReceipt(prior.metadata_json, mode);
|
|
126
|
-
db.exec("COMMIT");
|
|
127
119
|
return receipt;
|
|
128
120
|
}
|
|
129
121
|
if (declaration !== undefined) {
|
|
@@ -149,13 +141,8 @@ export function requestRuntimeScopeControl(context, scopeId, requestId, mode, ma
|
|
|
149
141
|
const payload = JSON.stringify(receipt);
|
|
150
142
|
db.run("INSERT INTO managed_resources (resource_kind,resource_key,owner_id,lease_token,lease_expires_at,process_id,metadata_json,created_at,updated_at) VALUES ('scope-control-request',?,?,?,?,NULL,?,?,?)", [requestKey, scopeId, requestKey, now, payload, now, now]);
|
|
151
143
|
db.run("INSERT INTO managed_resources (resource_kind,resource_key,owner_id,lease_token,lease_expires_at,process_id,metadata_json,created_at,updated_at) VALUES ('scope-control',?,?,?,?,NULL,?,?,?) ON CONFLICT(resource_kind,resource_key) DO UPDATE SET lease_token = excluded.lease_token, metadata_json = excluded.metadata_json, updated_at = excluded.updated_at", [scopeId, scopeId, requestKey, now, payload, now, now]);
|
|
152
|
-
db.exec("COMMIT");
|
|
153
144
|
return receipt;
|
|
154
|
-
}
|
|
155
|
-
catch (error) {
|
|
156
|
-
db.exec("ROLLBACK");
|
|
157
|
-
throw error;
|
|
158
|
-
}
|
|
145
|
+
});
|
|
159
146
|
}
|
|
160
147
|
function assertScopeAdmission(context, scopeId) {
|
|
161
148
|
const db = getResourceDatabase(resourceHome(context));
|
|
@@ -245,21 +232,15 @@ export function prepareRuntimeScopeFence(scopeId, requestId) {
|
|
|
245
232
|
export function fenceRuntimeBudget(context, scopeId, requestId) {
|
|
246
233
|
prepareRuntimeScopeFence(scopeId, requestId);
|
|
247
234
|
const db = getResourceDatabase(resourceHome(context));
|
|
248
|
-
db.
|
|
249
|
-
try {
|
|
235
|
+
return db.writeTransaction(() => {
|
|
250
236
|
const existing = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'provider-fence' AND resource_key = ?", [scopeId]);
|
|
251
237
|
const now = context.now();
|
|
252
238
|
const receipt = existing ? JSON.parse(existing.metadata_json)
|
|
253
239
|
: { scope_id: scopeId, request_id: requestId, dispatch_blocked: true, fenced_at: now };
|
|
254
240
|
if (!existing)
|
|
255
241
|
db.run("INSERT INTO managed_resources (resource_kind,resource_key,owner_id,lease_token,lease_expires_at,process_id,metadata_json,created_at,updated_at) VALUES ('provider-fence',?,?,?,?,NULL,?,?,?)", [scopeId, scopeId, digest(requestId), now, JSON.stringify(receipt), now, now]);
|
|
256
|
-
db.exec("COMMIT");
|
|
257
242
|
return receipt;
|
|
258
|
-
}
|
|
259
|
-
catch (error) {
|
|
260
|
-
db.exec("ROLLBACK");
|
|
261
|
-
throw error;
|
|
262
|
-
}
|
|
243
|
+
});
|
|
263
244
|
}
|
|
264
245
|
export function runtimeBudgetForRun(context, projectId, runId) {
|
|
265
246
|
const retained = context.db.get("SELECT 1 FROM sqlite_schema WHERE name = 'run_controllers'")
|
|
@@ -304,16 +285,16 @@ export function reserveProviderTurn(context, record, input) {
|
|
|
304
285
|
const db = getResourceDatabase(resourceHome(context));
|
|
305
286
|
const key = digest(JSON.stringify([record.id, input.operationId]));
|
|
306
287
|
const policy = JSON.stringify(budget), now = context.now();
|
|
307
|
-
|
|
308
|
-
|
|
288
|
+
const recoveryOwnerAlive = input.operation === "session.resume" && processIsAlive(record);
|
|
289
|
+
return db.writeTransaction(() => {
|
|
309
290
|
const current = db.get("SELECT * FROM managed_processes WHERE id = ?", [record.id]);
|
|
310
|
-
if (current?.lease_token !== record.lease_token || current.state !== "running")
|
|
291
|
+
if (current?.lease_token !== record.lease_token || current.state !== "running" || current.lease_expires_at <= context.now() || managedProcessBinding(current, resourceHome(context)).registration_sha256 !== managedProcessBinding(record, resourceHome(context)).registration_sha256)
|
|
311
292
|
throw new AppError("process_ownership_unknown", "Budget owner changed before admission", 1);
|
|
312
293
|
try {
|
|
313
294
|
assertScopeAdmission(context, budget.scope_id);
|
|
314
295
|
}
|
|
315
296
|
catch (error) {
|
|
316
|
-
if (!(error instanceof AppError) || error.code !== "runtime_scope_controlled" || !scopeRecoveryLoadAllowed(context, budget.scope_id, record, input))
|
|
297
|
+
if (!(error instanceof AppError) || error.code !== "runtime_scope_controlled" || !scopeRecoveryLoadAllowed(context, budget.scope_id, record, input, recoveryOwnerAlive))
|
|
317
298
|
throw error;
|
|
318
299
|
}
|
|
319
300
|
const frozen = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'provider-budget' AND resource_key = ?", [budget.scope_id]);
|
|
@@ -325,7 +306,6 @@ export function reserveProviderTurn(context, record, input) {
|
|
|
325
306
|
if (prior) {
|
|
326
307
|
if (prior.lease_token !== record.lease_token)
|
|
327
308
|
throw new AppError("process_ownership_unknown", "Retained budget claim belongs to another physical owner", 1);
|
|
328
|
-
db.exec("COMMIT");
|
|
329
309
|
return { admitted: true, budget_scope: budget.scope_id, reused: true };
|
|
330
310
|
}
|
|
331
311
|
const activeTurns = db.all("SELECT process_id, metadata_json FROM managed_resources WHERE resource_kind = 'provider-turn' AND json_extract(metadata_json, '$.scope_id') = ? AND json_extract(metadata_json, '$.harness') = ? ORDER BY created_at, resource_key", [budget.scope_id, harness]);
|
|
@@ -335,21 +315,15 @@ export function reserveProviderTurn(context, record, input) {
|
|
|
335
315
|
const claim = JSON.parse(turn.metadata_json);
|
|
336
316
|
return { process_id: turn.process_id, operation_id: claim.operation_id ?? null, operation: claim.operation ?? null, session_id: claim.session_id ?? null };
|
|
337
317
|
});
|
|
338
|
-
db.exec("COMMIT");
|
|
339
318
|
return { admitted: false, reason: "provider_capacity", budget_scope: budget.scope_id, harness, limit, active, blocking_turns };
|
|
340
319
|
}
|
|
341
320
|
db.run("INSERT INTO managed_resources (resource_kind, resource_key, owner_id, lease_token, lease_expires_at, process_id, metadata_json, created_at, updated_at) VALUES ('provider-turn', ?, ?, ?, ?, ?, ?, ?, ?)", [key, record.owner_id, record.lease_token, record.lease_expires_at, record.id, JSON.stringify({ scope_id: budget.scope_id, harness, operation_id: input.operationId, operation: input.operation, session_id: input.sessionId ?? null, state_dir: metadata.state_dir }), now, now]);
|
|
342
|
-
db.exec("COMMIT");
|
|
343
321
|
return { admitted: true, budget_scope: budget.scope_id, harness, limit, active: active + 1 };
|
|
344
|
-
}
|
|
345
|
-
catch (error) {
|
|
346
|
-
db.exec("ROLLBACK");
|
|
347
|
-
throw error;
|
|
348
|
-
}
|
|
322
|
+
});
|
|
349
323
|
}
|
|
350
|
-
function scopeRecoveryLoadAllowed(context, scopeId, record, input) {
|
|
324
|
+
function scopeRecoveryLoadAllowed(context, scopeId, record, input, physicalOwnerAlive) {
|
|
351
325
|
const metadata = JSON.parse(record.metadata_json);
|
|
352
|
-
if (input.operation !== "session.resume" || !input.daemonId || !metadata.state_dir || metadata.dd_flow_home !== context.ddFlowHome || !
|
|
326
|
+
if (input.operation !== "session.resume" || !input.daemonId || !metadata.state_dir || metadata.dd_flow_home !== context.ddFlowHome || !physicalOwnerAlive)
|
|
353
327
|
return false;
|
|
354
328
|
const db = getResourceDatabase(resourceHome(context));
|
|
355
329
|
const control = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control' AND resource_key = ?", [scopeId]);
|
|
@@ -6,7 +6,7 @@ import { getResourceDatabase } from "../storage/database.js";
|
|
|
6
6
|
import { resourceHome } from "./managed-processes.js";
|
|
7
7
|
import { validateRetainedRuntimeScopeManifest } from "./runtime-budget.js";
|
|
8
8
|
/** Archive a complete append-only prefix, not a claim that no later evidence
|
|
9
|
-
* can arrive.
|
|
9
|
+
* can arrive. Prepare before taking the registry writer; never take the EVAL
|
|
10
10
|
* journal lock here (its writer may itself be waiting for the registry).
|
|
11
11
|
*/
|
|
12
12
|
export function captureScopeJournal(context, control, manifest) {
|
|
@@ -41,10 +41,12 @@ export function captureScopeJournal(context, control, manifest) {
|
|
|
41
41
|
const sha256 = hash(prefix), key = hash(JSON.stringify([control.scope_id, control.generation, manifest.manifest_sha256, sha256]));
|
|
42
42
|
const payload = JSON.stringify({ scope_id: control.scope_id, generation: control.generation, manifest, manifest_bytes: manifestBytes, journal_source: source, journal_bytes: text });
|
|
43
43
|
const registry = getResourceDatabase(resourceHome(context));
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
registry.writeTransaction(() => {
|
|
45
|
+
const prior = registry.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control-capture' AND resource_key = ?", [key]);
|
|
46
|
+
if (prior && prior.metadata_json !== payload)
|
|
47
|
+
throw new AppError("runtime_scope_capture_changed", "Retained EVAL capture no longer matches its content identity", 1);
|
|
48
|
+
if (!prior)
|
|
49
|
+
registry.run("INSERT INTO managed_resources (resource_kind,resource_key,owner_id,lease_token,lease_expires_at,process_id,metadata_json,created_at,updated_at) VALUES ('scope-control-capture',?,?,?,?,NULL,?,?,?)", [key, control.scope_id, key, context.now(), payload, context.now(), context.now()]);
|
|
50
|
+
});
|
|
49
51
|
return { artifact_key: key, source, sha256, byte_length: prefix.length, event_count: events.length, request_sequence: request.data.sequence, consistency: "append_only_prefix", trailing_bytes: bytes.length - prefix.length };
|
|
50
52
|
}
|
|
@@ -2,7 +2,7 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { createContext } from "../runtime/context.js";
|
|
5
|
-
import { getResourceDatabase } from "../storage/database.js";
|
|
5
|
+
import { getResourceDatabase, withDeferredPublications } from "../storage/database.js";
|
|
6
6
|
import { AppError } from "../shared/errors.js";
|
|
7
7
|
import { finishManagedProcess, managedLeaseReleased, managedProcessBinding, managedProcessStatus, processTreeIsAlive, resourceHome, stopManagedProcess } from "./managed-processes.js";
|
|
8
8
|
import { requestRuntimeScopeControl, runtimeBudgetStatus, scopeLocalProcessRole, validateRetainedRuntimeScopeManifest } from "./runtime-budget.js";
|
|
@@ -14,6 +14,7 @@ import { verifyRecoverySnapshot } from "./eval-snapshots.js";
|
|
|
14
14
|
import { captureScopeJournal } from "./runtime-scope-capture.js";
|
|
15
15
|
import { historicalDaemonSettlement, assertHistoricalDaemonSettlement } from "./native-daemon-history.js";
|
|
16
16
|
import { managedProviderBelongsToDaemon } from "./managed-daemon-binding.js";
|
|
17
|
+
import { cleanupScopeWriters } from "./scope-writer-cleanup.js";
|
|
17
18
|
const scopeInventoryIdentity = (records) => JSON.stringify(records.map(record => managedProcessBinding(record, "")).sort((a, b) => a.process_id.localeCompare(b.process_id)));
|
|
18
19
|
/** Persist the all-role drain boundary, separately from recovery readiness.
|
|
19
20
|
* Registry -> RUN lock order matches scope dispatch. No await/provider call is
|
|
@@ -22,21 +23,17 @@ const scopeInventoryIdentity = (records) => JSON.stringify(records.map(record =>
|
|
|
22
23
|
export function retainScopeDrain(context, control, processes, runs, native, probes) {
|
|
23
24
|
const registry = getResourceDatabase(resourceHome(context));
|
|
24
25
|
const targets = new Map();
|
|
26
|
+
const heldTargets = new Set();
|
|
27
|
+
const workspaces = new Map();
|
|
25
28
|
const captures = [];
|
|
26
|
-
|
|
29
|
+
let registryHeld = false, primaryFailure;
|
|
30
|
+
let committedReceipt;
|
|
27
31
|
try {
|
|
28
32
|
for (const run of [...runs].sort((a, b) => String(a.runtime_home).localeCompare(String(b.runtime_home)))) {
|
|
29
33
|
const home = String(run.runtime_home);
|
|
30
34
|
let target = targets.get(home);
|
|
31
35
|
if (!target) {
|
|
32
36
|
target = createContext({ ...context.env, DD_FLOW_HOME: home, DD_FLOW_RESOURCE_HOME: resourceHome(context), DD_FLOW_ENGINE_MODE: "1" });
|
|
33
|
-
try {
|
|
34
|
-
target.db.beginWriteTransaction();
|
|
35
|
-
}
|
|
36
|
-
catch (error) {
|
|
37
|
-
target.db.close?.();
|
|
38
|
-
throw error;
|
|
39
|
-
}
|
|
40
37
|
targets.set(home, target);
|
|
41
38
|
}
|
|
42
39
|
const project = target.db.get("SELECT root FROM projects WHERE id = ?", [run.project_id]);
|
|
@@ -51,6 +48,7 @@ export function retainScopeDrain(context, control, processes, runs, native, prob
|
|
|
51
48
|
throw new AppError("runtime_scope_inventory_changed", "RUN no longer proves its captured process binding", 1);
|
|
52
49
|
}
|
|
53
50
|
run.receipt = fresh;
|
|
51
|
+
workspaces.set(run, target.db.get("SELECT workspace_root FROM runs WHERE project_id = ? AND id = ?", [run.project_id, run.run_id]).workspace_root);
|
|
54
52
|
// Capture failure does not undo physical drain. Retain the failure beside
|
|
55
53
|
// the stop evidence, and keep resume fenced until capture is reconciled.
|
|
56
54
|
try {
|
|
@@ -104,22 +102,52 @@ export function retainScopeDrain(context, control, processes, runs, native, prob
|
|
|
104
102
|
const capture = { manifest, journal, run_captures: captures, native_bindings: resumableNative.map(owner => ({ process_id: owner.process_id, role: owner.role, state_dir: owner.state_dir, daemon_id: owner.daemon_id, runtime_owner: owner.runtime_owner, inventory_version: owner.inventory_version, sessions: owner.sessions, operations: owner.operations })), probe_operations: probes, pending_reasons: [...new Set([manifestReason, ...captures.filter(item => item.status !== "verified").map(item => String(item.reason)), ...(resumableNative.length ? ["non_work_recovery_required"] : []), ...(probes.length ? ["probe_reconciliation_required"] : [])])] };
|
|
105
103
|
const receipt = { scope_id: control.scope_id, generation: control.generation, requested_mode: control.requested_mode, observed_at: context.now(), physical_settled: true, recovery_ready: false, process_bindings: processes.map(record => managedProcessBinding(record, resourceHome(context))), runs, native, probes, capture };
|
|
106
104
|
const key = crypto.createHash("sha256").update(JSON.stringify([control.scope_id, control.generation])).digest("hex");
|
|
105
|
+
// Captures are immutable evidence, not release authority. Only this short
|
|
106
|
+
// registry -> RUN boundary publishes the all-role drain grant.
|
|
107
|
+
registry.beginWriteTransaction();
|
|
108
|
+
registryHeld = true;
|
|
109
|
+
for (const target of targets.values()) {
|
|
110
|
+
target.db.beginWriteTransaction();
|
|
111
|
+
heldTargets.add(target);
|
|
112
|
+
}
|
|
113
|
+
const current = runtimeBudgetStatus(context, control.scope_id);
|
|
114
|
+
if (current.fence || current.control?.generation !== control.generation
|
|
115
|
+
|| scopeInventoryIdentity(processes) !== scopeInventoryIdentity(current.processes) || current.provider_turns.length
|
|
116
|
+
|| JSON.stringify(current.manifest) !== JSON.stringify(latest.manifest))
|
|
117
|
+
throw new AppError("runtime_scope_inventory_changed", "Scope changed while its drain evidence was prepared", 1);
|
|
118
|
+
for (const run of runs) {
|
|
119
|
+
const target = targets.get(String(run.runtime_home));
|
|
120
|
+
const project = target.db.get("SELECT root FROM projects WHERE id = ?", [run.project_id]);
|
|
121
|
+
const saved = target.db.get("SELECT workspace_root FROM runs WHERE project_id = ? AND id = ?", [run.project_id, run.run_id]);
|
|
122
|
+
const previous = run.receipt;
|
|
123
|
+
if (!project || saved?.workspace_root !== workspaces.get(run)
|
|
124
|
+
|| JSON.stringify(runControlStatus(target, { projectRoot: project.root, runId: String(run.run_id), controlId: previous.control.control_id })) !== JSON.stringify(run.receipt))
|
|
125
|
+
throw new AppError("runtime_scope_inventory_changed", "RUN changed while its drain evidence was prepared", 1);
|
|
126
|
+
}
|
|
107
127
|
registry.run("INSERT INTO managed_resources (resource_kind,resource_key,owner_id,lease_token,lease_expires_at,process_id,metadata_json,created_at,updated_at) VALUES ('scope-control-drain',?,?,?,?,NULL,?,?,?) ON CONFLICT(resource_kind,resource_key) DO UPDATE SET metadata_json = excluded.metadata_json, updated_at = excluded.updated_at", [key, control.scope_id, key, context.now(), JSON.stringify(receipt), context.now(), context.now()]);
|
|
108
128
|
registry.exec("COMMIT");
|
|
129
|
+
registryHeld = false;
|
|
130
|
+
committedReceipt = receipt;
|
|
109
131
|
return receipt;
|
|
110
132
|
}
|
|
111
133
|
catch (error) {
|
|
112
|
-
|
|
134
|
+
primaryFailure = error;
|
|
113
135
|
throw error;
|
|
114
136
|
}
|
|
115
137
|
finally {
|
|
116
|
-
|
|
138
|
+
const errors = cleanupScopeWriters([
|
|
139
|
+
...[...targets.values()].reverse().map(target => ({ name: target.ddFlowHome, db: target.db, rollback: heldTargets.has(target), close: "always" })),
|
|
140
|
+
{ name: resourceHome(context), db: registry, rollback: registryHeld, close: "on_error" }
|
|
141
|
+
]);
|
|
142
|
+
if (errors.length && committedReceipt)
|
|
143
|
+
committedReceipt.cleanup_errors = errors;
|
|
144
|
+
else if (errors.length && primaryFailure instanceof AppError)
|
|
145
|
+
primaryFailure.details.scope_cleanup_errors = errors;
|
|
146
|
+
else if (errors.length) {
|
|
117
147
|
try {
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
finally {
|
|
121
|
-
target.db.close?.();
|
|
148
|
+
process.stderr.write(`${JSON.stringify({ phase: "scope_drain_cleanup", primary: String(primaryFailure), errors })}\n`);
|
|
122
149
|
}
|
|
150
|
+
catch { /* Cleanup must not replace the primary outcome. */ }
|
|
123
151
|
}
|
|
124
152
|
}
|
|
125
153
|
}
|
|
@@ -157,16 +185,10 @@ export async function controlRuntimeScopeLocalProbes(context, scopeId, generatio
|
|
|
157
185
|
if (mode === "pause" && processTreeIsAlive(current))
|
|
158
186
|
return { process_id: record.id, physical_settled: false, reason: "local_probe_running" };
|
|
159
187
|
const operationId = `scope-probe:${crypto.createHash("sha256").update(JSON.stringify([scopeId, generation, record.id, record.lease_token, record.pid_started_at])).digest("hex")}`;
|
|
160
|
-
registry.
|
|
161
|
-
try {
|
|
188
|
+
registry.writeTransaction(() => {
|
|
162
189
|
current = validate();
|
|
163
190
|
registry.run("INSERT OR IGNORE INTO managed_resources (resource_kind,resource_key,owner_id,lease_token,lease_expires_at,process_id,metadata_json,created_at,updated_at) VALUES ('scope-control-operation',?,?,?,?,NULL,?,?,?)", [operationId, scopeId, record.lease_token, context.now(), JSON.stringify({ scope_id: scopeId, generation, process_id: record.id, operation: mode === "stop" ? "probe.stop" : "probe.observe", status: "dispatching" }), context.now(), context.now()]);
|
|
164
|
-
|
|
165
|
-
}
|
|
166
|
-
catch (error) {
|
|
167
|
-
registry.exec("ROLLBACK");
|
|
168
|
-
throw error;
|
|
169
|
-
}
|
|
191
|
+
});
|
|
170
192
|
// A prior owner may have died after recording intent but before signalling.
|
|
171
193
|
// Repeating an identity-checked stop never redispatches productive work.
|
|
172
194
|
if (mode === "stop" && processTreeIsAlive(current))
|
|
@@ -240,16 +262,10 @@ export async function controlRuntimeScopeNativeOwners(context, scopeId, generati
|
|
|
240
262
|
const operationId = `scope-cancel:${crypto.createHash("sha256").update(JSON.stringify([scopeId, generation, record.id, state.daemon_id, id])).digest("hex")}`;
|
|
241
263
|
const now = context.now();
|
|
242
264
|
let inserted = false;
|
|
243
|
-
registry.
|
|
244
|
-
try {
|
|
265
|
+
registry.writeTransaction(() => {
|
|
245
266
|
assertOwner();
|
|
246
267
|
inserted = registry.run("INSERT OR IGNORE INTO managed_resources (resource_kind,resource_key,owner_id,lease_token,lease_expires_at,process_id,metadata_json,created_at,updated_at) VALUES ('scope-control-operation',?,?,?,?,NULL,?,?,?)", [operationId, scopeId, record.lease_token, now, JSON.stringify({ scope_id: scopeId, generation, process_id: record.id, daemon_id: state.daemon_id, session_id: id, status: "dispatching" }), now, now]).changes === 1;
|
|
247
|
-
|
|
248
|
-
}
|
|
249
|
-
catch (error) {
|
|
250
|
-
registry.exec("ROLLBACK");
|
|
251
|
-
throw error;
|
|
252
|
-
}
|
|
268
|
+
});
|
|
253
269
|
try {
|
|
254
270
|
const saved = JSON.parse(registry.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control-operation' AND resource_key = ?", [operationId]).metadata_json);
|
|
255
271
|
if (saved.status === "completed") {
|
|
@@ -331,8 +347,7 @@ export async function controlRuntimeScopeRuns(context, scopeId, requestId, mode,
|
|
|
331
347
|
let input = { projectRoot: project.root, runId: record.run_id, requestId: runRequestId, action: mode };
|
|
332
348
|
// Lock order is resource registry then RUN database. No provider or await
|
|
333
349
|
// occurs here; a newer scope request cannot interleave RUN intent creation.
|
|
334
|
-
registry.
|
|
335
|
-
try {
|
|
350
|
+
withDeferredPublications(() => registry.writeTransaction(() => {
|
|
336
351
|
const current = registry.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control' AND resource_key = ?", [scopeId]);
|
|
337
352
|
if (!current || JSON.parse(current.metadata_json).generation !== control.generation || registry.get("SELECT 1 FROM managed_resources WHERE resource_kind = 'provider-fence' AND resource_key = ?", [scopeId]))
|
|
338
353
|
throw new AppError("runtime_scope_control_superseded", "Scope request no longer owns admission", 1);
|
|
@@ -346,12 +361,7 @@ export async function controlRuntimeScopeRuns(context, scopeId, requestId, mode,
|
|
|
346
361
|
...(active.requested_mode === "stop" ? { force: active.request.force ?? false } : { graceMs: active.request.grace_ms ?? 30_000, ...(active.request.escalate ? { escalate: active.request.escalate } : {}) }) };
|
|
347
362
|
}
|
|
348
363
|
recordRunControlIntent(target, input);
|
|
349
|
-
|
|
350
|
-
}
|
|
351
|
-
catch (error) {
|
|
352
|
-
registry.exec("ROLLBACK");
|
|
353
|
-
throw error;
|
|
354
|
-
}
|
|
364
|
+
}));
|
|
355
365
|
seen.add(key);
|
|
356
366
|
const receipt = await requestRunControl(target, { ...input, waitMs: 0, ...(reconcileId ? { reconcileId: crypto.createHash("sha256").update(JSON.stringify([scopeId, control.generation, reconcileId])).digest("hex") } : {}) });
|
|
357
367
|
runs.push({ runtime_home: metadata.dd_flow_home, project_id: record.project_id, run_id: record.run_id, request_id: input.requestId, receipt });
|