@devrouter/cli 0.0.66 → 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 +175 -19
- package/dist/devrouter.js +282 -23
- package/package.json +1 -1
- 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");
|
|
@@ -4863,7 +4875,7 @@ function loadRepoConfig(repoPath, read2 = (file) => import_node_fs14.default.rea
|
|
|
4863
4875
|
const config = parseConfig(parsed ?? {}, configPath);
|
|
4864
4876
|
const requiredVersion = config.devrouter?.version;
|
|
4865
4877
|
if (requiredVersion && !hasWarnedVersionMismatch) {
|
|
4866
|
-
const cliVersion = true ? "0.0.
|
|
4878
|
+
const cliVersion = true ? "0.0.67" : "0.0.0-dev";
|
|
4867
4879
|
if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
|
|
4868
4880
|
hasWarnedVersionMismatch = true;
|
|
4869
4881
|
process.stderr.write(
|
|
@@ -5418,6 +5430,66 @@ function stopRetainedManagedDevsyWorkspace(options) {
|
|
|
5418
5430
|
}
|
|
5419
5431
|
return owner.workspace;
|
|
5420
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
|
+
}
|
|
5421
5493
|
const context = registration2().context;
|
|
5422
5494
|
if (!context || !/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(context) || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(devsyId)) {
|
|
5423
5495
|
throw new Error("Managed stop requires a valid exact Devsy context and workspace ID.");
|
|
@@ -7396,6 +7468,7 @@ function assertReliabilityEvent(value) {
|
|
|
7396
7468
|
case "launched":
|
|
7397
7469
|
case "interrupted":
|
|
7398
7470
|
case "not-started":
|
|
7471
|
+
case "settle":
|
|
7399
7472
|
valid = isReliabilityId(value.operationId);
|
|
7400
7473
|
break;
|
|
7401
7474
|
case "completion":
|
|
@@ -7465,8 +7538,13 @@ function transition(state, outcome, effects = []) {
|
|
|
7465
7538
|
assertReliabilityState(state);
|
|
7466
7539
|
return { state, effects, outcome };
|
|
7467
7540
|
}
|
|
7468
|
-
function unchanged(state, outcome) {
|
|
7469
|
-
|
|
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);
|
|
7470
7548
|
}
|
|
7471
7549
|
function advance(value) {
|
|
7472
7550
|
return value === Number.MAX_SAFE_INTEGER ? null : value + 1;
|
|
@@ -7491,7 +7569,10 @@ function updateRunningPhase(state) {
|
|
|
7491
7569
|
}
|
|
7492
7570
|
function handleRequest(state, event) {
|
|
7493
7571
|
if (state.executionPolicy === "manual" && event.mode === "start")
|
|
7494
|
-
return
|
|
7572
|
+
return blocked(
|
|
7573
|
+
state,
|
|
7574
|
+
"start requests are not admitted under manual execution policy; use the ensure/stop commands."
|
|
7575
|
+
);
|
|
7495
7576
|
const existing = state.requests.find((request) => request.key === event.key);
|
|
7496
7577
|
if (existing) {
|
|
7497
7578
|
const consumer = state.consumers.find((candidate) => candidate.id === existing.consumerId);
|
|
@@ -7558,7 +7639,8 @@ function handleRequest(state, event) {
|
|
|
7558
7639
|
}
|
|
7559
7640
|
function canExecAfterInterruptedEnsure(state) {
|
|
7560
7641
|
const latestEnsure = [...state.operationHistory].reverse().find((operation) => operation.kind === "ensure");
|
|
7561
|
-
|
|
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));
|
|
7562
7644
|
}
|
|
7563
7645
|
function handleOperationRequest(state, event) {
|
|
7564
7646
|
if (state.executionPolicy !== "manual") return unchanged(state, "blocked");
|
|
@@ -7571,25 +7653,39 @@ function handleOperationRequest(state, event) {
|
|
|
7571
7653
|
if (state.operationHistory.some((entry) => entry.id === event.operationId))
|
|
7572
7654
|
return unchanged(state, "conflict");
|
|
7573
7655
|
if (event.kind === "exec" && canExecAfterInterruptedEnsure(state) && event.recoverInterruptedEnsure !== true)
|
|
7574
|
-
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
|
+
);
|
|
7575
7660
|
const fullyStopped = state.stopProof.workloadsStopped && state.stopProof.routesRemoved;
|
|
7576
7661
|
const reconcileEnsure = event.kind === "ensure" && state.operation?.kind === "ensure" && ["NOT_STARTED", "INTERRUPTED"].includes(state.operation.status);
|
|
7577
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))))
|
|
7578
|
-
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
|
+
);
|
|
7579
7667
|
if (state.phase === "stopping" || state.desired === "parked-for-capacity")
|
|
7580
|
-
return
|
|
7668
|
+
return blocked(
|
|
7669
|
+
state,
|
|
7670
|
+
`phase is '${state.phase}' with desired '${state.desired}'; wait for the running stop to finish.`
|
|
7671
|
+
);
|
|
7581
7672
|
if (event.kind === "exec" && (!event.runtimeRunning || state.desired !== "running" && state.intentRevision !== 0))
|
|
7582
|
-
return
|
|
7673
|
+
return blocked(
|
|
7674
|
+
state,
|
|
7675
|
+
`exec requires a running workspace (runtimeRunning=${event.runtimeRunning}, desired='${state.desired}').`
|
|
7676
|
+
);
|
|
7583
7677
|
const rollover = state.operationHistory.length >= RELIABILITY_MAX_ITEMS;
|
|
7584
7678
|
let retired = -1;
|
|
7585
7679
|
if (rollover) {
|
|
7586
|
-
if (state.operation && (!state.operation.drained || !["COMPLETED", "NOT_LAUNCHED"].includes(state.operation.status)))
|
|
7587
|
-
return unchanged(state, "blocked");
|
|
7588
7680
|
const latestEnsureId = [...state.operationHistory].reverse().find((entry) => entry.kind === "ensure")?.id;
|
|
7589
7681
|
retired = state.operationHistory.findIndex(
|
|
7590
|
-
(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)
|
|
7591
7683
|
);
|
|
7592
|
-
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
|
+
);
|
|
7593
7689
|
}
|
|
7594
7690
|
if (state.desired !== "running" || rollover) {
|
|
7595
7691
|
const revision = advance(state.intentRevision);
|
|
@@ -7644,7 +7740,10 @@ function handleDispatch(state) {
|
|
|
7644
7740
|
if (state.operation?.drained || state.operation?.status !== "NOT_STARTED")
|
|
7645
7741
|
return unchanged(state, "stale");
|
|
7646
7742
|
if (state.desired !== "running" || state.executionPolicy === "capacity-managed" && state.admission !== "admitted" || state.consumers.length === 0) {
|
|
7647
|
-
return
|
|
7743
|
+
return blocked(
|
|
7744
|
+
state,
|
|
7745
|
+
`dispatch requires desired 'running' with a registered consumer (desired='${state.desired}', consumers=${state.consumers.length}).`
|
|
7746
|
+
);
|
|
7648
7747
|
}
|
|
7649
7748
|
const corrective = state.phase === "recovering" && state.incident !== null;
|
|
7650
7749
|
state.operation = { ...state.operation, status: "DISPATCH_PENDING", exitCode: null };
|
|
@@ -7659,7 +7758,10 @@ function handleDispatchPersisted(state, event) {
|
|
|
7659
7758
|
if (state.desired !== "running" || state.executionPolicy === "capacity-managed" && (state.admission !== "admitted" || !state.chargeHeld)) {
|
|
7660
7759
|
state.operation = setUnknown(state.operation);
|
|
7661
7760
|
state.chargeHeld = state.executionPolicy === "capacity-managed";
|
|
7662
|
-
return
|
|
7761
|
+
return blocked(
|
|
7762
|
+
state,
|
|
7763
|
+
`dispatch persistence requires desired 'running' (desired='${state.desired}'); the operation outcome is unknown.`
|
|
7764
|
+
);
|
|
7663
7765
|
}
|
|
7664
7766
|
if (state.phase === "recovering" && state.incident !== null && state.incident.correctiveActionsTaken >= state.incident.actionLimit) {
|
|
7665
7767
|
return unchanged(state, "blocked");
|
|
@@ -7707,6 +7809,22 @@ function handleInterrupted(state, event) {
|
|
|
7707
7809
|
if (state.desired === "running") state.phase = "recovering";
|
|
7708
7810
|
return transition(state, "accepted");
|
|
7709
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
|
+
}
|
|
7710
7828
|
function handleObservation(state, event, nowMs) {
|
|
7711
7829
|
if (state.desired !== "running") return unchanged(state, "stale");
|
|
7712
7830
|
const previous = state.observations.find(
|
|
@@ -7716,7 +7834,10 @@ function handleObservation(state, event, nowMs) {
|
|
|
7716
7834
|
return unchanged(state, "stale");
|
|
7717
7835
|
}
|
|
7718
7836
|
if (!previous && state.observations.length >= RELIABILITY_MAX_ITEMS)
|
|
7719
|
-
return
|
|
7837
|
+
return blocked(
|
|
7838
|
+
state,
|
|
7839
|
+
`observation set holds ${state.observations.length} distinct capabilities; observations repeat per capability and never retire.`
|
|
7840
|
+
);
|
|
7720
7841
|
state.observations = state.observations.filter(
|
|
7721
7842
|
(observation) => observation.capability !== event.observation.capability
|
|
7722
7843
|
);
|
|
@@ -7744,7 +7865,10 @@ function handleStop(state) {
|
|
|
7744
7865
|
function handleStopProof(state, event) {
|
|
7745
7866
|
if (state.desired === "running") return unchanged(state, "stale");
|
|
7746
7867
|
if (state.executionPolicy === "manual" && state.operation && !state.operation.drained && event.workloadsStopped && event.routesRemoved)
|
|
7747
|
-
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
|
+
);
|
|
7748
7872
|
const previous = { ...state.stopProof };
|
|
7749
7873
|
if (previous.workloadsStopped === event.workloadsStopped && previous.routesRemoved === event.routesRemoved)
|
|
7750
7874
|
return unchanged(state, "joined");
|
|
@@ -7923,6 +8047,8 @@ function applyReliabilityEvent(state, event, nowMs) {
|
|
|
7923
8047
|
return handleCompletion(next, event);
|
|
7924
8048
|
case "interrupted":
|
|
7925
8049
|
return handleInterrupted(next, event);
|
|
8050
|
+
case "settle":
|
|
8051
|
+
return handleSettle(next, event);
|
|
7926
8052
|
case "observation":
|
|
7927
8053
|
return handleObservation(next, event, nowMs);
|
|
7928
8054
|
case "stop-proof":
|
|
@@ -7958,6 +8084,19 @@ function stepReliability(state, event, nowMs) {
|
|
|
7958
8084
|
var import_node_crypto11 = require("crypto");
|
|
7959
8085
|
var import_node_fs20 = __toESM(require("fs"));
|
|
7960
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
|
+
}
|
|
7961
8100
|
var MAX_RECORD_BYTES = 1048576;
|
|
7962
8101
|
function identityKey(identity2) {
|
|
7963
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)) {
|
|
@@ -7974,7 +8113,21 @@ function keys(value, expected) {
|
|
|
7974
8113
|
}
|
|
7975
8114
|
}
|
|
7976
8115
|
function validate(record5, identity2) {
|
|
7977
|
-
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
|
+
}
|
|
7978
8131
|
keys(record5.identity, ["repoPath", "workspace", "provider"]);
|
|
7979
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)) {
|
|
7980
8133
|
throw new Error("Reliability record has invalid version, ownership, or counters.");
|
|
@@ -8115,6 +8268,7 @@ function updateReliabilityOperation(identity2, operation) {
|
|
|
8115
8268
|
if (result && typeof result.then === "function") {
|
|
8116
8269
|
throw new Error("Reliability record updates must be synchronous.");
|
|
8117
8270
|
}
|
|
8271
|
+
record5.writtenByVersion = installedCliVersion();
|
|
8118
8272
|
validate(record5, identity2);
|
|
8119
8273
|
record5.revision += 1;
|
|
8120
8274
|
persist(record5);
|
|
@@ -12861,7 +13015,9 @@ async function withLifecycleOperationLock(repoPath, operation) {
|
|
|
12861
13015
|
function stepRecord(record5, event) {
|
|
12862
13016
|
const transition2 = stepReliability(record5.state, event, Date.now());
|
|
12863
13017
|
if (!["accepted", "joined"].includes(transition2.outcome))
|
|
12864
|
-
throw new Error(
|
|
13018
|
+
throw new Error(
|
|
13019
|
+
`Lifecycle transition is ${transition2.outcome}.${transition2.reason ? ` ${transition2.reason}` : ""}`
|
|
13020
|
+
);
|
|
12865
13021
|
record5.state = transition2.state;
|
|
12866
13022
|
}
|
|
12867
13023
|
function reconcileDrained(record5) {
|
package/dist/devrouter.js
CHANGED
|
@@ -2520,7 +2520,7 @@ function loadRepoConfig(repoPath, read2 = (file) => import_node_fs6.default.read
|
|
|
2520
2520
|
const config = parseConfig(parsed ?? {}, configPath);
|
|
2521
2521
|
const requiredVersion = config.devrouter?.version;
|
|
2522
2522
|
if (requiredVersion && !hasWarnedVersionMismatch) {
|
|
2523
|
-
const cliVersion = true ? "0.0.
|
|
2523
|
+
const cliVersion = true ? "0.0.67" : "0.0.0-dev";
|
|
2524
2524
|
if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
|
|
2525
2525
|
hasWarnedVersionMismatch = true;
|
|
2526
2526
|
process.stderr.write(
|
|
@@ -3494,6 +3494,19 @@ var init_reliability_contract = __esm({
|
|
|
3494
3494
|
});
|
|
3495
3495
|
|
|
3496
3496
|
// src/core/reliability-operation-store.ts
|
|
3497
|
+
function installedCliVersion() {
|
|
3498
|
+
return true ? "0.0.67" : "0.0.0-dev";
|
|
3499
|
+
}
|
|
3500
|
+
function compareCliVersions(left, right) {
|
|
3501
|
+
const parts = (value) => value.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
3502
|
+
const leftParts = parts(left);
|
|
3503
|
+
const rightParts = parts(right);
|
|
3504
|
+
for (let index = 0; index < 3; index += 1) {
|
|
3505
|
+
const difference = (leftParts[index] ?? 0) - (rightParts[index] ?? 0);
|
|
3506
|
+
if (difference !== 0) return difference;
|
|
3507
|
+
}
|
|
3508
|
+
return 0;
|
|
3509
|
+
}
|
|
3497
3510
|
function identityKey(identity2) {
|
|
3498
3511
|
if (!import_node_path10.default.isAbsolute(identity2.repoPath) || import_node_path10.default.resolve(identity2.repoPath) !== identity2.repoPath || identity2.repoPath.includes("\0") || identity2.workspace !== null && !isReliabilityId(identity2.workspace) || !["devpod", "devsy"].includes(identity2.provider)) {
|
|
3499
3512
|
throw new Error("Invalid reliability workspace identity.");
|
|
@@ -3509,7 +3522,21 @@ function keys(value, expected) {
|
|
|
3509
3522
|
}
|
|
3510
3523
|
}
|
|
3511
3524
|
function validate(record5, identity2) {
|
|
3512
|
-
keys(record5, [
|
|
3525
|
+
keys(record5, [
|
|
3526
|
+
"version",
|
|
3527
|
+
"identity",
|
|
3528
|
+
"revision",
|
|
3529
|
+
"state",
|
|
3530
|
+
"worker",
|
|
3531
|
+
"effectSequence",
|
|
3532
|
+
"outcome",
|
|
3533
|
+
"writtenByVersion"
|
|
3534
|
+
]);
|
|
3535
|
+
if (record5.writtenByVersion !== void 0 && compareCliVersions(record5.writtenByVersion, installedCliVersion()) > 0) {
|
|
3536
|
+
throw new Error(
|
|
3537
|
+
`Reliability record was last written by devrouter ${record5.writtenByVersion}, which is newer than the installed ${installedCliVersion()}; upgrade devrouter (devrouter upgrade) before running lifecycle commands.`
|
|
3538
|
+
);
|
|
3539
|
+
}
|
|
3513
3540
|
keys(record5.identity, ["repoPath", "workspace", "provider"]);
|
|
3514
3541
|
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)) {
|
|
3515
3542
|
throw new Error("Reliability record has invalid version, ownership, or counters.");
|
|
@@ -3650,6 +3677,7 @@ function updateReliabilityOperation(identity2, operation) {
|
|
|
3650
3677
|
if (result && typeof result.then === "function") {
|
|
3651
3678
|
throw new Error("Reliability record updates must be synchronous.");
|
|
3652
3679
|
}
|
|
3680
|
+
record5.writtenByVersion = installedCliVersion();
|
|
3653
3681
|
validate(record5, identity2);
|
|
3654
3682
|
record5.revision += 1;
|
|
3655
3683
|
persist(record5);
|
|
@@ -7462,6 +7490,10 @@ var init_ai_prompt = __esm({
|
|
|
7462
7490
|
{
|
|
7463
7491
|
command: "devrouter workspace gc [--json] [--yes]",
|
|
7464
7492
|
purpose: "Report missing owners without mutation by default; --yes deletes only exact eligible ledger-owned resources and records, never Git worktrees."
|
|
7493
|
+
},
|
|
7494
|
+
{
|
|
7495
|
+
command: "devrouter workspace journal settle [path] [--json]",
|
|
7496
|
+
purpose: "Settle a lifecycle operation whose worker is provably gone as unobservable so ensure/stop can proceed; never hand-edit the devrouter state directory."
|
|
7465
7497
|
}
|
|
7466
7498
|
];
|
|
7467
7499
|
}
|
|
@@ -11467,6 +11499,17 @@ function inspectDevsyRuntimeStatus(devsyId) {
|
|
|
11467
11499
|
if (result.status !== 0 || result.error) return "unknown";
|
|
11468
11500
|
return parseDevsyRuntimeStatus(result.stdout, devsyId);
|
|
11469
11501
|
}
|
|
11502
|
+
function inspectDevsyRuntimeAbsence(devsyId) {
|
|
11503
|
+
const result = (0, import_node_child_process16.spawnSync)("devsy", ["workspace", "status", devsyId, "--result-format", "json"], {
|
|
11504
|
+
encoding: "utf-8",
|
|
11505
|
+
timeout: 1e4
|
|
11506
|
+
});
|
|
11507
|
+
if (!result.error && result.status === 0) {
|
|
11508
|
+
return parseDevsyRuntimeStatus(result.stdout, devsyId) === "not-found";
|
|
11509
|
+
}
|
|
11510
|
+
const output2 = [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n");
|
|
11511
|
+
return /workspace not found/i.test(output2);
|
|
11512
|
+
}
|
|
11470
11513
|
function inspectDevsyWorkspaceOwnership(workspaces, devsyId, worktreePath) {
|
|
11471
11514
|
const idOwners = workspaces.filter((workspace) => workspace.id === devsyId);
|
|
11472
11515
|
const pathOwners = workspaces.filter(
|
|
@@ -13092,6 +13135,66 @@ function stopRetainedManagedDevsyWorkspace(options) {
|
|
|
13092
13135
|
}
|
|
13093
13136
|
return owner.workspace;
|
|
13094
13137
|
}
|
|
13138
|
+
function registrationStatus() {
|
|
13139
|
+
resetWorkspaceRuntimeCaches();
|
|
13140
|
+
if (resolveWorkspaceRuntimeOrDefault(repoPath) !== "devsy") {
|
|
13141
|
+
throw new Error("Managed stop provider selection changed.");
|
|
13142
|
+
}
|
|
13143
|
+
return inspectDevsyWorkspaceOwnership(listDevsyWorkspaces(), devsyId, repoPath);
|
|
13144
|
+
}
|
|
13145
|
+
function proveAbsentRegistrationStop() {
|
|
13146
|
+
const observe = () => {
|
|
13147
|
+
const owner = registrationStatus();
|
|
13148
|
+
if (owner.status === "conflict") throw new Error(owner.reason);
|
|
13149
|
+
if (owner.status !== "absent") {
|
|
13150
|
+
throw new Error("Managed stop absence proof requires the registration to remain absent.");
|
|
13151
|
+
}
|
|
13152
|
+
if (listDevpodWorkspacesRaw({ allowMissingExecutable: true }).some(
|
|
13153
|
+
(entry) => entry.id === devsyId || sameWorkspacePath(entry.source.localFolder, repoPath)
|
|
13154
|
+
)) {
|
|
13155
|
+
throw new Error(
|
|
13156
|
+
"Managed stop absence proof requires both provider registrations to remain absent."
|
|
13157
|
+
);
|
|
13158
|
+
}
|
|
13159
|
+
if (!inspectDevsyRuntimeAbsence(devsyId)) {
|
|
13160
|
+
throw new Error(
|
|
13161
|
+
"Managed stop absence proof requires Devsy to report the runtime not-found."
|
|
13162
|
+
);
|
|
13163
|
+
}
|
|
13164
|
+
if (inspectManagedStopContainers(state.composeProject).length !== 0) {
|
|
13165
|
+
throw new Error("Managed stop absence proof observed a remaining compose population.");
|
|
13166
|
+
}
|
|
13167
|
+
if (inspectProviderRunnerContainers(resolveManagedStopEndpoint(), devsyId).length !== 0) {
|
|
13168
|
+
throw new Error("Managed stop absence proof observed a remaining runner population.");
|
|
13169
|
+
}
|
|
13170
|
+
const owned = inspectWorkspaceContainers().filter(
|
|
13171
|
+
(container) => sameWorkspacePath(
|
|
13172
|
+
container.labels["com.docker.compose.project.working_dir"] ?? "",
|
|
13173
|
+
import_node_path36.default.join(repoPath, ".devcontainer")
|
|
13174
|
+
)
|
|
13175
|
+
);
|
|
13176
|
+
if (owned.length !== 0) {
|
|
13177
|
+
throw new Error("Managed stop absence proof observed a remaining workspace container.");
|
|
13178
|
+
}
|
|
13179
|
+
if (workspace) {
|
|
13180
|
+
const record5 = readWorkspaceOwnership(repoPath, workspace);
|
|
13181
|
+
if (!record5 || record5.devpodId !== devsyId || !sameWorkspacePath(record5.worktreePath, repoPath) || resolveWorktreeWorkspace(repoPath) !== workspace || resolveGitCommonDir(repoPath) !== workspaceEnv?.gitCommonDir || inspectWorkspaceOwnership(record5, listGitWorktrees(repoPath), void 0).ownerStatus !== "present") {
|
|
13182
|
+
throw new Error("Managed stop workspace ownership changed during absence proof.");
|
|
13183
|
+
}
|
|
13184
|
+
}
|
|
13185
|
+
if (!(0, import_node_util4.isDeepStrictEqual)(readManagedRuntimeState(repoPath, workspace), state)) {
|
|
13186
|
+
throw new Error("Managed stop retained state changed during absence proof.");
|
|
13187
|
+
}
|
|
13188
|
+
};
|
|
13189
|
+
observe();
|
|
13190
|
+
observe();
|
|
13191
|
+
}
|
|
13192
|
+
const initialOwner = registrationStatus();
|
|
13193
|
+
if (initialOwner.status === "conflict") throw new Error(initialOwner.reason);
|
|
13194
|
+
if (initialOwner.status === "absent") {
|
|
13195
|
+
proveAbsentRegistrationStop();
|
|
13196
|
+
return "proven-absent";
|
|
13197
|
+
}
|
|
13095
13198
|
const context = registration2().context;
|
|
13096
13199
|
if (!context || !/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(context) || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(devsyId)) {
|
|
13097
13200
|
throw new Error("Managed stop requires a valid exact Devsy context and workspace ID.");
|
|
@@ -13201,14 +13304,16 @@ function stopRetainedManagedDevsyWorkspace(options) {
|
|
|
13201
13304
|
if (providerFailed) throw providerError;
|
|
13202
13305
|
return true;
|
|
13203
13306
|
}
|
|
13204
|
-
var import_node_os4, import_node_path36;
|
|
13307
|
+
var import_node_os4, import_node_path36, import_node_util4;
|
|
13205
13308
|
var init_managed_devsy_stop = __esm({
|
|
13206
13309
|
"src/core/managed-devsy-stop.ts"() {
|
|
13207
13310
|
"use strict";
|
|
13208
13311
|
import_node_os4 = __toESM(require("os"));
|
|
13209
13312
|
import_node_path36 = __toESM(require("path"));
|
|
13313
|
+
import_node_util4 = require("util");
|
|
13210
13314
|
init_devcontainer_profile();
|
|
13211
13315
|
init_devpod_environment();
|
|
13316
|
+
init_devpod_registry();
|
|
13212
13317
|
init_devsy_workspaces();
|
|
13213
13318
|
init_managed_compose_population();
|
|
13214
13319
|
init_managed_runtime_state();
|
|
@@ -14185,7 +14290,7 @@ async function buildDoctorReport(options = {}) {
|
|
|
14185
14290
|
const config = runtimeConfig.config;
|
|
14186
14291
|
loadedConfig = config;
|
|
14187
14292
|
loadedWorkspace = runtimeConfig.workspace;
|
|
14188
|
-
const cliVersion = true ? "0.0.
|
|
14293
|
+
const cliVersion = true ? "0.0.67" : "0.0.0-dev";
|
|
14189
14294
|
const configVersion = config.devrouter?.version;
|
|
14190
14295
|
if (configVersion && cliVersion !== "0.0.0-dev" && compareSemver(configVersion, cliVersion) > 0) {
|
|
14191
14296
|
addCheck(checks, {
|
|
@@ -14871,6 +14976,7 @@ function assertReliabilityEvent(value) {
|
|
|
14871
14976
|
case "launched":
|
|
14872
14977
|
case "interrupted":
|
|
14873
14978
|
case "not-started":
|
|
14979
|
+
case "settle":
|
|
14874
14980
|
valid = isReliabilityId(value.operationId);
|
|
14875
14981
|
break;
|
|
14876
14982
|
case "completion":
|
|
@@ -14940,8 +15046,13 @@ function transition(state, outcome, effects = []) {
|
|
|
14940
15046
|
assertReliabilityState(state);
|
|
14941
15047
|
return { state, effects, outcome };
|
|
14942
15048
|
}
|
|
14943
|
-
function unchanged(state, outcome) {
|
|
14944
|
-
|
|
15049
|
+
function unchanged(state, outcome, reason) {
|
|
15050
|
+
const result = transition(cloneState(state), outcome);
|
|
15051
|
+
if (reason) result.reason = reason;
|
|
15052
|
+
return result;
|
|
15053
|
+
}
|
|
15054
|
+
function blocked2(state, reason) {
|
|
15055
|
+
return unchanged(state, "blocked", reason);
|
|
14945
15056
|
}
|
|
14946
15057
|
function advance(value) {
|
|
14947
15058
|
return value === Number.MAX_SAFE_INTEGER ? null : value + 1;
|
|
@@ -14966,7 +15077,10 @@ function updateRunningPhase(state) {
|
|
|
14966
15077
|
}
|
|
14967
15078
|
function handleRequest(state, event) {
|
|
14968
15079
|
if (state.executionPolicy === "manual" && event.mode === "start")
|
|
14969
|
-
return
|
|
15080
|
+
return blocked2(
|
|
15081
|
+
state,
|
|
15082
|
+
"start requests are not admitted under manual execution policy; use the ensure/stop commands."
|
|
15083
|
+
);
|
|
14970
15084
|
const existing = state.requests.find((request) => request.key === event.key);
|
|
14971
15085
|
if (existing) {
|
|
14972
15086
|
const consumer = state.consumers.find((candidate) => candidate.id === existing.consumerId);
|
|
@@ -15033,7 +15147,8 @@ function handleRequest(state, event) {
|
|
|
15033
15147
|
}
|
|
15034
15148
|
function canExecAfterInterruptedEnsure(state) {
|
|
15035
15149
|
const latestEnsure = [...state.operationHistory].reverse().find((operation) => operation.kind === "ensure");
|
|
15036
|
-
|
|
15150
|
+
const quiescent = state.stopProof.workloadsStopped && state.stopProof.routesRemoved;
|
|
15151
|
+
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));
|
|
15037
15152
|
}
|
|
15038
15153
|
function handleOperationRequest(state, event) {
|
|
15039
15154
|
if (state.executionPolicy !== "manual") return unchanged(state, "blocked");
|
|
@@ -15046,25 +15161,39 @@ function handleOperationRequest(state, event) {
|
|
|
15046
15161
|
if (state.operationHistory.some((entry) => entry.id === event.operationId))
|
|
15047
15162
|
return unchanged(state, "conflict");
|
|
15048
15163
|
if (event.kind === "exec" && canExecAfterInterruptedEnsure(state) && event.recoverInterruptedEnsure !== true)
|
|
15049
|
-
return
|
|
15164
|
+
return blocked2(
|
|
15165
|
+
state,
|
|
15166
|
+
"the latest ensure is INTERRUPTED and drained; exec requires recovering it first (run ensure, or exec with recovery proof)."
|
|
15167
|
+
);
|
|
15050
15168
|
const fullyStopped = state.stopProof.workloadsStopped && state.stopProof.routesRemoved;
|
|
15051
15169
|
const reconcileEnsure = event.kind === "ensure" && state.operation?.kind === "ensure" && ["NOT_STARTED", "INTERRUPTED"].includes(state.operation.status);
|
|
15052
15170
|
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))))
|
|
15053
|
-
return
|
|
15171
|
+
return blocked2(
|
|
15172
|
+
state,
|
|
15173
|
+
`operation ${state.operation.id} is ${state.operation.status} (drained=${state.operation.drained}) and no stop proof, ensure reconciliation, or exec recovery supersedes it.`
|
|
15174
|
+
);
|
|
15054
15175
|
if (state.phase === "stopping" || state.desired === "parked-for-capacity")
|
|
15055
|
-
return
|
|
15176
|
+
return blocked2(
|
|
15177
|
+
state,
|
|
15178
|
+
`phase is '${state.phase}' with desired '${state.desired}'; wait for the running stop to finish.`
|
|
15179
|
+
);
|
|
15056
15180
|
if (event.kind === "exec" && (!event.runtimeRunning || state.desired !== "running" && state.intentRevision !== 0))
|
|
15057
|
-
return
|
|
15181
|
+
return blocked2(
|
|
15182
|
+
state,
|
|
15183
|
+
`exec requires a running workspace (runtimeRunning=${event.runtimeRunning}, desired='${state.desired}').`
|
|
15184
|
+
);
|
|
15058
15185
|
const rollover = state.operationHistory.length >= RELIABILITY_MAX_ITEMS;
|
|
15059
15186
|
let retired = -1;
|
|
15060
15187
|
if (rollover) {
|
|
15061
|
-
if (state.operation && (!state.operation.drained || !["COMPLETED", "NOT_LAUNCHED"].includes(state.operation.status)))
|
|
15062
|
-
return unchanged(state, "blocked");
|
|
15063
15188
|
const latestEnsureId = [...state.operationHistory].reverse().find((entry) => entry.kind === "ensure")?.id;
|
|
15064
15189
|
retired = state.operationHistory.findIndex(
|
|
15065
|
-
(entry) => entry.id !== state.operation?.id && entry.id !== latestEnsureId && entry.drained && ["COMPLETED", "NOT_LAUNCHED"].includes(entry.status)
|
|
15190
|
+
(entry) => entry.id !== state.operation?.id && entry.id !== latestEnsureId && entry.drained && ["COMPLETED", "NOT_LAUNCHED", "INTERRUPTED"].includes(entry.status)
|
|
15066
15191
|
);
|
|
15067
|
-
if (retired === -1)
|
|
15192
|
+
if (retired === -1)
|
|
15193
|
+
return blocked2(
|
|
15194
|
+
state,
|
|
15195
|
+
`journal holds ${state.operationHistory.length} entries and every retirable entry is the current operation or the latest ensure.`
|
|
15196
|
+
);
|
|
15068
15197
|
}
|
|
15069
15198
|
if (state.desired !== "running" || rollover) {
|
|
15070
15199
|
const revision = advance(state.intentRevision);
|
|
@@ -15119,7 +15248,10 @@ function handleDispatch(state) {
|
|
|
15119
15248
|
if (state.operation?.drained || state.operation?.status !== "NOT_STARTED")
|
|
15120
15249
|
return unchanged(state, "stale");
|
|
15121
15250
|
if (state.desired !== "running" || state.executionPolicy === "capacity-managed" && state.admission !== "admitted" || state.consumers.length === 0) {
|
|
15122
|
-
return
|
|
15251
|
+
return blocked2(
|
|
15252
|
+
state,
|
|
15253
|
+
`dispatch requires desired 'running' with a registered consumer (desired='${state.desired}', consumers=${state.consumers.length}).`
|
|
15254
|
+
);
|
|
15123
15255
|
}
|
|
15124
15256
|
const corrective = state.phase === "recovering" && state.incident !== null;
|
|
15125
15257
|
state.operation = { ...state.operation, status: "DISPATCH_PENDING", exitCode: null };
|
|
@@ -15134,7 +15266,10 @@ function handleDispatchPersisted(state, event) {
|
|
|
15134
15266
|
if (state.desired !== "running" || state.executionPolicy === "capacity-managed" && (state.admission !== "admitted" || !state.chargeHeld)) {
|
|
15135
15267
|
state.operation = setUnknown(state.operation);
|
|
15136
15268
|
state.chargeHeld = state.executionPolicy === "capacity-managed";
|
|
15137
|
-
return
|
|
15269
|
+
return blocked2(
|
|
15270
|
+
state,
|
|
15271
|
+
`dispatch persistence requires desired 'running' (desired='${state.desired}'); the operation outcome is unknown.`
|
|
15272
|
+
);
|
|
15138
15273
|
}
|
|
15139
15274
|
if (state.phase === "recovering" && state.incident !== null && state.incident.correctiveActionsTaken >= state.incident.actionLimit) {
|
|
15140
15275
|
return unchanged(state, "blocked");
|
|
@@ -15182,6 +15317,22 @@ function handleInterrupted(state, event) {
|
|
|
15182
15317
|
if (state.desired === "running") state.phase = "recovering";
|
|
15183
15318
|
return transition(state, "accepted");
|
|
15184
15319
|
}
|
|
15320
|
+
function handleSettle(state, event) {
|
|
15321
|
+
if (state.executionPolicy !== "manual") return unchanged(state, "stale");
|
|
15322
|
+
if (!state.operation || state.operation.id !== event.operationId)
|
|
15323
|
+
return unchanged(state, "stale");
|
|
15324
|
+
if (state.operation.drained && state.operation.status === "INTERRUPTED")
|
|
15325
|
+
return unchanged(state, "joined");
|
|
15326
|
+
if (state.operation.status === "COMPLETED") return unchanged(state, "joined");
|
|
15327
|
+
state.operation = {
|
|
15328
|
+
...state.operation,
|
|
15329
|
+
status: "INTERRUPTED",
|
|
15330
|
+
drained: true,
|
|
15331
|
+
exitCode: null
|
|
15332
|
+
};
|
|
15333
|
+
if (state.desired === "running") state.phase = "recovering";
|
|
15334
|
+
return transition(state, "accepted");
|
|
15335
|
+
}
|
|
15185
15336
|
function handleObservation(state, event, nowMs) {
|
|
15186
15337
|
if (state.desired !== "running") return unchanged(state, "stale");
|
|
15187
15338
|
const previous = state.observations.find(
|
|
@@ -15191,7 +15342,10 @@ function handleObservation(state, event, nowMs) {
|
|
|
15191
15342
|
return unchanged(state, "stale");
|
|
15192
15343
|
}
|
|
15193
15344
|
if (!previous && state.observations.length >= RELIABILITY_MAX_ITEMS)
|
|
15194
|
-
return
|
|
15345
|
+
return blocked2(
|
|
15346
|
+
state,
|
|
15347
|
+
`observation set holds ${state.observations.length} distinct capabilities; observations repeat per capability and never retire.`
|
|
15348
|
+
);
|
|
15195
15349
|
state.observations = state.observations.filter(
|
|
15196
15350
|
(observation) => observation.capability !== event.observation.capability
|
|
15197
15351
|
);
|
|
@@ -15219,7 +15373,10 @@ function handleStop(state) {
|
|
|
15219
15373
|
function handleStopProof(state, event) {
|
|
15220
15374
|
if (state.desired === "running") return unchanged(state, "stale");
|
|
15221
15375
|
if (state.executionPolicy === "manual" && state.operation && !state.operation.drained && event.workloadsStopped && event.routesRemoved)
|
|
15222
|
-
return
|
|
15376
|
+
return blocked2(
|
|
15377
|
+
state,
|
|
15378
|
+
`stop proof cannot settle while operation ${state.operation.id} is not drained; wait for its worker or settle the journal.`
|
|
15379
|
+
);
|
|
15223
15380
|
const previous = { ...state.stopProof };
|
|
15224
15381
|
if (previous.workloadsStopped === event.workloadsStopped && previous.routesRemoved === event.routesRemoved)
|
|
15225
15382
|
return unchanged(state, "joined");
|
|
@@ -15398,6 +15555,8 @@ function applyReliabilityEvent(state, event, nowMs) {
|
|
|
15398
15555
|
return handleCompletion(next, event);
|
|
15399
15556
|
case "interrupted":
|
|
15400
15557
|
return handleInterrupted(next, event);
|
|
15558
|
+
case "settle":
|
|
15559
|
+
return handleSettle(next, event);
|
|
15401
15560
|
case "observation":
|
|
15402
15561
|
return handleObservation(next, event, nowMs);
|
|
15403
15562
|
case "stop-proof":
|
|
@@ -15558,7 +15717,9 @@ async function runLifecycleWorker(request, beforeAdmission) {
|
|
|
15558
15717
|
Date.now()
|
|
15559
15718
|
);
|
|
15560
15719
|
if (admitted.outcome !== "accepted")
|
|
15561
|
-
throw new Error(
|
|
15720
|
+
throw new Error(
|
|
15721
|
+
`Lifecycle admission is ${admitted.outcome}.${admitted.reason ? ` ${admitted.reason}` : ""}`
|
|
15722
|
+
);
|
|
15562
15723
|
record5.state = admitted.state;
|
|
15563
15724
|
record5.outcome = null;
|
|
15564
15725
|
request.fence = reliabilityFence(record5.state);
|
|
@@ -16048,7 +16209,9 @@ async function withLifecycleOperationLock(repoPath, operation) {
|
|
|
16048
16209
|
function stepRecord(record5, event) {
|
|
16049
16210
|
const transition2 = stepReliability(record5.state, event, Date.now());
|
|
16050
16211
|
if (!["accepted", "joined"].includes(transition2.outcome))
|
|
16051
|
-
throw new Error(
|
|
16212
|
+
throw new Error(
|
|
16213
|
+
`Lifecycle transition is ${transition2.outcome}.${transition2.reason ? ` ${transition2.reason}` : ""}`
|
|
16214
|
+
);
|
|
16052
16215
|
record5.state = transition2.state;
|
|
16053
16216
|
}
|
|
16054
16217
|
function reconcileDrained(record5) {
|
|
@@ -20397,6 +20560,70 @@ var init_workspace_cleanup = __esm({
|
|
|
20397
20560
|
}
|
|
20398
20561
|
});
|
|
20399
20562
|
|
|
20563
|
+
// src/core/workspace-journal-settle.ts
|
|
20564
|
+
async function settleWorkspaceJournal(repoPath) {
|
|
20565
|
+
repoPath = comparableWorkspacePath(repoPath);
|
|
20566
|
+
if (isLinkedWorktree(repoPath) && !readPersistedWorkspace(repoPath)) {
|
|
20567
|
+
throw new Error("Settle requires the existing linked workspace identity.");
|
|
20568
|
+
}
|
|
20569
|
+
const identity2 = {
|
|
20570
|
+
repoPath,
|
|
20571
|
+
workspace: resolveWorktreeWorkspace(repoPath) ?? null,
|
|
20572
|
+
provider: resolveWorkspaceRuntimeOrDefault(repoPath)
|
|
20573
|
+
};
|
|
20574
|
+
if (!readReliabilityOperation(identity2)) {
|
|
20575
|
+
throw new Error(`No reliability journal exists for '${repoPath}'; nothing to settle.`);
|
|
20576
|
+
}
|
|
20577
|
+
const settled = await withWorkspaceLifecycleLock(
|
|
20578
|
+
repoPath,
|
|
20579
|
+
async () => updateReliabilityOperation(identity2, (record5) => settleRecord(record5))
|
|
20580
|
+
);
|
|
20581
|
+
return { repoPath, workspace: identity2.workspace, ...settled };
|
|
20582
|
+
}
|
|
20583
|
+
function settleRecord(record5) {
|
|
20584
|
+
if (record5.worker) {
|
|
20585
|
+
const birth = processBirthIdentity(record5.worker.pid);
|
|
20586
|
+
if (birth === record5.worker.birth || !workerGroupAbsent(record5.worker.pid)) {
|
|
20587
|
+
throw new Error(
|
|
20588
|
+
`Lifecycle worker '${record5.worker.id}' is still running; settle is refused while its outcome is observable.`
|
|
20589
|
+
);
|
|
20590
|
+
}
|
|
20591
|
+
reconcileDrained(record5);
|
|
20592
|
+
if (record5.worker) throw new Error("Dead lifecycle worker could not be drained.");
|
|
20593
|
+
}
|
|
20594
|
+
const operation = record5.state.operation;
|
|
20595
|
+
if (!operation) throw new Error("No lifecycle operation is recorded; nothing to settle.");
|
|
20596
|
+
const priorStatus = operation.status;
|
|
20597
|
+
const transition2 = stepReliability(
|
|
20598
|
+
record5.state,
|
|
20599
|
+
{ ...reliabilityFence(record5.state), type: "settle", operationId: operation.id },
|
|
20600
|
+
Date.now()
|
|
20601
|
+
);
|
|
20602
|
+
if (!["accepted", "joined"].includes(transition2.outcome))
|
|
20603
|
+
throw new Error(
|
|
20604
|
+
`Lifecycle settlement is ${transition2.outcome}.${transition2.reason ? ` ${transition2.reason}` : ""}`
|
|
20605
|
+
);
|
|
20606
|
+
record5.state = transition2.state;
|
|
20607
|
+
return {
|
|
20608
|
+
operationId: operation.id,
|
|
20609
|
+
status: transition2.outcome === "joined" ? "already-settled" : "settled",
|
|
20610
|
+
priorStatus
|
|
20611
|
+
};
|
|
20612
|
+
}
|
|
20613
|
+
var init_workspace_journal_settle = __esm({
|
|
20614
|
+
"src/core/workspace-journal-settle.ts"() {
|
|
20615
|
+
"use strict";
|
|
20616
|
+
init_file_lock();
|
|
20617
|
+
init_reliability_contract();
|
|
20618
|
+
init_reliability_lifecycle();
|
|
20619
|
+
init_reliability_model();
|
|
20620
|
+
init_reliability_operation_store();
|
|
20621
|
+
init_reliability_worker();
|
|
20622
|
+
init_workspace();
|
|
20623
|
+
init_workspace_runtime();
|
|
20624
|
+
}
|
|
20625
|
+
});
|
|
20626
|
+
|
|
20400
20627
|
// src/core/workspace-lifecycle.ts
|
|
20401
20628
|
function withWorkspaceAllocationTransaction(repoPath, operation) {
|
|
20402
20629
|
return withWorkspaceOwnershipTransaction(repoPath, operation, {
|
|
@@ -20800,6 +21027,7 @@ __export(workspace_exports, {
|
|
|
20800
21027
|
runWorkspaceCleanupCommand: () => runWorkspaceCleanupCommand,
|
|
20801
21028
|
runWorkspaceDownCommand: () => runWorkspaceDownCommand,
|
|
20802
21029
|
runWorkspaceGcCommand: () => runWorkspaceGcCommand,
|
|
21030
|
+
runWorkspaceJournalSettleCommand: () => runWorkspaceJournalSettleCommand,
|
|
20803
21031
|
runWorkspaceLsCommand: () => runWorkspaceLsCommand,
|
|
20804
21032
|
runWorkspaceStopCommand: () => runWorkspaceStopCommand,
|
|
20805
21033
|
runWorkspaceUpCommand: () => runWorkspaceUpCommand
|
|
@@ -20889,6 +21117,26 @@ function runWorkspaceGcCommand(options) {
|
|
|
20889
21117
|
}
|
|
20890
21118
|
if (report.summary.errors > 0) process.exitCode = 1;
|
|
20891
21119
|
}
|
|
21120
|
+
async function runWorkspaceJournalSettleCommand(options) {
|
|
21121
|
+
const repoPath = resolveGitCheckoutPath(options.path);
|
|
21122
|
+
const result = await settleWorkspaceJournal(repoPath);
|
|
21123
|
+
if (options.json) {
|
|
21124
|
+
printJSON(result);
|
|
21125
|
+
return;
|
|
21126
|
+
}
|
|
21127
|
+
const workspaceLabel = result.workspace ? ` (workspace '${result.workspace}')` : "";
|
|
21128
|
+
if (result.status === "already-settled") {
|
|
21129
|
+
process.stdout.write(
|
|
21130
|
+
`Journal operation ${result.operationId} is already settled${workspaceLabel}.
|
|
21131
|
+
`
|
|
21132
|
+
);
|
|
21133
|
+
return;
|
|
21134
|
+
}
|
|
21135
|
+
process.stdout.write(
|
|
21136
|
+
`Settled journal operation ${result.operationId} (was ${result.priorStatus})${workspaceLabel}; ensure and stop may proceed.
|
|
21137
|
+
`
|
|
21138
|
+
);
|
|
21139
|
+
}
|
|
20892
21140
|
var init_workspace2 = __esm({
|
|
20893
21141
|
"src/commands/workspace.ts"() {
|
|
20894
21142
|
"use strict";
|
|
@@ -20896,8 +21144,10 @@ var init_workspace2 = __esm({
|
|
|
20896
21144
|
init_repo_config();
|
|
20897
21145
|
init_workspace_cleanup();
|
|
20898
21146
|
init_workspace_gc();
|
|
21147
|
+
init_workspace_journal_settle();
|
|
20899
21148
|
init_workspace_lifecycle();
|
|
20900
21149
|
init_workspace_ownership();
|
|
21150
|
+
init_environment_path();
|
|
20901
21151
|
}
|
|
20902
21152
|
});
|
|
20903
21153
|
|
|
@@ -20939,7 +21189,7 @@ var init_version = __esm({
|
|
|
20939
21189
|
|
|
20940
21190
|
// src/cli.ts
|
|
20941
21191
|
var import_commander = require("commander");
|
|
20942
|
-
var CLI_VERSION = true ? "0.0.
|
|
21192
|
+
var CLI_VERSION = true ? "0.0.67" : "0.0.0-dev";
|
|
20943
21193
|
var VERSION_FLAGS = /* @__PURE__ */ new Set(["-V", "--version"]);
|
|
20944
21194
|
function withErrorHandling(action2) {
|
|
20945
21195
|
return async (...args) => {
|
|
@@ -21309,6 +21559,15 @@ workspaceCommand2.command("down").description("Delete a workspace's runtime and
|
|
|
21309
21559
|
await runWorkspaceDownCommand2(target, options);
|
|
21310
21560
|
})
|
|
21311
21561
|
);
|
|
21562
|
+
workspaceCommand2.command("journal settle").description(
|
|
21563
|
+
"Settle an interrupted lifecycle operation as unobservable so ensure and stop can proceed"
|
|
21564
|
+
).argument("[path]", "Checkout path (defaults to current directory)").option("--json", "Output JSON").action(
|
|
21565
|
+
withErrorHandling(async (path54, _options, command) => {
|
|
21566
|
+
const options = command.opts();
|
|
21567
|
+
const { runWorkspaceJournalSettleCommand: runWorkspaceJournalSettleCommand2 } = await Promise.resolve().then(() => (init_workspace2(), workspace_exports));
|
|
21568
|
+
await runWorkspaceJournalSettleCommand2({ ...path54 ? { path: path54 } : {}, ...options });
|
|
21569
|
+
})
|
|
21570
|
+
);
|
|
21312
21571
|
function parseVersionRequest(argv) {
|
|
21313
21572
|
let requested = false;
|
|
21314
21573
|
let repo;
|
package/package.json
CHANGED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Upgrade to devrouter 0.0.67
|
|
2
|
+
|
|
3
|
+
This patch release makes the reliability journal incapable of deadlocking
|
|
4
|
+
lifecycle commands and completes managed stop when a registration was deleted
|
|
5
|
+
rather than replaced. It restores workspaces that every canonical command
|
|
6
|
+
previously refused with "Lifecycle admission is blocked." after a saturated
|
|
7
|
+
journal held a drained `INTERRUPTED` ensure.
|
|
8
|
+
|
|
9
|
+
## Task
|
|
10
|
+
|
|
11
|
+
Install the published release and update `.devrouter.yml` devrouter.version to
|
|
12
|
+
0.0.67. No schema migration or configuration change is required. Do not change
|
|
13
|
+
the committed configuration, delete registrations, containers, or source files,
|
|
14
|
+
and do not reset lifecycle state manually. Recovery runs inside the existing
|
|
15
|
+
canonical commands under their normal locks, ownership proofs, and journal
|
|
16
|
+
accounting. If a command reports a live worker, wait for it or re-run later;
|
|
17
|
+
`workspace journal settle` refuses while a worker outcome is still observable.
|
|
18
|
+
|
|
19
|
+
## Validation
|
|
20
|
+
|
|
21
|
+
On a workspace whose commands were refused with "Lifecycle admission is
|
|
22
|
+
blocked." (saturated journal with a drained interrupted ensure), run
|
|
23
|
+
`devrouter stop <path>` and confirm it completes — including the case where the
|
|
24
|
+
Devsy registration was already deleted and every workload is positively absent
|
|
25
|
+
— then run `devrouter ensure <path> --json` and confirm admission succeeds with
|
|
26
|
+
routes published. On a record whose worker is provably gone and that still
|
|
27
|
+
refuses progress, run `devrouter workspace journal settle <path> --json` and
|
|
28
|
+
confirm it reports the settled operation, then retry `ensure`. Confirm refusal
|
|
29
|
+
messages now name the blocking field and remediation. For workspaces that never
|
|
30
|
+
hit the deadlock, confirm existing stop, ensure, exec, and `devrouter doctor`
|
|
31
|
+
behavior is unchanged, and confirm a downgrade attempt (record written by a
|
|
32
|
+
newer CLI than the installed one) fails with an explicit upgrade instruction
|
|
33
|
+
instead of a mid-flight refusal.
|
|
34
|
+
|
|
35
|
+
## Report
|
|
36
|
+
|
|
37
|
+
Report the tested CLI version, the exact workspace path or synthetic fixture,
|
|
38
|
+
each command result (stop, ensure, settle if used) with route state after the
|
|
39
|
+
run, any diagnostic failure output verbatim, and whether the
|
|
40
|
+
deleted-registration case, the saturated-journal case, or only the unchanged
|
|
41
|
+
retained path was exercised.
|