@devrouter/cli 0.0.65 → 0.0.67
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/dist/devrouter-lifecycle-worker.js +252 -20
- package/dist/devrouter.js +521 -185
- package/package.json +1 -1
- package/upgrade-prompts/0.0.66.md +31 -0
- package/upgrade-prompts/0.0.67.md +41 -0
|
@@ -1188,6 +1188,17 @@ function inspectDevsyRuntimeStatus(devsyId) {
|
|
|
1188
1188
|
if (result.status !== 0 || result.error) return "unknown";
|
|
1189
1189
|
return parseDevsyRuntimeStatus(result.stdout, devsyId);
|
|
1190
1190
|
}
|
|
1191
|
+
function inspectDevsyRuntimeAbsence(devsyId) {
|
|
1192
|
+
const result = (0, import_node_child_process5.spawnSync)("devsy", ["workspace", "status", devsyId, "--result-format", "json"], {
|
|
1193
|
+
encoding: "utf-8",
|
|
1194
|
+
timeout: 1e4
|
|
1195
|
+
});
|
|
1196
|
+
if (!result.error && result.status === 0) {
|
|
1197
|
+
return parseDevsyRuntimeStatus(result.stdout, devsyId) === "not-found";
|
|
1198
|
+
}
|
|
1199
|
+
const output = [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n");
|
|
1200
|
+
return /workspace not found/i.test(output);
|
|
1201
|
+
}
|
|
1191
1202
|
function inspectDevsyWorkspaceOwnership(workspaces, devsyId, worktreePath) {
|
|
1192
1203
|
const idOwners = workspaces.filter((workspace) => workspace.id === devsyId);
|
|
1193
1204
|
const pathOwners = workspaces.filter(
|
|
@@ -1987,6 +1998,7 @@ function requireReadyDevsyAgent(options = {}) {
|
|
|
1987
1998
|
// src/core/managed-devsy-stop.ts
|
|
1988
1999
|
var import_node_os4 = __toESM(require("os"));
|
|
1989
2000
|
var import_node_path20 = __toESM(require("path"));
|
|
2001
|
+
var import_node_util4 = require("util");
|
|
1990
2002
|
|
|
1991
2003
|
// src/core/devcontainer-profile.ts
|
|
1992
2004
|
var import_node_child_process10 = require("child_process");
|
|
@@ -3202,7 +3214,16 @@ function registration(state) {
|
|
|
3202
3214
|
throw new Error("Stop workspace identity changed.");
|
|
3203
3215
|
resolveGitCommonDir(state.repoPath);
|
|
3204
3216
|
} else if (state.workspace !== void 0) throw new Error("Stop checkout identity changed.");
|
|
3205
|
-
|
|
3217
|
+
return identityOf(owner.workspace);
|
|
3218
|
+
}
|
|
3219
|
+
function managedRunnerId(uid, providerId) {
|
|
3220
|
+
if (uid === void 0) {
|
|
3221
|
+
return providerId;
|
|
3222
|
+
}
|
|
3223
|
+
const bytes = Buffer.byteLength(uid);
|
|
3224
|
+
return bytes === 16 || bytes === 40 ? uid : providerId;
|
|
3225
|
+
}
|
|
3226
|
+
function identityOf(value) {
|
|
3206
3227
|
return {
|
|
3207
3228
|
context: value.context ?? "",
|
|
3208
3229
|
providerId: value.id,
|
|
@@ -3347,6 +3368,64 @@ function absentRegistrationIdentity(state) {
|
|
|
3347
3368
|
}
|
|
3348
3369
|
return { record: record5, gitCommonDir: resolveGitCommonDir(state.repoPath) };
|
|
3349
3370
|
}
|
|
3371
|
+
function replacementObservation(state) {
|
|
3372
|
+
resetWorkspaceRuntimeCaches();
|
|
3373
|
+
if (resolveWorkspaceRuntimeOrDefault(state.repoPath) !== "devsy")
|
|
3374
|
+
throw new Error("Stop provider changed.");
|
|
3375
|
+
if (!state.workspace || !isLinkedWorktree(state.repoPath))
|
|
3376
|
+
throw new Error("Replacement stop requires a linked workspace.");
|
|
3377
|
+
const record5 = readWorkspaceOwnership(state.repoPath, state.workspace);
|
|
3378
|
+
if (!record5 || record5.workspace !== state.workspace || record5.devpodId !== state.devpodId || !sameWorkspacePath(record5.worktreePath, state.repoPath) || resolveWorktreeWorkspace(state.repoPath) !== state.workspace || inspectWorkspaceOwnership(record5, listGitWorktrees(state.repoPath), void 0).ownerStatus !== "present")
|
|
3379
|
+
throw new Error("Replacement stop workspace ownership changed.");
|
|
3380
|
+
const owner = inspectDevsyWorkspaceOwnership(
|
|
3381
|
+
listDevsyWorkspaces(),
|
|
3382
|
+
state.devpodId,
|
|
3383
|
+
state.repoPath
|
|
3384
|
+
);
|
|
3385
|
+
if (owner.status !== "owned")
|
|
3386
|
+
throw new Error("Replacement stop requires one exact provider registration.");
|
|
3387
|
+
if (listDevpodWorkspacesRaw({ allowMissingExecutable: true }).some(
|
|
3388
|
+
(entry) => entry.id === state.devpodId || sameWorkspacePath(entry.source.localFolder, state.repoPath)
|
|
3389
|
+
))
|
|
3390
|
+
throw new Error("Replacement stop requires no competing provider registration.");
|
|
3391
|
+
proveLocalDockerSelection(owner.workspace);
|
|
3392
|
+
return owner.workspace;
|
|
3393
|
+
}
|
|
3394
|
+
function proveReplacementStopAbsence(state, baseline, replacement) {
|
|
3395
|
+
const observation = () => {
|
|
3396
|
+
const workspace = replacementObservation(state);
|
|
3397
|
+
if (workspace.uid !== replacement.uid)
|
|
3398
|
+
throw new Error("Replacement stop registration changed during inspection.");
|
|
3399
|
+
if (resolveManagedStopEndpoint() !== baseline.endpoint)
|
|
3400
|
+
throw new Error("Stop endpoint changed.");
|
|
3401
|
+
if (inspectManagedStopDaemon(baseline.endpoint) !== baseline.daemonId)
|
|
3402
|
+
throw new Error("Stop daemon changed.");
|
|
3403
|
+
return workspace;
|
|
3404
|
+
};
|
|
3405
|
+
const before = observation();
|
|
3406
|
+
if (!(0, import_node_util2.isDeepStrictEqual)(readManagedRuntimeState(state.repoPath, state.workspace), state))
|
|
3407
|
+
throw new Error("Retained stop generation changed.");
|
|
3408
|
+
assertManagedStopContainersAbsent(
|
|
3409
|
+
baseline.endpoint,
|
|
3410
|
+
baseline.containers.map((container) => container.id)
|
|
3411
|
+
);
|
|
3412
|
+
const oldRunner = managedRunnerId(baseline.uid, baseline.providerId);
|
|
3413
|
+
const newRunner = managedRunnerId(before.uid, baseline.providerId);
|
|
3414
|
+
const runners = oldRunner === newRunner ? [oldRunner] : [oldRunner, newRunner];
|
|
3415
|
+
const assertEmptyPopulation = () => {
|
|
3416
|
+
if (inspectManagedStopContainers(baseline.project, baseline.endpoint).length !== 0 || inspectManagedStopWorkspaceIds(baseline.endpoint, baseline.composeDirectory).length !== 0 || runners.some((id3) => inspectProviderRunnerContainers(baseline.endpoint, id3).length !== 0))
|
|
3417
|
+
throw new Error("Replacement stop observed a remaining workspace population.");
|
|
3418
|
+
};
|
|
3419
|
+
assertEmptyPopulation();
|
|
3420
|
+
const after = observation();
|
|
3421
|
+
if (after.uid !== before.uid || !(0, import_node_util2.isDeepStrictEqual)(readManagedRuntimeState(state.repoPath, state.workspace), state))
|
|
3422
|
+
throw new Error("Replacement stop authority changed during inspection.");
|
|
3423
|
+
assertManagedStopContainersAbsent(
|
|
3424
|
+
baseline.endpoint,
|
|
3425
|
+
baseline.containers.map((container) => container.id)
|
|
3426
|
+
);
|
|
3427
|
+
assertEmptyPopulation();
|
|
3428
|
+
}
|
|
3350
3429
|
function proveManagedStop(state) {
|
|
3351
3430
|
const baseline = validateManagedStopBaseline(state.stopBaseline, state);
|
|
3352
3431
|
const owner = inspectDevsyWorkspaceOwnership(
|
|
@@ -3354,6 +3433,15 @@ function proveManagedStop(state) {
|
|
|
3354
3433
|
state.devpodId,
|
|
3355
3434
|
state.repoPath
|
|
3356
3435
|
);
|
|
3436
|
+
if (owner.status === "owned") {
|
|
3437
|
+
const identityKeys = ["context", "providerId", "uid", "sourcePath", "sourceContainer"];
|
|
3438
|
+
const current = identityOf(owner.workspace);
|
|
3439
|
+
const changed = identityKeys.filter((key) => baseline[key] !== current[key]);
|
|
3440
|
+
if (changed.length === 1 && changed[0] === "uid" && !baseline.sourceContainer && !current.sourceContainer && state.workspace !== void 0 && isLinkedWorktree(state.repoPath)) {
|
|
3441
|
+
proveReplacementStopAbsence(state, baseline, { uid: current.uid || void 0 });
|
|
3442
|
+
return { status: "proven-absent", containers: [] };
|
|
3443
|
+
}
|
|
3444
|
+
}
|
|
3357
3445
|
if (owner.status !== "absent")
|
|
3358
3446
|
return { status: "retained", containers: proveRetainedManagedStop(state) };
|
|
3359
3447
|
if (baseline.sourceContainer) throw new Error("Absent stop cannot prove a source container.");
|
|
@@ -4787,7 +4875,7 @@ function loadRepoConfig(repoPath, read2 = (file) => import_node_fs14.default.rea
|
|
|
4787
4875
|
const config = parseConfig(parsed ?? {}, configPath);
|
|
4788
4876
|
const requiredVersion = config.devrouter?.version;
|
|
4789
4877
|
if (requiredVersion && !hasWarnedVersionMismatch) {
|
|
4790
|
-
const cliVersion = true ? "0.0.
|
|
4878
|
+
const cliVersion = true ? "0.0.67" : "0.0.0-dev";
|
|
4791
4879
|
if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
|
|
4792
4880
|
hasWarnedVersionMismatch = true;
|
|
4793
4881
|
process.stderr.write(
|
|
@@ -5342,6 +5430,66 @@ function stopRetainedManagedDevsyWorkspace(options) {
|
|
|
5342
5430
|
}
|
|
5343
5431
|
return owner.workspace;
|
|
5344
5432
|
}
|
|
5433
|
+
function registrationStatus() {
|
|
5434
|
+
resetWorkspaceRuntimeCaches();
|
|
5435
|
+
if (resolveWorkspaceRuntimeOrDefault(repoPath) !== "devsy") {
|
|
5436
|
+
throw new Error("Managed stop provider selection changed.");
|
|
5437
|
+
}
|
|
5438
|
+
return inspectDevsyWorkspaceOwnership(listDevsyWorkspaces(), devsyId, repoPath);
|
|
5439
|
+
}
|
|
5440
|
+
function proveAbsentRegistrationStop() {
|
|
5441
|
+
const observe = () => {
|
|
5442
|
+
const owner = registrationStatus();
|
|
5443
|
+
if (owner.status === "conflict") throw new Error(owner.reason);
|
|
5444
|
+
if (owner.status !== "absent") {
|
|
5445
|
+
throw new Error("Managed stop absence proof requires the registration to remain absent.");
|
|
5446
|
+
}
|
|
5447
|
+
if (listDevpodWorkspacesRaw({ allowMissingExecutable: true }).some(
|
|
5448
|
+
(entry) => entry.id === devsyId || sameWorkspacePath(entry.source.localFolder, repoPath)
|
|
5449
|
+
)) {
|
|
5450
|
+
throw new Error(
|
|
5451
|
+
"Managed stop absence proof requires both provider registrations to remain absent."
|
|
5452
|
+
);
|
|
5453
|
+
}
|
|
5454
|
+
if (!inspectDevsyRuntimeAbsence(devsyId)) {
|
|
5455
|
+
throw new Error(
|
|
5456
|
+
"Managed stop absence proof requires Devsy to report the runtime not-found."
|
|
5457
|
+
);
|
|
5458
|
+
}
|
|
5459
|
+
if (inspectManagedStopContainers(state.composeProject).length !== 0) {
|
|
5460
|
+
throw new Error("Managed stop absence proof observed a remaining compose population.");
|
|
5461
|
+
}
|
|
5462
|
+
if (inspectProviderRunnerContainers(resolveManagedStopEndpoint(), devsyId).length !== 0) {
|
|
5463
|
+
throw new Error("Managed stop absence proof observed a remaining runner population.");
|
|
5464
|
+
}
|
|
5465
|
+
const owned = inspectWorkspaceContainers().filter(
|
|
5466
|
+
(container) => sameWorkspacePath(
|
|
5467
|
+
container.labels["com.docker.compose.project.working_dir"] ?? "",
|
|
5468
|
+
import_node_path20.default.join(repoPath, ".devcontainer")
|
|
5469
|
+
)
|
|
5470
|
+
);
|
|
5471
|
+
if (owned.length !== 0) {
|
|
5472
|
+
throw new Error("Managed stop absence proof observed a remaining workspace container.");
|
|
5473
|
+
}
|
|
5474
|
+
if (workspace) {
|
|
5475
|
+
const record5 = readWorkspaceOwnership(repoPath, workspace);
|
|
5476
|
+
if (!record5 || record5.devpodId !== devsyId || !sameWorkspacePath(record5.worktreePath, repoPath) || resolveWorktreeWorkspace(repoPath) !== workspace || resolveGitCommonDir(repoPath) !== workspaceEnv?.gitCommonDir || inspectWorkspaceOwnership(record5, listGitWorktrees(repoPath), void 0).ownerStatus !== "present") {
|
|
5477
|
+
throw new Error("Managed stop workspace ownership changed during absence proof.");
|
|
5478
|
+
}
|
|
5479
|
+
}
|
|
5480
|
+
if (!(0, import_node_util4.isDeepStrictEqual)(readManagedRuntimeState(repoPath, workspace), state)) {
|
|
5481
|
+
throw new Error("Managed stop retained state changed during absence proof.");
|
|
5482
|
+
}
|
|
5483
|
+
};
|
|
5484
|
+
observe();
|
|
5485
|
+
observe();
|
|
5486
|
+
}
|
|
5487
|
+
const initialOwner = registrationStatus();
|
|
5488
|
+
if (initialOwner.status === "conflict") throw new Error(initialOwner.reason);
|
|
5489
|
+
if (initialOwner.status === "absent") {
|
|
5490
|
+
proveAbsentRegistrationStop();
|
|
5491
|
+
return "proven-absent";
|
|
5492
|
+
}
|
|
5345
5493
|
const context = registration2().context;
|
|
5346
5494
|
if (!context || !/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(context) || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(devsyId)) {
|
|
5347
5495
|
throw new Error("Managed stop requires a valid exact Devsy context and workspace ID.");
|
|
@@ -7320,6 +7468,7 @@ function assertReliabilityEvent(value) {
|
|
|
7320
7468
|
case "launched":
|
|
7321
7469
|
case "interrupted":
|
|
7322
7470
|
case "not-started":
|
|
7471
|
+
case "settle":
|
|
7323
7472
|
valid = isReliabilityId(value.operationId);
|
|
7324
7473
|
break;
|
|
7325
7474
|
case "completion":
|
|
@@ -7389,8 +7538,13 @@ function transition(state, outcome, effects = []) {
|
|
|
7389
7538
|
assertReliabilityState(state);
|
|
7390
7539
|
return { state, effects, outcome };
|
|
7391
7540
|
}
|
|
7392
|
-
function unchanged(state, outcome) {
|
|
7393
|
-
|
|
7541
|
+
function unchanged(state, outcome, reason2) {
|
|
7542
|
+
const result = transition(cloneState(state), outcome);
|
|
7543
|
+
if (reason2) result.reason = reason2;
|
|
7544
|
+
return result;
|
|
7545
|
+
}
|
|
7546
|
+
function blocked(state, reason2) {
|
|
7547
|
+
return unchanged(state, "blocked", reason2);
|
|
7394
7548
|
}
|
|
7395
7549
|
function advance(value) {
|
|
7396
7550
|
return value === Number.MAX_SAFE_INTEGER ? null : value + 1;
|
|
@@ -7415,7 +7569,10 @@ function updateRunningPhase(state) {
|
|
|
7415
7569
|
}
|
|
7416
7570
|
function handleRequest(state, event) {
|
|
7417
7571
|
if (state.executionPolicy === "manual" && event.mode === "start")
|
|
7418
|
-
return
|
|
7572
|
+
return blocked(
|
|
7573
|
+
state,
|
|
7574
|
+
"start requests are not admitted under manual execution policy; use the ensure/stop commands."
|
|
7575
|
+
);
|
|
7419
7576
|
const existing = state.requests.find((request) => request.key === event.key);
|
|
7420
7577
|
if (existing) {
|
|
7421
7578
|
const consumer = state.consumers.find((candidate) => candidate.id === existing.consumerId);
|
|
@@ -7482,7 +7639,8 @@ function handleRequest(state, event) {
|
|
|
7482
7639
|
}
|
|
7483
7640
|
function canExecAfterInterruptedEnsure(state) {
|
|
7484
7641
|
const latestEnsure = [...state.operationHistory].reverse().find((operation) => operation.kind === "ensure");
|
|
7485
|
-
|
|
7642
|
+
const quiescent = state.stopProof.workloadsStopped && state.stopProof.routesRemoved;
|
|
7643
|
+
return state.executionPolicy === "manual" && (state.desired === "running" || quiescent && state.desired === "stopped-by-user") && latestEnsure?.status === "INTERRUPTED" && latestEnsure.drained && state.operation?.drained === true && (state.operation.kind === "ensure" && (state.phase === "recovering" || quiescent) || state.operation.kind === "exec" && ["COMPLETED", "NOT_LAUNCHED", "NOT_STARTED"].includes(state.operation.status));
|
|
7486
7644
|
}
|
|
7487
7645
|
function handleOperationRequest(state, event) {
|
|
7488
7646
|
if (state.executionPolicy !== "manual") return unchanged(state, "blocked");
|
|
@@ -7495,25 +7653,39 @@ function handleOperationRequest(state, event) {
|
|
|
7495
7653
|
if (state.operationHistory.some((entry) => entry.id === event.operationId))
|
|
7496
7654
|
return unchanged(state, "conflict");
|
|
7497
7655
|
if (event.kind === "exec" && canExecAfterInterruptedEnsure(state) && event.recoverInterruptedEnsure !== true)
|
|
7498
|
-
return
|
|
7656
|
+
return blocked(
|
|
7657
|
+
state,
|
|
7658
|
+
"the latest ensure is INTERRUPTED and drained; exec requires recovering it first (run ensure, or exec with recovery proof)."
|
|
7659
|
+
);
|
|
7499
7660
|
const fullyStopped = state.stopProof.workloadsStopped && state.stopProof.routesRemoved;
|
|
7500
7661
|
const reconcileEnsure = event.kind === "ensure" && state.operation?.kind === "ensure" && ["NOT_STARTED", "INTERRUPTED"].includes(state.operation.status);
|
|
7501
7662
|
if (state.operation && (!state.operation.drained || !["COMPLETED", "NOT_LAUNCHED", "NOT_STARTED"].includes(state.operation.status) && !fullyStopped && !reconcileEnsure && !(event.kind === "exec" && event.recoverInterruptedEnsure === true && canExecAfterInterruptedEnsure(state))))
|
|
7502
|
-
return
|
|
7663
|
+
return blocked(
|
|
7664
|
+
state,
|
|
7665
|
+
`operation ${state.operation.id} is ${state.operation.status} (drained=${state.operation.drained}) and no stop proof, ensure reconciliation, or exec recovery supersedes it.`
|
|
7666
|
+
);
|
|
7503
7667
|
if (state.phase === "stopping" || state.desired === "parked-for-capacity")
|
|
7504
|
-
return
|
|
7668
|
+
return blocked(
|
|
7669
|
+
state,
|
|
7670
|
+
`phase is '${state.phase}' with desired '${state.desired}'; wait for the running stop to finish.`
|
|
7671
|
+
);
|
|
7505
7672
|
if (event.kind === "exec" && (!event.runtimeRunning || state.desired !== "running" && state.intentRevision !== 0))
|
|
7506
|
-
return
|
|
7673
|
+
return blocked(
|
|
7674
|
+
state,
|
|
7675
|
+
`exec requires a running workspace (runtimeRunning=${event.runtimeRunning}, desired='${state.desired}').`
|
|
7676
|
+
);
|
|
7507
7677
|
const rollover = state.operationHistory.length >= RELIABILITY_MAX_ITEMS;
|
|
7508
7678
|
let retired = -1;
|
|
7509
7679
|
if (rollover) {
|
|
7510
|
-
if (state.operation && (!state.operation.drained || !["COMPLETED", "NOT_LAUNCHED"].includes(state.operation.status)))
|
|
7511
|
-
return unchanged(state, "blocked");
|
|
7512
7680
|
const latestEnsureId = [...state.operationHistory].reverse().find((entry) => entry.kind === "ensure")?.id;
|
|
7513
7681
|
retired = state.operationHistory.findIndex(
|
|
7514
|
-
(entry) => entry.id !== state.operation?.id && entry.id !== latestEnsureId && entry.drained && ["COMPLETED", "NOT_LAUNCHED"].includes(entry.status)
|
|
7682
|
+
(entry) => entry.id !== state.operation?.id && entry.id !== latestEnsureId && entry.drained && ["COMPLETED", "NOT_LAUNCHED", "INTERRUPTED"].includes(entry.status)
|
|
7515
7683
|
);
|
|
7516
|
-
if (retired === -1)
|
|
7684
|
+
if (retired === -1)
|
|
7685
|
+
return blocked(
|
|
7686
|
+
state,
|
|
7687
|
+
`journal holds ${state.operationHistory.length} entries and every retirable entry is the current operation or the latest ensure.`
|
|
7688
|
+
);
|
|
7517
7689
|
}
|
|
7518
7690
|
if (state.desired !== "running" || rollover) {
|
|
7519
7691
|
const revision = advance(state.intentRevision);
|
|
@@ -7568,7 +7740,10 @@ function handleDispatch(state) {
|
|
|
7568
7740
|
if (state.operation?.drained || state.operation?.status !== "NOT_STARTED")
|
|
7569
7741
|
return unchanged(state, "stale");
|
|
7570
7742
|
if (state.desired !== "running" || state.executionPolicy === "capacity-managed" && state.admission !== "admitted" || state.consumers.length === 0) {
|
|
7571
|
-
return
|
|
7743
|
+
return blocked(
|
|
7744
|
+
state,
|
|
7745
|
+
`dispatch requires desired 'running' with a registered consumer (desired='${state.desired}', consumers=${state.consumers.length}).`
|
|
7746
|
+
);
|
|
7572
7747
|
}
|
|
7573
7748
|
const corrective = state.phase === "recovering" && state.incident !== null;
|
|
7574
7749
|
state.operation = { ...state.operation, status: "DISPATCH_PENDING", exitCode: null };
|
|
@@ -7583,7 +7758,10 @@ function handleDispatchPersisted(state, event) {
|
|
|
7583
7758
|
if (state.desired !== "running" || state.executionPolicy === "capacity-managed" && (state.admission !== "admitted" || !state.chargeHeld)) {
|
|
7584
7759
|
state.operation = setUnknown(state.operation);
|
|
7585
7760
|
state.chargeHeld = state.executionPolicy === "capacity-managed";
|
|
7586
|
-
return
|
|
7761
|
+
return blocked(
|
|
7762
|
+
state,
|
|
7763
|
+
`dispatch persistence requires desired 'running' (desired='${state.desired}'); the operation outcome is unknown.`
|
|
7764
|
+
);
|
|
7587
7765
|
}
|
|
7588
7766
|
if (state.phase === "recovering" && state.incident !== null && state.incident.correctiveActionsTaken >= state.incident.actionLimit) {
|
|
7589
7767
|
return unchanged(state, "blocked");
|
|
@@ -7631,6 +7809,22 @@ function handleInterrupted(state, event) {
|
|
|
7631
7809
|
if (state.desired === "running") state.phase = "recovering";
|
|
7632
7810
|
return transition(state, "accepted");
|
|
7633
7811
|
}
|
|
7812
|
+
function handleSettle(state, event) {
|
|
7813
|
+
if (state.executionPolicy !== "manual") return unchanged(state, "stale");
|
|
7814
|
+
if (!state.operation || state.operation.id !== event.operationId)
|
|
7815
|
+
return unchanged(state, "stale");
|
|
7816
|
+
if (state.operation.drained && state.operation.status === "INTERRUPTED")
|
|
7817
|
+
return unchanged(state, "joined");
|
|
7818
|
+
if (state.operation.status === "COMPLETED") return unchanged(state, "joined");
|
|
7819
|
+
state.operation = {
|
|
7820
|
+
...state.operation,
|
|
7821
|
+
status: "INTERRUPTED",
|
|
7822
|
+
drained: true,
|
|
7823
|
+
exitCode: null
|
|
7824
|
+
};
|
|
7825
|
+
if (state.desired === "running") state.phase = "recovering";
|
|
7826
|
+
return transition(state, "accepted");
|
|
7827
|
+
}
|
|
7634
7828
|
function handleObservation(state, event, nowMs) {
|
|
7635
7829
|
if (state.desired !== "running") return unchanged(state, "stale");
|
|
7636
7830
|
const previous = state.observations.find(
|
|
@@ -7640,7 +7834,10 @@ function handleObservation(state, event, nowMs) {
|
|
|
7640
7834
|
return unchanged(state, "stale");
|
|
7641
7835
|
}
|
|
7642
7836
|
if (!previous && state.observations.length >= RELIABILITY_MAX_ITEMS)
|
|
7643
|
-
return
|
|
7837
|
+
return blocked(
|
|
7838
|
+
state,
|
|
7839
|
+
`observation set holds ${state.observations.length} distinct capabilities; observations repeat per capability and never retire.`
|
|
7840
|
+
);
|
|
7644
7841
|
state.observations = state.observations.filter(
|
|
7645
7842
|
(observation) => observation.capability !== event.observation.capability
|
|
7646
7843
|
);
|
|
@@ -7668,7 +7865,10 @@ function handleStop(state) {
|
|
|
7668
7865
|
function handleStopProof(state, event) {
|
|
7669
7866
|
if (state.desired === "running") return unchanged(state, "stale");
|
|
7670
7867
|
if (state.executionPolicy === "manual" && state.operation && !state.operation.drained && event.workloadsStopped && event.routesRemoved)
|
|
7671
|
-
return
|
|
7868
|
+
return blocked(
|
|
7869
|
+
state,
|
|
7870
|
+
`stop proof cannot settle while operation ${state.operation.id} is not drained; wait for its worker or settle the journal.`
|
|
7871
|
+
);
|
|
7672
7872
|
const previous = { ...state.stopProof };
|
|
7673
7873
|
if (previous.workloadsStopped === event.workloadsStopped && previous.routesRemoved === event.routesRemoved)
|
|
7674
7874
|
return unchanged(state, "joined");
|
|
@@ -7847,6 +8047,8 @@ function applyReliabilityEvent(state, event, nowMs) {
|
|
|
7847
8047
|
return handleCompletion(next, event);
|
|
7848
8048
|
case "interrupted":
|
|
7849
8049
|
return handleInterrupted(next, event);
|
|
8050
|
+
case "settle":
|
|
8051
|
+
return handleSettle(next, event);
|
|
7850
8052
|
case "observation":
|
|
7851
8053
|
return handleObservation(next, event, nowMs);
|
|
7852
8054
|
case "stop-proof":
|
|
@@ -7882,6 +8084,19 @@ function stepReliability(state, event, nowMs) {
|
|
|
7882
8084
|
var import_node_crypto11 = require("crypto");
|
|
7883
8085
|
var import_node_fs20 = __toESM(require("fs"));
|
|
7884
8086
|
var import_node_path25 = __toESM(require("path"));
|
|
8087
|
+
function installedCliVersion() {
|
|
8088
|
+
return true ? "0.0.67" : "0.0.0-dev";
|
|
8089
|
+
}
|
|
8090
|
+
function compareCliVersions(left, right) {
|
|
8091
|
+
const parts = (value) => value.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
8092
|
+
const leftParts = parts(left);
|
|
8093
|
+
const rightParts = parts(right);
|
|
8094
|
+
for (let index = 0; index < 3; index += 1) {
|
|
8095
|
+
const difference = (leftParts[index] ?? 0) - (rightParts[index] ?? 0);
|
|
8096
|
+
if (difference !== 0) return difference;
|
|
8097
|
+
}
|
|
8098
|
+
return 0;
|
|
8099
|
+
}
|
|
7885
8100
|
var MAX_RECORD_BYTES = 1048576;
|
|
7886
8101
|
function identityKey(identity2) {
|
|
7887
8102
|
if (!import_node_path25.default.isAbsolute(identity2.repoPath) || import_node_path25.default.resolve(identity2.repoPath) !== identity2.repoPath || identity2.repoPath.includes("\0") || identity2.workspace !== null && !isReliabilityId(identity2.workspace) || !["devpod", "devsy"].includes(identity2.provider)) {
|
|
@@ -7898,7 +8113,21 @@ function keys(value, expected) {
|
|
|
7898
8113
|
}
|
|
7899
8114
|
}
|
|
7900
8115
|
function validate(record5, identity2) {
|
|
7901
|
-
keys(record5, [
|
|
8116
|
+
keys(record5, [
|
|
8117
|
+
"version",
|
|
8118
|
+
"identity",
|
|
8119
|
+
"revision",
|
|
8120
|
+
"state",
|
|
8121
|
+
"worker",
|
|
8122
|
+
"effectSequence",
|
|
8123
|
+
"outcome",
|
|
8124
|
+
"writtenByVersion"
|
|
8125
|
+
]);
|
|
8126
|
+
if (record5.writtenByVersion !== void 0 && compareCliVersions(record5.writtenByVersion, installedCliVersion()) > 0) {
|
|
8127
|
+
throw new Error(
|
|
8128
|
+
`Reliability record was last written by devrouter ${record5.writtenByVersion}, which is newer than the installed ${installedCliVersion()}; upgrade devrouter (devrouter upgrade) before running lifecycle commands.`
|
|
8129
|
+
);
|
|
8130
|
+
}
|
|
7902
8131
|
keys(record5.identity, ["repoPath", "workspace", "provider"]);
|
|
7903
8132
|
if (record5.version !== 1 || record5.identity.repoPath !== identity2.repoPath || record5.identity.workspace !== identity2.workspace || record5.identity.provider !== identity2.provider || !isReliabilityCounter(record5.revision) || !isReliabilityCounter(record5.effectSequence)) {
|
|
7904
8133
|
throw new Error("Reliability record has invalid version, ownership, or counters.");
|
|
@@ -8039,6 +8268,7 @@ function updateReliabilityOperation(identity2, operation) {
|
|
|
8039
8268
|
if (result && typeof result.then === "function") {
|
|
8040
8269
|
throw new Error("Reliability record updates must be synchronous.");
|
|
8041
8270
|
}
|
|
8271
|
+
record5.writtenByVersion = installedCliVersion();
|
|
8042
8272
|
validate(record5, identity2);
|
|
8043
8273
|
record5.revision += 1;
|
|
8044
8274
|
persist(record5);
|
|
@@ -12785,7 +13015,9 @@ async function withLifecycleOperationLock(repoPath, operation) {
|
|
|
12785
13015
|
function stepRecord(record5, event) {
|
|
12786
13016
|
const transition2 = stepReliability(record5.state, event, Date.now());
|
|
12787
13017
|
if (!["accepted", "joined"].includes(transition2.outcome))
|
|
12788
|
-
throw new Error(
|
|
13018
|
+
throw new Error(
|
|
13019
|
+
`Lifecycle transition is ${transition2.outcome}.${transition2.reason ? ` ${transition2.reason}` : ""}`
|
|
13020
|
+
);
|
|
12789
13021
|
record5.state = transition2.state;
|
|
12790
13022
|
}
|
|
12791
13023
|
function reconcileDrained(record5) {
|