@deksden-com/dd-flow-cli 0.9.0-beta.139 → 0.9.0-beta.140
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 +13 -0
- package/dist/build-info.json +3 -3
- package/dist/cli/run-cli.js +2 -0
- package/dist/runtime/context.js +1 -1
- package/dist/services/managed-processes.js +25 -2
- package/dist/services/native-daemon-history.js +2 -2
- package/dist/services/runtime-scope-stop.js +34 -4
- package/dist/storage/database.js +22 -11
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @deksden-com/dd-flow-cli
|
|
2
2
|
|
|
3
|
+
## 0.9.0-beta.140
|
|
4
|
+
|
|
5
|
+
- Compare observable birth after EPERM so a foreign reused PID is not mistaken for the retained owner; unknown identity remains conservative.
|
|
6
|
+
|
|
7
|
+
- Separate pre-reboot physical retirement of controllers and waiting probes from their retained logical outcome.
|
|
8
|
+
|
|
9
|
+
- Preserve legacy resource writer contracts during terminal retirement and recognize retained resource aliases without changing their historical bytes.
|
|
10
|
+
- Retire structurally validated registries predating writer contracts without installing a new schema or fence definition.
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- Retire validated pre-reboot legacy daemon processes without claiming native Session success or signalling reused PIDs.
|
|
15
|
+
|
|
3
16
|
## 0.9.0-beta.139
|
|
4
17
|
|
|
5
18
|
### 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-10-
|
|
3
|
+
"cli_version": "0.9.0-beta.140",
|
|
4
|
+
"cli_commit": "995243962934773092d9d2a127b0e1224baa622d",
|
|
5
|
+
"built_at": "2026-10-17T10:10:08.823Z",
|
|
6
6
|
"built_with_canon": {
|
|
7
7
|
"version": "4.1.2",
|
|
8
8
|
"commit": "1ddedf9d3f086e046f8e7b194c739ec386e7c8b9",
|
package/dist/cli/run-cli.js
CHANGED
|
@@ -232,6 +232,8 @@ async function runCliRequest(args, io, env) {
|
|
|
232
232
|
// Registration may bootstrap the resource registry, but never the unrelated
|
|
233
233
|
// RUN store. Its caller's absolute deadline also bounds that first open.
|
|
234
234
|
const maintenanceOptions = isRuntimeMaintenance(output.args) ? { resourceDatabaseMode: output.args[2] === "register" ? "initialize" : output.args[2] === "status" ? "read_existing" : "prepared_existing", ...(env.DD_FLOW_HOOK_DEADLINE_AT ? { databaseDeadlineAt: Number(env.DD_FLOW_HOOK_DEADLINE_AT) } : {}) } : {};
|
|
235
|
+
if (output.args[0] === "runtime" && output.args[1] === "scope" && ["fence", "stop"].includes(output.args[2] ?? ""))
|
|
236
|
+
maintenanceOptions.resourceDatabaseMode = "retirement";
|
|
235
237
|
// Keep ordinary CLI JSON/stderr compatibility; bounded maintenance callers
|
|
236
238
|
// provide this request's deadline and consume its structured phase evidence.
|
|
237
239
|
if (maintenanceOptions.databaseDeadlineAt !== undefined)
|
package/dist/runtime/context.js
CHANGED
|
@@ -11,7 +11,7 @@ export function createRouterContext(env, deadlineAt) {
|
|
|
11
11
|
}
|
|
12
12
|
export function createContext(env, mode = "initialize", importPermitId, options = {}) {
|
|
13
13
|
const ddFlowHome = resolveDdFlowHome(env);
|
|
14
|
-
if (options.resourceDatabaseMode === "prepared_existing" || options.resourceDatabaseMode === "initialize" || options.resourceDatabaseMode === "create_new")
|
|
14
|
+
if (options.resourceDatabaseMode === "prepared_existing" || options.resourceDatabaseMode === "initialize" || options.resourceDatabaseMode === "create_new" || options.resourceDatabaseMode === "retirement")
|
|
15
15
|
assertImportAdmission(ddFlowHome, importPermitId);
|
|
16
16
|
return {
|
|
17
17
|
ddFlowHome,
|
|
@@ -20,6 +20,19 @@ const defaultLeaseMs = 15 * 60_000;
|
|
|
20
20
|
export function resourceHome(context) {
|
|
21
21
|
return context.env?.DD_FLOW_RESOURCE_HOME ?? context.ddFlowHome ?? "/tmp/dd-flow-runtime";
|
|
22
22
|
}
|
|
23
|
+
/** Retained owners may name the same registry through a historical alias. */
|
|
24
|
+
export function retainedResourceHomeMatches(context, retained) {
|
|
25
|
+
if (retained === resourceHome(context))
|
|
26
|
+
return true;
|
|
27
|
+
if (typeof retained !== "string" || !path.isAbsolute(retained))
|
|
28
|
+
return false;
|
|
29
|
+
try {
|
|
30
|
+
return fs.realpathSync(retained) === fs.realpathSync(resourceHome(context));
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
23
36
|
/** Runtime IDs are home-local; a shared host registry needs this extra scope. */
|
|
24
37
|
export function managedProcessRuntimeHome(record, authority) {
|
|
25
38
|
const metadata = parseMetadata(record.metadata_json);
|
|
@@ -300,7 +313,13 @@ export function processIsAlive(record) {
|
|
|
300
313
|
process.kill(record.pid, 0);
|
|
301
314
|
}
|
|
302
315
|
catch (error) {
|
|
303
|
-
|
|
316
|
+
const code = error.code;
|
|
317
|
+
if (code === "ESRCH")
|
|
318
|
+
return false;
|
|
319
|
+
// A reused PID may now belong to another user. EPERM proves presence,
|
|
320
|
+
// not identity; still compare its observed birth before calling it ours.
|
|
321
|
+
if (code !== "EPERM")
|
|
322
|
+
return true;
|
|
304
323
|
}
|
|
305
324
|
const metadata = record.metadata_json ? parseMetadata(record.metadata_json) : {};
|
|
306
325
|
if (Object.hasOwn(metadata, "process_birth")) {
|
|
@@ -319,7 +338,11 @@ export function managedProcessObservation(record) {
|
|
|
319
338
|
process.kill(record.pid, 0);
|
|
320
339
|
}
|
|
321
340
|
catch (error) {
|
|
322
|
-
|
|
341
|
+
const code = error.code;
|
|
342
|
+
if (code === "ESRCH")
|
|
343
|
+
return "absent";
|
|
344
|
+
if (code !== "EPERM")
|
|
345
|
+
return "unknown";
|
|
323
346
|
}
|
|
324
347
|
const reply = spawnSync('ps', ['-o', 'lstart=,stat=', '-p', String(record.pid)], { encoding: 'utf8', timeout: 1000, env: { ...process.env, TZ: 'UTC', LC_ALL: 'C' } });
|
|
325
348
|
if (reply.status !== 0 || !reply.stdout.trim())
|
|
@@ -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 { AppError } from "../shared/errors.js";
|
|
5
|
-
import { managedProcessBinding, managedProcessStatus, processTreeIsAlive, resourceHome } from "./managed-processes.js";
|
|
5
|
+
import { managedProcessBinding, managedProcessStatus, processTreeIsAlive, resourceHome, retainedResourceHomeMatches } from "./managed-processes.js";
|
|
6
6
|
import { retainedDaemonOperations } from "./run-control.js";
|
|
7
7
|
import { managedProviderBelongsToDaemon } from "./managed-daemon-binding.js";
|
|
8
8
|
/** A replaced daemon is a settled resource, never another Session to resume. */
|
|
@@ -26,7 +26,7 @@ export function historicalDaemonSettlement(context, scopeId, record) {
|
|
|
26
26
|
if (matches.length !== 1)
|
|
27
27
|
throw new AppError("runtime_scope_owner_unknown", "Previous daemon has no unique retained receipt", 1);
|
|
28
28
|
const { file, bytes, state } = matches[0], owner = state.config?.runtime_owner;
|
|
29
|
-
if (!state.daemon_id || path.basename(file) !== `${state.daemon_id}.json` || !owner || owner.budget?.scope_id !== scopeId || owner.owner_id !== record.owner_id || owner.role !== metadata.role || owner.state_dir !== metadata.state_dir || owner.adapter_executable !== metadata.adapter_executable || owner.dd_flow_home !== metadata.dd_flow_home || owner.resource_home
|
|
29
|
+
if (!state.daemon_id || path.basename(file) !== `${state.daemon_id}.json` || !owner || owner.budget?.scope_id !== scopeId || owner.owner_id !== record.owner_id || owner.role !== metadata.role || owner.state_dir !== metadata.state_dir || owner.adapter_executable !== metadata.adapter_executable || owner.dd_flow_home !== metadata.dd_flow_home || !retainedResourceHomeMatches(context, owner.resource_home) || (owner.project_id ?? null) !== record.project_id || (owner.run_id ?? null) !== record.run_id || (owner.work_id ?? null) !== record.work_id || !record.operation_id?.startsWith(`${owner.operation_id}:`) || state.resource_process?.lease_token !== record.lease_token || state.pid !== record.pid)
|
|
30
30
|
throw new AppError("runtime_scope_owner_unknown", "Archived daemon does not match its registration", 1);
|
|
31
31
|
const closed = state.shutdown_state === "clean" && state.active_tree === false
|
|
32
32
|
? retainedDaemonOperations(metadata.state_dir).find(operation => operation.daemon_id === state.daemon_id && operation.operation === "daemon.stop" && operation.state === "completed" && operation.result?.stopped === true && operation.result?.clean === true) : undefined;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import os from "node:os";
|
|
4
5
|
import { createContext } from "../runtime/context.js";
|
|
5
6
|
import { AppError } from "../shared/errors.js";
|
|
6
7
|
import { runHarnessAdapter } from "./harness-adapter.js";
|
|
7
|
-
import { managedLeaseReleased, managedProcessBinding, managedProcessRuntimeHome, managedProcessStatus, processIsAlive, processTreeIsAlive, resourceHome, stopManagedProcess } from "./managed-processes.js";
|
|
8
|
+
import { managedLeaseReleased, managedProcessBinding, managedProcessRuntimeHome, managedProcessStatus, processIsAlive, processTreeIsAlive, resourceHome, retainedResourceHomeMatches, stopManagedProcess } from "./managed-processes.js";
|
|
8
9
|
import { terminalCheckSettled } from "./code-checks.js";
|
|
9
10
|
import { fenceRuntimeBudget, runtimeBudgetStatus, scopeLocalProcessRole } from "./runtime-budget.js";
|
|
10
11
|
import { retainedDaemonOperations } from "./run-control.js";
|
|
@@ -26,7 +27,7 @@ export async function stopRuntimeScope(context, scopeId, requestId) {
|
|
|
26
27
|
const read = () => JSON.parse(fs.readFileSync(file, "utf8"));
|
|
27
28
|
const state = read();
|
|
28
29
|
const owner = state.config?.runtime_owner;
|
|
29
|
-
if (!state.daemon_id || !owner || owner.budget?.scope_id !== scopeId || owner.owner_id !== record.owner_id || owner.role !== metadata.role || owner.state_dir !== metadata.state_dir || owner.adapter_executable !== metadata.adapter_executable || owner.resource_home
|
|
30
|
+
if (!state.daemon_id || !owner || owner.budget?.scope_id !== scopeId || owner.owner_id !== record.owner_id || owner.role !== metadata.role || owner.state_dir !== metadata.state_dir || owner.adapter_executable !== metadata.adapter_executable || !retainedResourceHomeMatches(context, owner.resource_home) || (owner.project_id ?? null) !== record.project_id || (owner.run_id ?? null) !== record.run_id || (owner.work_id ?? null) !== record.work_id || !record.operation_id?.startsWith(`${owner.operation_id}:`) || state.resource_process?.id !== record.id || state.resource_process.lease_token !== record.lease_token || state.pid !== record.pid)
|
|
30
31
|
throw new AppError("runtime_scope_owner_unknown", "Native owner does not match its registration", 1);
|
|
31
32
|
const closed = state.shutdown_state === "clean" && state.active_tree === false && !processTreeIsAlive(record)
|
|
32
33
|
? retainedDaemonOperations(metadata.state_dir).find(operation => operation.daemon_id === state.daemon_id && operation.operation === "daemon.stop" && operation.state === "completed" && operation.result?.stopped === true && operation.result?.clean === true) : undefined;
|
|
@@ -38,7 +39,7 @@ export async function stopRuntimeScope(context, scopeId, requestId) {
|
|
|
38
39
|
throw new AppError("runtime_scope_owner_changed", "Native registration changed before control", 1);
|
|
39
40
|
if (!prior && !processIsAlive(current))
|
|
40
41
|
throw new AppError("runtime_scope_outcome_unknown", "Dead native owner requires retained shutdown evidence", 1);
|
|
41
|
-
const env = { ...context.env, DD_FLOW_HOME: owner.dd_flow_home, DD_FLOW_BIN: owner.dd_flow_bin, DD_FLOW_RESOURCE_HOME:
|
|
42
|
+
const env = { ...context.env, DD_FLOW_HOME: owner.dd_flow_home, DD_FLOW_BIN: owner.dd_flow_bin, DD_FLOW_RESOURCE_HOME: resourceHome(context) };
|
|
42
43
|
delete env.DD_FLOW_CONTROL_TOKEN;
|
|
43
44
|
delete env.DD_FLOW_CONTROLLER_TOKEN;
|
|
44
45
|
const receipt = await runHarnessAdapter({ executable: metadata.adapter_executable,
|
|
@@ -109,6 +110,10 @@ export async function stopRuntimeScope(context, scopeId, requestId) {
|
|
|
109
110
|
nodes.push({ process_id: record.id, state_dir: metadata.state_dir ?? "", settled: true, operation_id: operationId, owner_operation_id: operationId });
|
|
110
111
|
continue;
|
|
111
112
|
}
|
|
113
|
+
if (scopeLocalProcessRole(record) && previousBoot(record)) {
|
|
114
|
+
nodes.push({ process_id: record.id, settled: false, native_settled: false, physical_stopped: true, reason: "runtime_scope_outcome_unknown", message: "Physical owner predates host boot; logical outcome is retained" });
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
112
117
|
if (scopeLocalProcessRole(record) && record.operation_id && record.pid && record.pid_started_at) {
|
|
113
118
|
try {
|
|
114
119
|
await stopManagedProcess(context, { id: record.id, leaseToken: record.lease_token, force: false, graceMs: 500, reason: `scope-stop:${scopeId}` });
|
|
@@ -128,6 +133,8 @@ export async function stopRuntimeScope(context, scopeId, requestId) {
|
|
|
128
133
|
const retired = nodes.find(node => node.physical_stopped === true && node.state_dir === metadata.state_dir && node.processes?.some(item => item.process_id === record.id && JSON.stringify(item.binding) === binding));
|
|
129
134
|
if (retired && !processTreeIsAlive(record))
|
|
130
135
|
nodes.push({ process_id: record.id, ...(metadata.state_dir ? { state_dir: metadata.state_dir } : {}), settled: false, native_settled: false, physical_stopped: true, ...(retired.reason ? { reason: retired.reason } : {}) });
|
|
136
|
+
else if (previousBoot(record))
|
|
137
|
+
nodes.push({ process_id: record.id, settled: false, native_settled: false, physical_stopped: true, reason: "runtime_scope_outcome_unknown", message: "Physical owner predates host boot; logical outcome is retained" });
|
|
131
138
|
else
|
|
132
139
|
nodes.push({ process_id: record.id, settled: false, reason: "runtime_scope_resource_unsettled", message: "Resource needs its own settlement proof" });
|
|
133
140
|
}
|
|
@@ -152,7 +159,7 @@ async function stopVerifiedNativeProcesses(context, scopeId, daemon) {
|
|
|
152
159
|
if (!owner || !current || current.lease_token !== daemon.lease_token || current.pid !== daemon.pid || current.pid_started_at !== daemon.pid_started_at
|
|
153
160
|
|| !daemon.pid || !daemon.pid_started_at || owner.budget?.scope_id !== scopeId || owner.owner_id !== daemon.owner_id
|
|
154
161
|
|| owner.state_dir !== metadata.state_dir || owner.role !== metadata.role || owner.adapter_executable !== metadata.adapter_executable
|
|
155
|
-
|| owner.dd_flow_home !== metadata.dd_flow_home || owner.resource_home
|
|
162
|
+
|| owner.dd_flow_home !== metadata.dd_flow_home || !retainedResourceHomeMatches(context, owner.resource_home)
|
|
156
163
|
|| state.resource_process?.id !== daemon.id || state.resource_process.lease_token !== daemon.lease_token
|
|
157
164
|
|| (state.pid !== daemon.pid && !(state.pid == null && state.shutdown_state === "starting")))
|
|
158
165
|
return { stopped: null };
|
|
@@ -166,6 +173,12 @@ async function stopVerifiedNativeProcesses(context, scopeId, daemon) {
|
|
|
166
173
|
&& child.dispatch_barrier === "run-generation@1" && child.budget?.scope_id === scopeId;
|
|
167
174
|
}) : [];
|
|
168
175
|
const provider = providers.length === 1 ? providers[0] : unpublished.length === 1 ? unpublished[0] : undefined;
|
|
176
|
+
if (provider && previousBoot(daemon) && previousBoot(provider))
|
|
177
|
+
return { stopped: true, provider };
|
|
178
|
+
// Legacy daemons did not register their provider. A host reboot proves local
|
|
179
|
+
// process exit, not native Session completion. Never signal a reused PID.
|
|
180
|
+
if (providers.length === 0 && unpublished.length === 0 && !state.provider_process && !state.provider_pid && previousBoot(daemon))
|
|
181
|
+
return { stopped: true };
|
|
169
182
|
if (providers.length > 1 || unpublished.length > 1 || state.provider_pid && !provider || !provider && state.shutdown_state !== "starting")
|
|
170
183
|
return { stopped: null };
|
|
171
184
|
try {
|
|
@@ -178,3 +191,20 @@ async function stopVerifiedNativeProcesses(context, scopeId, daemon) {
|
|
|
178
191
|
return { stopped: false, ...(provider ? { provider } : {}), error: { code: cleanup.code ?? "physical_stop_failed", message: String(cleanup) } };
|
|
179
192
|
}
|
|
180
193
|
}
|
|
194
|
+
function previousBoot(record) {
|
|
195
|
+
const metadata = JSON.parse(record.metadata_json);
|
|
196
|
+
// A waiting, unconfirmed launch cannot outlive the pre-boot owner that alone
|
|
197
|
+
// could release it. This is physical retirement, not a successful launch.
|
|
198
|
+
const identity = record.pid ? record : record.state === "starting" && Number.isSafeInteger(metadata.owner_pid) && metadata.owner_pid > 0 && metadata.owner_pid_started_at
|
|
199
|
+
? { pid: metadata.owner_pid, pid_started_at: metadata.owner_pid_started_at, metadata_json: JSON.stringify(metadata.owner_birth ? { process_birth: metadata.owner_birth } : {}) } : null;
|
|
200
|
+
if (!identity || processIsAlive(identity))
|
|
201
|
+
return false;
|
|
202
|
+
// Legacy lstart has no timezone. Keep a conservative 26-hour ambiguity
|
|
203
|
+
// margin; recent/malformed identities must use ordinary ownership proof.
|
|
204
|
+
const started = Date.parse(record.started_at);
|
|
205
|
+
const birth = identity.pid_started_at && /^\w{3} \w{3}\s+\d{1,2} \d{2}:\d{2}:\d{2} \d{4}$/.test(identity.pid_started_at)
|
|
206
|
+
? Date.parse(`${identity.pid_started_at} UTC`) : NaN;
|
|
207
|
+
const uptime = os.uptime();
|
|
208
|
+
const boundary = Date.now() - uptime * 1000 - 26 * 60 * 60 * 1000;
|
|
209
|
+
return Number.isFinite(uptime) && uptime > 0 && Number.isFinite(started) && Number.isFinite(birth) && started < boundary && birth < boundary;
|
|
210
|
+
}
|
package/dist/storage/database.js
CHANGED
|
@@ -531,6 +531,8 @@ export function getResourceDatabase(resourceHome, mode = "initialize", deadlineA
|
|
|
531
531
|
const deadline = requestDeadline(deadlineAt);
|
|
532
532
|
deadline?.("resource_open");
|
|
533
533
|
const dbPath = path.join(resourceHome, "runtime.sqlite");
|
|
534
|
+
if (mode === "retirement" && !fs.existsSync(dbPath))
|
|
535
|
+
return getResourceDatabase(resourceHome, "initialize", deadlineAt);
|
|
534
536
|
const cached = resourceDatabases.get(dbPath);
|
|
535
537
|
if (mode === "create_new") {
|
|
536
538
|
if (cached)
|
|
@@ -562,29 +564,38 @@ export function getResourceDatabase(resourceHome, mode = "initialize", deadlineA
|
|
|
562
564
|
if (mode === "prepared_existing" && !fs.existsSync(dbPath))
|
|
563
565
|
throw new AppError("resource_storage_unprepared", "Prepare the resource registry before maintenance", 1);
|
|
564
566
|
if (cached) {
|
|
565
|
-
if (mode === "prepared_existing")
|
|
567
|
+
if (mode === "prepared_existing" || mode === "retirement")
|
|
566
568
|
assertResourceDatabaseIdentity(cached);
|
|
567
569
|
const bounded = deadlineAt === undefined ? cached : cached.withDeadline(deadlineAt);
|
|
568
|
-
|
|
569
|
-
|
|
570
|
+
const reader = { prepare: (sql) => ({ get: (...params) => bounded.get(sql, params), all: (...params) => bounded.all(sql, params), run: (...params) => bounded.run(sql, params) }) };
|
|
571
|
+
if (mode === "prepared_existing" || mode === "retirement")
|
|
572
|
+
assertResourceStoreReady(reader, mode === "retirement");
|
|
573
|
+
else {
|
|
574
|
+
const required = readStoreWriterContract(reader);
|
|
575
|
+
if (required !== null && required !== STORE_WRITER_VERSION)
|
|
576
|
+
throw new AppError("store_writer_incompatible", "Retirement writer cannot admit productive resource operations", 1, { required, supported: STORE_WRITER_VERSION });
|
|
577
|
+
}
|
|
570
578
|
return bounded;
|
|
571
579
|
}
|
|
572
|
-
if (mode !== "prepared_existing")
|
|
580
|
+
if (mode !== "prepared_existing" && mode !== "retirement")
|
|
573
581
|
ensureDir(resourceHome);
|
|
574
582
|
channel("dd-flow.sqlite.ingress").publish({ phase: "store_connection", store_kind: "resource" });
|
|
575
583
|
const db = new DatabaseSync(dbPath);
|
|
576
584
|
try {
|
|
577
585
|
channel("dd-flow.sqlite.ingress").publish({ phase: "store_writer_registration", store_kind: "resource" });
|
|
578
586
|
db.exec(`PRAGMA busy_timeout = ${deadline?.("resource_open") ?? 4000}`);
|
|
579
|
-
|
|
587
|
+
if (mode === "retirement")
|
|
588
|
+
registerReconciliationWriter(db);
|
|
589
|
+
else
|
|
590
|
+
registerStoreWriter(db);
|
|
580
591
|
}
|
|
581
592
|
catch (error) {
|
|
582
593
|
return closeAfterOpenFailure(db, error);
|
|
583
594
|
}
|
|
584
595
|
channel("dd-flow.sqlite.ingress").publish({ phase: "store_validation", store_kind: "resource" });
|
|
585
596
|
try {
|
|
586
|
-
if (mode === "prepared_existing")
|
|
587
|
-
assertResourceStoreReady(db);
|
|
597
|
+
if (mode === "prepared_existing" || mode === "retirement")
|
|
598
|
+
assertResourceStoreReady(db, mode === "retirement");
|
|
588
599
|
else
|
|
589
600
|
configureDatabase(db, "initialize", deadline);
|
|
590
601
|
}
|
|
@@ -600,7 +611,7 @@ export function getResourceDatabase(resourceHome, mode = "initialize", deadlineA
|
|
|
600
611
|
return closeAfterOpenFailure(db, error);
|
|
601
612
|
}
|
|
602
613
|
try {
|
|
603
|
-
if (mode !== "prepared_existing")
|
|
614
|
+
if (mode !== "prepared_existing" && mode !== "retirement")
|
|
604
615
|
executeStoreSchema(db, `
|
|
605
616
|
CREATE TABLE IF NOT EXISTS managed_processes (
|
|
606
617
|
id TEXT PRIMARY KEY,
|
|
@@ -669,11 +680,11 @@ function requestDeadline(at) {
|
|
|
669
680
|
return phase => { const remaining = at - Date.now(); if (remaining <= 0)
|
|
670
681
|
throw new AppError("process_heartbeat_timeout", "Maintenance storage deadline expired", 1, { phase, deadline_at: at, effect: "unknown" }); return Math.max(1, Math.min(4000, remaining)); };
|
|
671
682
|
}
|
|
672
|
-
function assertResourceStoreReady(db) {
|
|
683
|
+
function assertResourceStoreReady(db, retirement = false) {
|
|
673
684
|
const version = readStoreWriterContract(db);
|
|
674
|
-
if (version === null)
|
|
685
|
+
if (version === null && !retirement)
|
|
675
686
|
throw new AppError("resource_storage_unprepared", "Prepare the resource registry writer contract before maintenance", 1);
|
|
676
|
-
if (version !== STORE_WRITER_VERSION)
|
|
687
|
+
if (version !== null && version !== STORE_WRITER_VERSION && !(retirement && version === 1))
|
|
677
688
|
throw new AppError("store_writer_incompatible", "This CLI cannot maintain the resource registry's required contract", 1, { required: version, supported: STORE_WRITER_VERSION });
|
|
678
689
|
for (const [table, columns] of Object.entries({ managed_processes: "id,kind,pid,pid_started_at,owner_id,lease_token,lease_expires_at,project_id,run_id,work_id,check_id,operation_id,stdout_path,stderr_path,state,started_at,updated_at,finished_at,termination_reason,metadata_json", managed_resources: "resource_kind,resource_key,owner_id,lease_token,lease_expires_at,process_id,metadata_json,created_at,updated_at" })) {
|
|
679
690
|
try {
|