@devrouter/cli 0.0.71 → 0.0.73
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 +20 -11
- package/dist/devrouter.js +404 -73
- package/package.json +1 -1
- package/upgrade-prompts/0.0.72.md +27 -0
- package/upgrade-prompts/0.0.73.md +54 -0
|
@@ -5159,7 +5159,7 @@ function loadRepoConfig(repoPath, read2 = (file) => import_node_fs14.default.rea
|
|
|
5159
5159
|
const config = parseConfig(parsed ?? {}, configPath);
|
|
5160
5160
|
const requiredVersion = config.devrouter?.version;
|
|
5161
5161
|
if (requiredVersion && !hasWarnedVersionMismatch) {
|
|
5162
|
-
const cliVersion = true ? "0.0.
|
|
5162
|
+
const cliVersion = true ? "0.0.73" : "0.0.0-dev";
|
|
5163
5163
|
if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
|
|
5164
5164
|
hasWarnedVersionMismatch = true;
|
|
5165
5165
|
process.stderr.write(
|
|
@@ -8596,8 +8596,6 @@ function handleOperationRequest(state, event) {
|
|
|
8596
8596
|
);
|
|
8597
8597
|
if (state.operationHistory.some((entry) => entry.id === event.operationId))
|
|
8598
8598
|
return unchanged(state, "conflict");
|
|
8599
|
-
if (state.executionPolicy !== "manual" && state.operationHistory.length >= RELIABILITY_MAX_ITEMS)
|
|
8600
|
-
return unchanged(state, "blocked");
|
|
8601
8599
|
if (event.kind === "exec" && canExecAfterInterruptedEnsure(state) && event.recoverInterruptedEnsure !== true)
|
|
8602
8600
|
return blocked(
|
|
8603
8601
|
state,
|
|
@@ -8620,7 +8618,7 @@ function handleOperationRequest(state, event) {
|
|
|
8620
8618
|
state,
|
|
8621
8619
|
`exec requires a running workspace (runtimeRunning=${event.runtimeRunning}, desired='${state.desired}').`
|
|
8622
8620
|
);
|
|
8623
|
-
const rollover = state.
|
|
8621
|
+
const rollover = state.operationHistory.length >= RELIABILITY_MAX_ITEMS;
|
|
8624
8622
|
let retired = -1;
|
|
8625
8623
|
if (rollover) {
|
|
8626
8624
|
const latestEnsureId = [...state.operationHistory].reverse().find((entry) => entry.kind === "ensure")?.id;
|
|
@@ -8903,14 +8901,16 @@ function handleRecover(state, event) {
|
|
|
8903
8901
|
if (state.executionPolicy === "manual" || state.desired !== "running")
|
|
8904
8902
|
return unchanged(state, "blocked");
|
|
8905
8903
|
if (possibleDispatch(state.operation)) return unchanged(state, "blocked");
|
|
8904
|
+
const stranded = state.operation?.drained === true && ["NOT_STARTED", "NOT_LAUNCHED"].includes(state.operation.status);
|
|
8905
|
+
const operation = stranded ? null : state.operation;
|
|
8906
8906
|
if (state.incident !== null && state.incident.id !== event.incidentId) {
|
|
8907
8907
|
return unchanged(state, "conflict");
|
|
8908
8908
|
}
|
|
8909
8909
|
if (state.incident !== null) {
|
|
8910
|
-
if (
|
|
8911
|
-
return
|
|
8910
|
+
if (operation && pendingCommand(operation)) {
|
|
8911
|
+
return operation.id === event.operationId && state.phase === "recovering" ? unchanged(state, "joined") : unchanged(state, "conflict");
|
|
8912
8912
|
}
|
|
8913
|
-
if (
|
|
8913
|
+
if (operation?.id === event.operationId && operation.status === "COMPLETED") {
|
|
8914
8914
|
return unchanged(state, "joined");
|
|
8915
8915
|
}
|
|
8916
8916
|
if (state.incident.correctiveActionsTaken >= state.incident.actionLimit) {
|
|
@@ -8918,17 +8918,20 @@ function handleRecover(state, event) {
|
|
|
8918
8918
|
}
|
|
8919
8919
|
}
|
|
8920
8920
|
if (state.incident === null) {
|
|
8921
|
-
if (
|
|
8922
|
-
if (
|
|
8921
|
+
if (operation?.status === "NOT_STARTED") return unchanged(state, "blocked");
|
|
8922
|
+
if (operation?.id === event.operationId) return unchanged(state, "conflict");
|
|
8923
8923
|
state.incident = {
|
|
8924
8924
|
id: event.incidentId,
|
|
8925
8925
|
correctiveActionsTaken: 0,
|
|
8926
8926
|
actionLimit: event.actionLimit
|
|
8927
8927
|
};
|
|
8928
8928
|
}
|
|
8929
|
-
if (!
|
|
8929
|
+
if (!operation || operation.status === "COMPLETED" || operation.status === "INTERRUPTED") {
|
|
8930
8930
|
const runtimeGeneration = advance(state.runtimeGeneration);
|
|
8931
8931
|
if (runtimeGeneration === null) return unchanged(state, "blocked");
|
|
8932
|
+
const consumer = state.consumers[0];
|
|
8933
|
+
if (state.profile === null || consumer === void 0) return unchanged(state, "blocked");
|
|
8934
|
+
if (state.operationHistory.length >= RELIABILITY_MAX_ITEMS) return unchanged(state, "blocked");
|
|
8932
8935
|
state.runtimeGeneration = runtimeGeneration;
|
|
8933
8936
|
state.observations = [];
|
|
8934
8937
|
state.operation = {
|
|
@@ -8938,6 +8941,12 @@ function handleRecover(state, event) {
|
|
|
8938
8941
|
status: "NOT_STARTED",
|
|
8939
8942
|
exitCode: null
|
|
8940
8943
|
};
|
|
8944
|
+
state.operationHistory.push({
|
|
8945
|
+
...state.operation,
|
|
8946
|
+
key: event.operationId,
|
|
8947
|
+
profile: state.profile,
|
|
8948
|
+
consumer: cloneConsumer(consumer)
|
|
8949
|
+
});
|
|
8941
8950
|
}
|
|
8942
8951
|
state.phase = "recovering";
|
|
8943
8952
|
return transition(state, "accepted");
|
|
@@ -9037,7 +9046,7 @@ var import_node_fs25 = __toESM(require("fs"));
|
|
|
9037
9046
|
var import_node_path30 = __toESM(require("path"));
|
|
9038
9047
|
var import_node_util5 = require("util");
|
|
9039
9048
|
function installedCliVersion() {
|
|
9040
|
-
return true ? "0.0.
|
|
9049
|
+
return true ? "0.0.73" : "0.0.0-dev";
|
|
9041
9050
|
}
|
|
9042
9051
|
function compareCliVersions(left, right) {
|
|
9043
9052
|
const parts = (value) => value.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
package/dist/devrouter.js
CHANGED
|
@@ -31,7 +31,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
31
31
|
));
|
|
32
32
|
|
|
33
33
|
// src/core/controller-probe.ts
|
|
34
|
-
function
|
|
34
|
+
function probe(command, args, signal, options) {
|
|
35
35
|
return new Promise((resolve, reject) => {
|
|
36
36
|
if (signal.aborted) {
|
|
37
37
|
reject(new Error("Observation cancelled."));
|
|
@@ -85,12 +85,19 @@ function runControllerProbe(command, args, signal, options = {}) {
|
|
|
85
85
|
if (finished) return;
|
|
86
86
|
finished = true;
|
|
87
87
|
cleanup();
|
|
88
|
-
if (failed
|
|
89
|
-
|
|
90
|
-
else resolve(Buffer.concat(chunks).toString("utf8"));
|
|
88
|
+
if (failed) reject(new Error("Observation process failed or exceeded its bound."));
|
|
89
|
+
else resolve({ output: Buffer.concat(chunks).toString("utf8"), code });
|
|
91
90
|
});
|
|
92
91
|
});
|
|
93
92
|
}
|
|
93
|
+
async function runControllerProbe(command, args, signal, options = {}) {
|
|
94
|
+
const result = await probe(command, args, signal, options);
|
|
95
|
+
if (result.code !== 0) throw new Error("Observation process failed or exceeded its bound.");
|
|
96
|
+
return result.output;
|
|
97
|
+
}
|
|
98
|
+
function runControllerProbeStatus(command, args, signal, options = {}) {
|
|
99
|
+
return probe(command, args, signal, options);
|
|
100
|
+
}
|
|
94
101
|
var import_node_child_process, ControllerProbeUnavailable;
|
|
95
102
|
var init_controller_probe = __esm({
|
|
96
103
|
"src/core/controller-probe.ts"() {
|
|
@@ -132,12 +139,12 @@ async function readCapacityHostSnapshot(signal, dependencies = {}) {
|
|
|
132
139
|
try {
|
|
133
140
|
if ((dependencies.platform ?? process.platform) !== "darwin" || signal.aborted)
|
|
134
141
|
throw new Error("Host probe unavailable.");
|
|
135
|
-
const
|
|
142
|
+
const probe2 = dependencies.probe ?? runControllerProbe;
|
|
136
143
|
const cancellation = new AbortController();
|
|
137
144
|
const probeSignal = AbortSignal.any([signal, cancellation.signal]);
|
|
138
145
|
const results = await Promise.allSettled(
|
|
139
146
|
["hw.memsize", "kern.memorystatus_vm_pressure_level"].map(
|
|
140
|
-
(name) => Promise.resolve().then(() =>
|
|
147
|
+
(name) => Promise.resolve().then(() => probe2("/usr/sbin/sysctl", ["-n", name], probeSignal)).catch((error) => {
|
|
141
148
|
cancellation.abort();
|
|
142
149
|
throw error;
|
|
143
150
|
})
|
|
@@ -1803,6 +1810,62 @@ function parseScheduling(value) {
|
|
|
1803
1810
|
}
|
|
1804
1811
|
return result;
|
|
1805
1812
|
}
|
|
1813
|
+
function parseRecovery(value) {
|
|
1814
|
+
if (value === void 0) return { ...DEFAULT_CAPACITY_RECOVERY };
|
|
1815
|
+
const label2 = "recovery";
|
|
1816
|
+
const recovery = ensureObject(value, label2);
|
|
1817
|
+
ensureAllowedKeys(
|
|
1818
|
+
recovery,
|
|
1819
|
+
[
|
|
1820
|
+
"enabled",
|
|
1821
|
+
"maxProcessRestarts",
|
|
1822
|
+
"maxServiceRestarts",
|
|
1823
|
+
"maxCorrectiveActions",
|
|
1824
|
+
"windowSeconds",
|
|
1825
|
+
"observationSeconds",
|
|
1826
|
+
"resumeDwellSeconds"
|
|
1827
|
+
],
|
|
1828
|
+
label2
|
|
1829
|
+
);
|
|
1830
|
+
if (typeof recovery.enabled !== "boolean") {
|
|
1831
|
+
throw new Error(`${label2}.enabled must be a boolean.`);
|
|
1832
|
+
}
|
|
1833
|
+
const result = {
|
|
1834
|
+
enabled: recovery.enabled,
|
|
1835
|
+
maxProcessRestarts: parseSafeInteger(
|
|
1836
|
+
recovery.maxProcessRestarts,
|
|
1837
|
+
`${label2}.maxProcessRestarts`,
|
|
1838
|
+
0
|
|
1839
|
+
),
|
|
1840
|
+
maxServiceRestarts: parseSafeInteger(
|
|
1841
|
+
recovery.maxServiceRestarts,
|
|
1842
|
+
`${label2}.maxServiceRestarts`,
|
|
1843
|
+
0
|
|
1844
|
+
),
|
|
1845
|
+
maxCorrectiveActions: parsePositiveInteger(
|
|
1846
|
+
recovery.maxCorrectiveActions,
|
|
1847
|
+
`${label2}.maxCorrectiveActions`
|
|
1848
|
+
),
|
|
1849
|
+
windowSeconds: parsePositiveInteger(recovery.windowSeconds, `${label2}.windowSeconds`),
|
|
1850
|
+
observationSeconds: parsePositiveInteger(
|
|
1851
|
+
recovery.observationSeconds,
|
|
1852
|
+
`${label2}.observationSeconds`
|
|
1853
|
+
),
|
|
1854
|
+
resumeDwellSeconds: parsePositiveInteger(
|
|
1855
|
+
recovery.resumeDwellSeconds,
|
|
1856
|
+
`${label2}.resumeDwellSeconds`
|
|
1857
|
+
)
|
|
1858
|
+
};
|
|
1859
|
+
if (result.maxCorrectiveActions < result.maxProcessRestarts + result.maxServiceRestarts) {
|
|
1860
|
+
throw new Error(
|
|
1861
|
+
`${label2}.maxCorrectiveActions must cover maxProcessRestarts and maxServiceRestarts.`
|
|
1862
|
+
);
|
|
1863
|
+
}
|
|
1864
|
+
if (result.maxProcessRestarts > 4 || result.maxServiceRestarts > 2 || result.maxCorrectiveActions > 8 || result.windowSeconds > 3600 || result.observationSeconds > 300 || result.resumeDwellSeconds > 3600) {
|
|
1865
|
+
throw new Error(`${label2} exceeds supported controller bounds.`);
|
|
1866
|
+
}
|
|
1867
|
+
return result;
|
|
1868
|
+
}
|
|
1806
1869
|
function validateDomainReferences(domains) {
|
|
1807
1870
|
const daemonIds = /* @__PURE__ */ new Map();
|
|
1808
1871
|
const endpoints = /* @__PURE__ */ new Map();
|
|
@@ -1926,7 +1989,7 @@ function parseCapacityPolicy(value) {
|
|
|
1926
1989
|
const root = ensureObject(value, "capacity policy");
|
|
1927
1990
|
ensureAllowedKeys(
|
|
1928
1991
|
root,
|
|
1929
|
-
["version", "revision", "admissions", "scheduling", "domains", "enrollments"],
|
|
1992
|
+
["version", "revision", "admissions", "scheduling", "recovery", "domains", "enrollments"],
|
|
1930
1993
|
"capacity policy"
|
|
1931
1994
|
);
|
|
1932
1995
|
const version = parsePositiveInteger(root.version, "capacity policy.version");
|
|
@@ -1939,6 +2002,7 @@ function parseCapacityPolicy(value) {
|
|
|
1939
2002
|
throw new Error("capacity policy.admissions must be 'enabled' or 'paused'.");
|
|
1940
2003
|
}
|
|
1941
2004
|
const scheduling = parseScheduling(root.scheduling);
|
|
2005
|
+
const recovery = parseRecovery(root.recovery);
|
|
1942
2006
|
const domainsValue = ensureObject(root.domains, "capacity policy.domains");
|
|
1943
2007
|
const domainEntries = Object.entries(domainsValue);
|
|
1944
2008
|
if (domainEntries.length > MAX_DOMAINS) {
|
|
@@ -1985,11 +2049,12 @@ function parseCapacityPolicy(value) {
|
|
|
1985
2049
|
revision,
|
|
1986
2050
|
admissions,
|
|
1987
2051
|
scheduling,
|
|
2052
|
+
recovery,
|
|
1988
2053
|
domains,
|
|
1989
2054
|
enrollments
|
|
1990
2055
|
};
|
|
1991
2056
|
}
|
|
1992
|
-
var import_node_fs4, import_node_path6, MAX_DOMAINS, MAX_ENROLLMENTS, MAX_PROFILES_PER_ENROLLMENT, MAX_STRING_LENGTH, MAX_PATH_LENGTH, DOMAIN_ID_RE, PROFILE_NAME_RE, WORKSPACE_TOKEN_RE, SHA256_RE;
|
|
2057
|
+
var import_node_fs4, import_node_path6, MAX_DOMAINS, MAX_ENROLLMENTS, MAX_PROFILES_PER_ENROLLMENT, MAX_STRING_LENGTH, MAX_PATH_LENGTH, DOMAIN_ID_RE, PROFILE_NAME_RE, WORKSPACE_TOKEN_RE, SHA256_RE, DEFAULT_CAPACITY_RECOVERY;
|
|
1993
2058
|
var init_capacity_policy = __esm({
|
|
1994
2059
|
"src/core/capacity-policy.ts"() {
|
|
1995
2060
|
"use strict";
|
|
@@ -2004,6 +2069,15 @@ var init_capacity_policy = __esm({
|
|
|
2004
2069
|
PROFILE_NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
2005
2070
|
WORKSPACE_TOKEN_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
2006
2071
|
SHA256_RE = /^[0-9a-f]{64}$/;
|
|
2072
|
+
DEFAULT_CAPACITY_RECOVERY = {
|
|
2073
|
+
enabled: false,
|
|
2074
|
+
maxProcessRestarts: 2,
|
|
2075
|
+
maxServiceRestarts: 1,
|
|
2076
|
+
maxCorrectiveActions: 3,
|
|
2077
|
+
windowSeconds: 600,
|
|
2078
|
+
observationSeconds: 30,
|
|
2079
|
+
resumeDwellSeconds: 300
|
|
2080
|
+
};
|
|
2007
2081
|
}
|
|
2008
2082
|
});
|
|
2009
2083
|
|
|
@@ -4133,7 +4207,7 @@ function loadRepoConfig(repoPath, read2 = (file) => import_node_fs7.default.read
|
|
|
4133
4207
|
const config = parseConfig(parsed ?? {}, configPath);
|
|
4134
4208
|
const requiredVersion = config.devrouter?.version;
|
|
4135
4209
|
if (requiredVersion && !hasWarnedVersionMismatch) {
|
|
4136
|
-
const cliVersion = true ? "0.0.
|
|
4210
|
+
const cliVersion = true ? "0.0.73" : "0.0.0-dev";
|
|
4137
4211
|
if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
|
|
4138
4212
|
hasWarnedVersionMismatch = true;
|
|
4139
4213
|
process.stderr.write(
|
|
@@ -5386,7 +5460,7 @@ var init_reliability_contract = __esm({
|
|
|
5386
5460
|
|
|
5387
5461
|
// src/core/reliability-operation-store.ts
|
|
5388
5462
|
function installedCliVersion() {
|
|
5389
|
-
return true ? "0.0.
|
|
5463
|
+
return true ? "0.0.73" : "0.0.0-dev";
|
|
5390
5464
|
}
|
|
5391
5465
|
function compareCliVersions(left, right) {
|
|
5392
5466
|
const parts = (value) => value.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
@@ -6423,6 +6497,16 @@ async function readProviderGeneration(repoPath, providerId, signal) {
|
|
|
6423
6497
|
throw new Error("Capacity provider generation is malformed.");
|
|
6424
6498
|
return generation;
|
|
6425
6499
|
}
|
|
6500
|
+
function ownershipPopulationEvidence(record5, state) {
|
|
6501
|
+
return {
|
|
6502
|
+
environmentId: record5.state.environmentId,
|
|
6503
|
+
executionPolicy: record5.state.executionPolicy,
|
|
6504
|
+
enrollment: record5.enrollment ?? null,
|
|
6505
|
+
composeProject: state?.composeProject ?? null,
|
|
6506
|
+
devpodId: state?.devpodId ?? null,
|
|
6507
|
+
stopBaseline: state?.stopBaseline ?? null
|
|
6508
|
+
};
|
|
6509
|
+
}
|
|
6426
6510
|
async function resolveCapacityOwnership(policy, runtimeDomain, signal, dependencies = {
|
|
6427
6511
|
resolve: resolveCapacityEnrollment,
|
|
6428
6512
|
providerGeneration: readProviderGeneration,
|
|
@@ -6473,18 +6557,21 @@ async function resolveCapacityOwnership(policy, runtimeDomain, signal, dependenc
|
|
|
6473
6557
|
check();
|
|
6474
6558
|
if (!(0, import_node_util5.isDeepStrictEqual)(current, entry.binding) || !(0, import_node_util5.isDeepStrictEqual)(generation, entry.generation))
|
|
6475
6559
|
throw new Error("Capacity provider ownership changed during collection.");
|
|
6476
|
-
if (!entry.evidence || !(0, import_node_util5.isDeepStrictEqual)(
|
|
6477
|
-
|
|
6478
|
-
|
|
6479
|
-
|
|
6480
|
-
|
|
6481
|
-
|
|
6482
|
-
|
|
6483
|
-
|
|
6484
|
-
|
|
6485
|
-
|
|
6560
|
+
if (!entry.evidence || !(0, import_node_util5.isDeepStrictEqual)(
|
|
6561
|
+
entry.evidence,
|
|
6562
|
+
ownershipPopulationEvidence(
|
|
6563
|
+
dependencies.journal({
|
|
6564
|
+
repoPath: entry.enrollment.repoPath,
|
|
6565
|
+
workspace: entry.enrollment.workspace || null,
|
|
6566
|
+
provider: entry.enrollment.provider
|
|
6567
|
+
}),
|
|
6568
|
+
dependencies.managed(
|
|
6569
|
+
entry.enrollment.repoPath,
|
|
6570
|
+
entry.enrollment.workspace || void 0,
|
|
6571
|
+
(file) => readControllerEvidence(file, 1048576)
|
|
6572
|
+
)
|
|
6486
6573
|
)
|
|
6487
|
-
|
|
6574
|
+
))
|
|
6488
6575
|
throw new Error("Capacity ownership records changed before publication.");
|
|
6489
6576
|
}
|
|
6490
6577
|
},
|
|
@@ -6533,7 +6620,7 @@ async function resolveCapacityOwnership(policy, runtimeDomain, signal, dependenc
|
|
|
6533
6620
|
throw new Error("Capacity witnessed startup generation changed.");
|
|
6534
6621
|
} else if (!baseline || baseline.provider !== enrollment.provider || baseline.context !== entry.generation.context || baseline.uid !== entry.generation.uid || baseline.sourceContainer !== entry.generation.sourceContainer || baseline.endpoint !== `unix://${domain.endpoint}` || baseline.daemonId !== domain.daemonId)
|
|
6535
6622
|
throw new Error("Capacity population lacks an exact retained runtime baseline.");
|
|
6536
|
-
const evidence =
|
|
6623
|
+
const evidence = ownershipPopulationEvidence(record5, state);
|
|
6537
6624
|
if (entry.evidence && !(0, import_node_util5.isDeepStrictEqual)(entry.evidence, evidence))
|
|
6538
6625
|
throw new Error("Capacity ownership generation changed during collection.");
|
|
6539
6626
|
entry.evidence = structuredClone(evidence);
|
|
@@ -6569,7 +6656,13 @@ async function resolveCapacityOwnership(policy, runtimeDomain, signal, dependenc
|
|
|
6569
6656
|
endpoint: `unix://${domain.endpoint}`
|
|
6570
6657
|
});
|
|
6571
6658
|
}
|
|
6572
|
-
if (!(0, import_node_util5.isDeepStrictEqual)(state, readState()) || !(0, import_node_util5.isDeepStrictEqual)(
|
|
6659
|
+
if (!(0, import_node_util5.isDeepStrictEqual)(state, readState()) || !(0, import_node_util5.isDeepStrictEqual)(
|
|
6660
|
+
ownershipPopulationEvidence(record5, state),
|
|
6661
|
+
ownershipPopulationEvidence(
|
|
6662
|
+
dependencies.journal(identity2),
|
|
6663
|
+
state
|
|
6664
|
+
)
|
|
6665
|
+
))
|
|
6573
6666
|
throw new Error("Capacity ownership records changed during collection.");
|
|
6574
6667
|
result.push({ environmentId: binding.environment.id, containers: containers2 });
|
|
6575
6668
|
}
|
|
@@ -6623,8 +6716,8 @@ async function collectDeclaredRuntimeCapacity(domain, proveOwned, signal, depend
|
|
|
6623
6716
|
const sampledAtMs = Date.now();
|
|
6624
6717
|
const cancellation = new AbortController();
|
|
6625
6718
|
const combined = AbortSignal.any([signal, cancellation.signal]);
|
|
6626
|
-
const timer = setTimeout(() => cancellation.abort(),
|
|
6627
|
-
const deadline = performance.now() +
|
|
6719
|
+
const timer = setTimeout(() => cancellation.abort(), RUNTIME_PROBE_BUDGET_MS);
|
|
6720
|
+
const deadline = performance.now() + RUNTIME_PROBE_BUDGET_MS;
|
|
6628
6721
|
const check = () => {
|
|
6629
6722
|
if (combined.aborted || performance.now() >= deadline)
|
|
6630
6723
|
throw new Error("Runtime capacity collection expired.");
|
|
@@ -6717,13 +6810,14 @@ async function collectDeclaredRuntimeCapacity(domain, proveOwned, signal, depend
|
|
|
6717
6810
|
clearTimeout(timer);
|
|
6718
6811
|
}
|
|
6719
6812
|
}
|
|
6720
|
-
var import_node_util6;
|
|
6813
|
+
var import_node_util6, RUNTIME_PROBE_BUDGET_MS;
|
|
6721
6814
|
var init_capacity_runtime_probe = __esm({
|
|
6722
6815
|
"src/core/capacity-runtime-probe.ts"() {
|
|
6723
6816
|
"use strict";
|
|
6724
6817
|
import_node_util6 = require("util");
|
|
6725
6818
|
init_capacity_docker_probe();
|
|
6726
6819
|
init_capacity_host_probe();
|
|
6820
|
+
RUNTIME_PROBE_BUDGET_MS = 8e3;
|
|
6727
6821
|
}
|
|
6728
6822
|
});
|
|
6729
6823
|
|
|
@@ -8538,8 +8632,8 @@ function parseWorkspaceRuntime(value) {
|
|
|
8538
8632
|
}
|
|
8539
8633
|
function isRuntimeInstalled(runtime) {
|
|
8540
8634
|
const probeArgs = runtime === "devsy" ? ["--version"] : ["version"];
|
|
8541
|
-
const
|
|
8542
|
-
return
|
|
8635
|
+
const probe2 = (0, import_node_child_process10.spawnSync)(runtime, probeArgs, { encoding: "utf-8" });
|
|
8636
|
+
return probe2.status === 0 && !probe2.error;
|
|
8543
8637
|
}
|
|
8544
8638
|
function readWorkspaceRuntimeConfig() {
|
|
8545
8639
|
let raw;
|
|
@@ -11833,8 +11927,6 @@ function handleOperationRequest(state, event) {
|
|
|
11833
11927
|
);
|
|
11834
11928
|
if (state.operationHistory.some((entry) => entry.id === event.operationId))
|
|
11835
11929
|
return unchanged(state, "conflict");
|
|
11836
|
-
if (state.executionPolicy !== "manual" && state.operationHistory.length >= RELIABILITY_MAX_ITEMS)
|
|
11837
|
-
return unchanged(state, "blocked");
|
|
11838
11930
|
if (event.kind === "exec" && canExecAfterInterruptedEnsure(state) && event.recoverInterruptedEnsure !== true)
|
|
11839
11931
|
return blocked(
|
|
11840
11932
|
state,
|
|
@@ -11857,7 +11949,7 @@ function handleOperationRequest(state, event) {
|
|
|
11857
11949
|
state,
|
|
11858
11950
|
`exec requires a running workspace (runtimeRunning=${event.runtimeRunning}, desired='${state.desired}').`
|
|
11859
11951
|
);
|
|
11860
|
-
const rollover = state.
|
|
11952
|
+
const rollover = state.operationHistory.length >= RELIABILITY_MAX_ITEMS;
|
|
11861
11953
|
let retired = -1;
|
|
11862
11954
|
if (rollover) {
|
|
11863
11955
|
const latestEnsureId = [...state.operationHistory].reverse().find((entry) => entry.kind === "ensure")?.id;
|
|
@@ -12140,14 +12232,16 @@ function handleRecover(state, event) {
|
|
|
12140
12232
|
if (state.executionPolicy === "manual" || state.desired !== "running")
|
|
12141
12233
|
return unchanged(state, "blocked");
|
|
12142
12234
|
if (possibleDispatch(state.operation)) return unchanged(state, "blocked");
|
|
12235
|
+
const stranded = state.operation?.drained === true && ["NOT_STARTED", "NOT_LAUNCHED"].includes(state.operation.status);
|
|
12236
|
+
const operation = stranded ? null : state.operation;
|
|
12143
12237
|
if (state.incident !== null && state.incident.id !== event.incidentId) {
|
|
12144
12238
|
return unchanged(state, "conflict");
|
|
12145
12239
|
}
|
|
12146
12240
|
if (state.incident !== null) {
|
|
12147
|
-
if (
|
|
12148
|
-
return
|
|
12241
|
+
if (operation && pendingCommand(operation)) {
|
|
12242
|
+
return operation.id === event.operationId && state.phase === "recovering" ? unchanged(state, "joined") : unchanged(state, "conflict");
|
|
12149
12243
|
}
|
|
12150
|
-
if (
|
|
12244
|
+
if (operation?.id === event.operationId && operation.status === "COMPLETED") {
|
|
12151
12245
|
return unchanged(state, "joined");
|
|
12152
12246
|
}
|
|
12153
12247
|
if (state.incident.correctiveActionsTaken >= state.incident.actionLimit) {
|
|
@@ -12155,17 +12249,20 @@ function handleRecover(state, event) {
|
|
|
12155
12249
|
}
|
|
12156
12250
|
}
|
|
12157
12251
|
if (state.incident === null) {
|
|
12158
|
-
if (
|
|
12159
|
-
if (
|
|
12252
|
+
if (operation?.status === "NOT_STARTED") return unchanged(state, "blocked");
|
|
12253
|
+
if (operation?.id === event.operationId) return unchanged(state, "conflict");
|
|
12160
12254
|
state.incident = {
|
|
12161
12255
|
id: event.incidentId,
|
|
12162
12256
|
correctiveActionsTaken: 0,
|
|
12163
12257
|
actionLimit: event.actionLimit
|
|
12164
12258
|
};
|
|
12165
12259
|
}
|
|
12166
|
-
if (!
|
|
12260
|
+
if (!operation || operation.status === "COMPLETED" || operation.status === "INTERRUPTED") {
|
|
12167
12261
|
const runtimeGeneration = advance(state.runtimeGeneration);
|
|
12168
12262
|
if (runtimeGeneration === null) return unchanged(state, "blocked");
|
|
12263
|
+
const consumer = state.consumers[0];
|
|
12264
|
+
if (state.profile === null || consumer === void 0) return unchanged(state, "blocked");
|
|
12265
|
+
if (state.operationHistory.length >= RELIABILITY_MAX_ITEMS) return unchanged(state, "blocked");
|
|
12169
12266
|
state.runtimeGeneration = runtimeGeneration;
|
|
12170
12267
|
state.observations = [];
|
|
12171
12268
|
state.operation = {
|
|
@@ -12175,6 +12272,12 @@ function handleRecover(state, event) {
|
|
|
12175
12272
|
status: "NOT_STARTED",
|
|
12176
12273
|
exitCode: null
|
|
12177
12274
|
};
|
|
12275
|
+
state.operationHistory.push({
|
|
12276
|
+
...state.operation,
|
|
12277
|
+
key: event.operationId,
|
|
12278
|
+
profile: state.profile,
|
|
12279
|
+
consumer: cloneConsumer(consumer)
|
|
12280
|
+
});
|
|
12178
12281
|
}
|
|
12179
12282
|
state.phase = "recovering";
|
|
12180
12283
|
return transition(state, "accepted");
|
|
@@ -12267,6 +12370,49 @@ function stepReliability(state, event, nowMs) {
|
|
|
12267
12370
|
}
|
|
12268
12371
|
return result;
|
|
12269
12372
|
}
|
|
12373
|
+
function requiredCapabilities(state) {
|
|
12374
|
+
const required = /* @__PURE__ */ new Set();
|
|
12375
|
+
for (const consumer of state.consumers) {
|
|
12376
|
+
for (const capability of consumer.requiredCapabilities) required.add(capability);
|
|
12377
|
+
}
|
|
12378
|
+
return [...required];
|
|
12379
|
+
}
|
|
12380
|
+
function failedCapability(state, required, nowMs) {
|
|
12381
|
+
return required.some((capability) => {
|
|
12382
|
+
const observation = state.observations.find((entry) => entry.capability === capability);
|
|
12383
|
+
return observation !== void 0 && observation.infrastructure === "failed" && observation.observedAtMs >= state.observationsAfterMs && observation.observedAtMs <= nowMs && nowMs - observation.observedAtMs < observation.validForMs;
|
|
12384
|
+
});
|
|
12385
|
+
}
|
|
12386
|
+
function decideRecovery(input2) {
|
|
12387
|
+
const { state, nowMs, actionLimit, pressureDwellSatisfied } = input2;
|
|
12388
|
+
assertReliabilityState(state);
|
|
12389
|
+
if (!isReliabilityCounter(nowMs) || nowMs < state.observationsAfterMs)
|
|
12390
|
+
throw new Error("Invalid reliability clock.");
|
|
12391
|
+
if (input2.failedCapabilities !== void 0 && (input2.failedCapabilities.length > RELIABILITY_MAX_ITEMS || input2.failedCapabilities.some((capability) => !isReliabilityId(capability))))
|
|
12392
|
+
throw new Error("Invalid recovery evidence.");
|
|
12393
|
+
if (state.executionPolicy === "manual") return { action: "none", reason: "unmanaged" };
|
|
12394
|
+
if (state.desired === "stopped-by-user") return { action: "none", reason: "user-stopped" };
|
|
12395
|
+
if (state.desired === "parked-for-capacity") {
|
|
12396
|
+
const resumeReady = pressureDwellSatisfied && state.stopProof.workloadsStopped && state.stopProof.routesRemoved && state.consumers.length > 0 && state.admission === "admitted" && state.profile !== null && (state.incident === null || state.incident.correctiveActionsTaken < state.incident.actionLimit) && state.operation !== null && ["NOT_STARTED", "COMPLETED", "INTERRUPTED"].includes(state.operation.status);
|
|
12397
|
+
return { action: resumeReady ? "resume" : "none", reason: "parked" };
|
|
12398
|
+
}
|
|
12399
|
+
if (!isReliabilityCounter(actionLimit) || actionLimit < 1)
|
|
12400
|
+
throw new Error("Invalid recovery action budget.");
|
|
12401
|
+
const failed = input2.failedCapabilities !== void 0 ? input2.failedCapabilities.length > 0 : failedCapability(state, requiredCapabilities(state), nowMs);
|
|
12402
|
+
if (!failed) return { action: "none", reason: "healthy" };
|
|
12403
|
+
if (possibleDispatch(state.operation)) return { action: "none", reason: "active-operation" };
|
|
12404
|
+
if (state.incident !== null) {
|
|
12405
|
+
if (state.incident.correctiveActionsTaken >= state.incident.actionLimit)
|
|
12406
|
+
return { action: "blocked", reason: "budget-exhausted" };
|
|
12407
|
+
return {
|
|
12408
|
+
action: "continue",
|
|
12409
|
+
reason: "recovery-eligible",
|
|
12410
|
+
incidentId: state.incident.id,
|
|
12411
|
+
actionLimit: state.incident.actionLimit
|
|
12412
|
+
};
|
|
12413
|
+
}
|
|
12414
|
+
return { action: "start", reason: "recovery-eligible", actionLimit };
|
|
12415
|
+
}
|
|
12270
12416
|
var INFRASTRUCTURE_VALUES, APPLICATION_VALUES, ADMISSION_VALUES;
|
|
12271
12417
|
var init_reliability_model = __esm({
|
|
12272
12418
|
"src/core/reliability-model.ts"() {
|
|
@@ -14843,6 +14989,52 @@ function assertCurrentCapacityController(expected, directory = import_node_path4
|
|
|
14843
14989
|
if (!expected || current?.store !== expected.store || current.epoch !== expected.epoch)
|
|
14844
14990
|
throw new Error("Capacity controller incarnation changed.");
|
|
14845
14991
|
}
|
|
14992
|
+
function prepareRecoveryLifecycleOperation(input2) {
|
|
14993
|
+
const ids2 = newLifecycleIds();
|
|
14994
|
+
return updateReliabilityOperation(input2.identity, (record5) => {
|
|
14995
|
+
assertCurrentCapacityController(input2.controller);
|
|
14996
|
+
if (record5.version !== 2 || record5.state.executionPolicy !== "capacity-managed" || record5.enrollment?.policyRevision !== input2.policyRevision)
|
|
14997
|
+
throw new Error("Managed recovery requires current durable enrollment.");
|
|
14998
|
+
if (record5.phaseSettlement) return void 0;
|
|
14999
|
+
reconcileDrained(record5);
|
|
15000
|
+
if (record5.worker) return void 0;
|
|
15001
|
+
const decision = decideRecovery({
|
|
15002
|
+
state: record5.state,
|
|
15003
|
+
nowMs: Date.now(),
|
|
15004
|
+
actionLimit: input2.actionLimit,
|
|
15005
|
+
pressureDwellSatisfied: false,
|
|
15006
|
+
failedCapabilities: input2.failedCapabilities
|
|
15007
|
+
});
|
|
15008
|
+
if (decision.action !== "start" && decision.action !== "continue") return void 0;
|
|
15009
|
+
const transition2 = stepReliability(
|
|
15010
|
+
record5.state,
|
|
15011
|
+
{
|
|
15012
|
+
...reliabilityFence(record5.state),
|
|
15013
|
+
type: "recover",
|
|
15014
|
+
incidentId: decision.action === "continue" ? decision.incidentId : input2.incidentId,
|
|
15015
|
+
actionLimit: input2.actionLimit,
|
|
15016
|
+
operationId: ids2.operationId
|
|
15017
|
+
},
|
|
15018
|
+
Date.now()
|
|
15019
|
+
);
|
|
15020
|
+
if (transition2.outcome !== "accepted") return void 0;
|
|
15021
|
+
record5.state = transition2.state;
|
|
15022
|
+
record5.outcome = null;
|
|
15023
|
+
record5.result = null;
|
|
15024
|
+
record5.startupWitness = null;
|
|
15025
|
+
return {
|
|
15026
|
+
operationId: ids2.operationId,
|
|
15027
|
+
request: {
|
|
15028
|
+
...ids2,
|
|
15029
|
+
kind: "ensure",
|
|
15030
|
+
repoPath: input2.identity.repoPath,
|
|
15031
|
+
identity: { ...input2.identity },
|
|
15032
|
+
fence: reliabilityFence(record5.state),
|
|
15033
|
+
options: { profile: input2.profile, quiet: true }
|
|
15034
|
+
}
|
|
15035
|
+
};
|
|
15036
|
+
});
|
|
15037
|
+
}
|
|
14846
15038
|
function settlePreparedLifecycleCapacity(input2) {
|
|
14847
15039
|
const directory = input2.directory ?? import_node_path44.default.join(DEVROUTER_HOME, "controller");
|
|
14848
15040
|
const store = new CapacityStore(directory);
|
|
@@ -15177,6 +15369,17 @@ async function superviseThroughController(input2) {
|
|
|
15177
15369
|
);
|
|
15178
15370
|
};
|
|
15179
15371
|
const deadline = Date.now() + CAPACITY_CLIENT_WAIT_MS;
|
|
15372
|
+
let outputGapReported = false;
|
|
15373
|
+
const onOutput = (page) => {
|
|
15374
|
+
if (page.gap && !outputGapReported) {
|
|
15375
|
+
process.stderr.write("devrouter: controller output buffer dropped earlier command output.\n");
|
|
15376
|
+
outputGapReported = true;
|
|
15377
|
+
}
|
|
15378
|
+
for (const chunk of page.chunks) {
|
|
15379
|
+
const stream = chunk.stream === "stderr" ? process.stderr : process.stdout;
|
|
15380
|
+
stream.write(Buffer.from(chunk.data, page.encoding));
|
|
15381
|
+
}
|
|
15382
|
+
};
|
|
15180
15383
|
try {
|
|
15181
15384
|
const bind = () => observeControllerBinding(directory, {
|
|
15182
15385
|
path: input2.repoPath,
|
|
@@ -15191,7 +15394,7 @@ async function superviseThroughController(input2) {
|
|
|
15191
15394
|
let pending = await submitControllerOperation(
|
|
15192
15395
|
directory,
|
|
15193
15396
|
input2.kind === "exec" ? { ...binding, requestId: input2.requestId, kind: "exec", command: input2.command ?? [] } : { ...binding, requestId: input2.requestId, kind: "ensure" },
|
|
15194
|
-
{ waitSeconds: controllerWaitSeconds(deadline) }
|
|
15397
|
+
{ waitSeconds: controllerWaitSeconds(deadline), onOutput }
|
|
15195
15398
|
);
|
|
15196
15399
|
while (pending.status !== "terminal") {
|
|
15197
15400
|
cancelled();
|
|
@@ -15207,7 +15410,7 @@ async function superviseThroughController(input2) {
|
|
|
15207
15410
|
pending = await followControllerOperation(
|
|
15208
15411
|
directory,
|
|
15209
15412
|
{ ...binding, operationId: pending.operationId },
|
|
15210
|
-
{ waitSeconds: controllerWaitSeconds(deadline) }
|
|
15413
|
+
{ waitSeconds: controllerWaitSeconds(deadline), onOutput }
|
|
15211
15414
|
);
|
|
15212
15415
|
} catch {
|
|
15213
15416
|
await new Promise((resolve) => setTimeout(resolve, Math.min(1e3, deadline - Date.now())));
|
|
@@ -15233,23 +15436,28 @@ function renewLifecycleCapacity(request2, policy, samples, controller, directory
|
|
|
15233
15436
|
const binding = record5.capacity;
|
|
15234
15437
|
if (!matchesFence(record5, request2.fence) || !binding || binding.operationId !== request2.operationId || binding.workerId !== request2.workerId || binding.controller?.store !== controller.store || binding.controller?.epoch !== controller.epoch)
|
|
15235
15438
|
throw new Error("Capacity renewal no longer owns this operation.");
|
|
15236
|
-
binding.validUntilMs = 0;
|
|
15237
15439
|
try {
|
|
15238
|
-
if (record5.state.executionPolicy !== "capacity-managed" || record5.state.operation?.id !== request2.operationId || record5.state.operation.drained || record5.state.desired !== "running" || policy.admissions !== "enabled" || policy.revision !== binding.policyRevision || JSON.stringify(readCapacityPolicy(directory)) !== JSON.stringify(policy))
|
|
15440
|
+
if (record5.state.executionPolicy !== "capacity-managed" || record5.state.operation?.id !== request2.operationId || record5.state.operation.drained || record5.state.desired !== "running" || policy.admissions !== "enabled" || policy.revision !== binding.policyRevision || JSON.stringify(readCapacityPolicy(directory)) !== JSON.stringify(policy)) {
|
|
15441
|
+
binding.validUntilMs = 0;
|
|
15239
15442
|
return false;
|
|
15443
|
+
}
|
|
15240
15444
|
const enrolled = record5.enrollment;
|
|
15241
15445
|
const runtime = enrolled && policy.domains[enrolled.runtimeDomain];
|
|
15242
15446
|
const enrollment = policy.enrollments.find(
|
|
15243
15447
|
(entry) => entry.repoPath === record5.identity.repoPath && entry.workspace === record5.identity.workspace && entry.provider === record5.identity.provider && entry.providerId === enrolled?.providerId && entry.gitCommonDir === enrolled.gitCommonDir && entry.estimatesDigest === enrolled.estimatesDigest && entry.hostDomain === enrolled.hostDomain && entry.runtimeDomain === enrolled.runtimeDomain && entry.profiles.includes(record5.state.profile ?? "")
|
|
15244
15448
|
);
|
|
15245
|
-
if (!enrolled || enrolled.policyRevision !== policy.revision || !enrollment || runtime?.kind !== "runtime" || runtime.endpoint !== enrolled.endpoint || runtime.daemonId !== enrolled.daemonId)
|
|
15449
|
+
if (!enrolled || enrolled.policyRevision !== policy.revision || !enrollment || runtime?.kind !== "runtime" || runtime.endpoint !== enrolled.endpoint || runtime.daemonId !== enrolled.daemonId) {
|
|
15450
|
+
binding.validUntilMs = 0;
|
|
15246
15451
|
return false;
|
|
15452
|
+
}
|
|
15247
15453
|
const snapshot = new CapacityStore(directory).read();
|
|
15248
15454
|
const reservation = snapshot.reservations.find(
|
|
15249
15455
|
(entry) => entry.reservationId === binding.reservationId
|
|
15250
15456
|
);
|
|
15251
|
-
if (!reservation || reservation.environmentId !== record5.state.environmentId || reservation.operationId !== request2.operationId || reservation.policyRevision !== policy.revision)
|
|
15457
|
+
if (!reservation || reservation.environmentId !== record5.state.environmentId || reservation.operationId !== request2.operationId || reservation.policyRevision !== policy.revision) {
|
|
15458
|
+
binding.validUntilMs = 0;
|
|
15252
15459
|
return false;
|
|
15460
|
+
}
|
|
15253
15461
|
const maxAge = policy.scheduling.maxSampleAgeSeconds * 1e3;
|
|
15254
15462
|
const decision = evaluateCapacity(
|
|
15255
15463
|
policy.domains,
|
|
@@ -15276,16 +15484,21 @@ function renewLifecycleCapacity(request2, policy, samples, controller, directory
|
|
|
15276
15484
|
snapshot.pools
|
|
15277
15485
|
).admitted;
|
|
15278
15486
|
})();
|
|
15279
|
-
if (!tolerated)
|
|
15487
|
+
if (!tolerated) {
|
|
15488
|
+
if (decision.reason !== "unknown" && decision.reason !== "stale")
|
|
15489
|
+
binding.validUntilMs = 0;
|
|
15490
|
+
return false;
|
|
15491
|
+
}
|
|
15280
15492
|
}
|
|
15493
|
+
const observed = Object.keys(reservation.totals).map(
|
|
15494
|
+
(domain) => samples[domain]?.sampledAtMs
|
|
15495
|
+
);
|
|
15496
|
+
if (observed.some((sampledAtMs) => typeof sampledAtMs !== "number")) return false;
|
|
15281
15497
|
binding.snapshotRevision = snapshot.revision;
|
|
15282
|
-
binding.validUntilMs = Math.min(
|
|
15283
|
-
...Object.keys(reservation.totals).map((domain) => samples[domain]?.sampledAtMs ?? 0)
|
|
15284
|
-
) + maxAge;
|
|
15498
|
+
binding.validUntilMs = Math.min(...observed) + maxAge;
|
|
15285
15499
|
assertCapacityEffect(record5, request2.workerId, nowMs, directory);
|
|
15286
15500
|
return true;
|
|
15287
15501
|
} catch {
|
|
15288
|
-
binding.validUntilMs = 0;
|
|
15289
15502
|
return false;
|
|
15290
15503
|
}
|
|
15291
15504
|
});
|
|
@@ -15429,6 +15642,11 @@ var init_capacity_queue = __esm({
|
|
|
15429
15642
|
});
|
|
15430
15643
|
return request2.operationId;
|
|
15431
15644
|
}
|
|
15645
|
+
/** True while the queue still owns the operation, awaiting admission or running. */
|
|
15646
|
+
hasOperation(operationId) {
|
|
15647
|
+
const entry = this.entries.get(operationId);
|
|
15648
|
+
return entry !== void 0 && entry.phase !== "terminal";
|
|
15649
|
+
}
|
|
15432
15650
|
observe(operationId) {
|
|
15433
15651
|
const entry = this.entries.get(operationId);
|
|
15434
15652
|
return entry ? { operationId, phase: entry.phase, reason: entry.reason, output: entry.output.read() } : void 0;
|
|
@@ -15966,6 +16184,87 @@ function createCapacityController(options) {
|
|
|
15966
16184
|
settlePreparations();
|
|
15967
16185
|
return queue.tick();
|
|
15968
16186
|
},
|
|
16187
|
+
/**
|
|
16188
|
+
* Open or advance one bounded automatic recovery for an environment whose
|
|
16189
|
+
* required capability has positively failed. It is inert unless the
|
|
16190
|
+
* operator policy enables recovery, and every decision, budget, and
|
|
16191
|
+
* admission rule is the one an operator-requested ensure obeys.
|
|
16192
|
+
*/
|
|
16193
|
+
async recover(environment, failedCapabilities, signal) {
|
|
16194
|
+
signal = AbortSignal.any([signal, lifetime.signal]);
|
|
16195
|
+
if (signal.aborted || !policy.recovery.enabled) return;
|
|
16196
|
+
if (failedCapabilities.length === 0 || failedCapabilities.length > 64) return;
|
|
16197
|
+
const current = readCapacityPolicy(options.directory);
|
|
16198
|
+
if (current?.admissions !== "enabled" || !(0, import_node_util12.isDeepStrictEqual)(current, policy)) return;
|
|
16199
|
+
let resolved;
|
|
16200
|
+
try {
|
|
16201
|
+
resolved = await enrollCapacityLifecycle(
|
|
16202
|
+
current,
|
|
16203
|
+
{ path: environment.repoPath, profile: environment.profile, require: [] },
|
|
16204
|
+
signal,
|
|
16205
|
+
options.directory
|
|
16206
|
+
);
|
|
16207
|
+
} catch {
|
|
16208
|
+
return;
|
|
16209
|
+
}
|
|
16210
|
+
if (signal.aborted || !(0, import_node_util12.isDeepStrictEqual)(resolved.environment, environment)) return;
|
|
16211
|
+
if (!(0, import_node_util12.isDeepStrictEqual)(readCapacityPolicy(options.directory), current)) return;
|
|
16212
|
+
const runtime = current.domains[resolved.enrollment.runtimeDomain];
|
|
16213
|
+
if (runtime?.kind !== "runtime" || runtime.hostDomain !== resolved.enrollment.hostDomain || current.domains[runtime.hostDomain]?.kind !== "host" || !current.enrollments.some((entry) => (0, import_node_util12.isDeepStrictEqual)(entry, resolved.enrollment)))
|
|
16214
|
+
return;
|
|
16215
|
+
const pool = {
|
|
16216
|
+
daemonId: runtime.daemonId,
|
|
16217
|
+
runtimeDomain: resolved.enrollment.runtimeDomain,
|
|
16218
|
+
hostDomain: runtime.hostDomain,
|
|
16219
|
+
hostChargeCeilingBytes: runtime.hostChargeCeilingBytes
|
|
16220
|
+
};
|
|
16221
|
+
const identity2 = {
|
|
16222
|
+
repoPath: environment.repoPath,
|
|
16223
|
+
workspace: environment.workspace || null,
|
|
16224
|
+
provider: environment.provider
|
|
16225
|
+
};
|
|
16226
|
+
const record5 = readReliabilityOperation(identity2);
|
|
16227
|
+
if (!record5) return;
|
|
16228
|
+
if (record5.state.operation && queue.hasOperation(record5.state.operation.id)) return;
|
|
16229
|
+
let prepared;
|
|
16230
|
+
try {
|
|
16231
|
+
prepared = prepareRecoveryLifecycleOperation({
|
|
16232
|
+
identity: identity2,
|
|
16233
|
+
controller,
|
|
16234
|
+
policyRevision: current.revision,
|
|
16235
|
+
actionLimit: policy.recovery.maxCorrectiveActions,
|
|
16236
|
+
failedCapabilities,
|
|
16237
|
+
profile: environment.profile,
|
|
16238
|
+
incidentId: `incident-${(0, import_node_crypto22.randomUUID)()}`
|
|
16239
|
+
});
|
|
16240
|
+
} catch {
|
|
16241
|
+
return;
|
|
16242
|
+
}
|
|
16243
|
+
if (!prepared) return;
|
|
16244
|
+
const charge = capacityRequest(resolved.estimates, resolved.enrollment, {
|
|
16245
|
+
environmentId: record5.state.environmentId,
|
|
16246
|
+
profile: environment.profile,
|
|
16247
|
+
...record5.activeProfile ? { activeProfile: record5.activeProfile } : {},
|
|
16248
|
+
kind: "ensure"
|
|
16249
|
+
});
|
|
16250
|
+
try {
|
|
16251
|
+
queue.enqueue(
|
|
16252
|
+
prepared.request,
|
|
16253
|
+
{
|
|
16254
|
+
...charge,
|
|
16255
|
+
operationId: prepared.operationId,
|
|
16256
|
+
reservationId: (0, import_node_crypto22.randomUUID)(),
|
|
16257
|
+
policyRevision: current.revision
|
|
16258
|
+
},
|
|
16259
|
+
{ estimates: resolved.estimates, enrollment: resolved.enrollment, pool }
|
|
16260
|
+
);
|
|
16261
|
+
} catch {
|
|
16262
|
+
try {
|
|
16263
|
+
retireQueuedLifecycle(prepared.request);
|
|
16264
|
+
} catch {
|
|
16265
|
+
}
|
|
16266
|
+
}
|
|
16267
|
+
},
|
|
15969
16268
|
close() {
|
|
15970
16269
|
lifetime.abort();
|
|
15971
16270
|
queue.close();
|
|
@@ -16096,12 +16395,13 @@ var init_controller_monitor = __esm({
|
|
|
16096
16395
|
ControllerObservationBindingChanged = class extends Error {
|
|
16097
16396
|
};
|
|
16098
16397
|
ControllerMonitor = class {
|
|
16099
|
-
constructor(sessions, collect, serialize, clock = () => Math.floor(performance.now()), fence = withReliabilityObservationFence) {
|
|
16398
|
+
constructor(sessions, collect, serialize, clock = () => Math.floor(performance.now()), fence = withReliabilityObservationFence, recover) {
|
|
16100
16399
|
this.sessions = sessions;
|
|
16101
16400
|
this.collect = collect;
|
|
16102
16401
|
this.serialize = serialize;
|
|
16103
16402
|
this.clock = clock;
|
|
16104
16403
|
this.fence = fence;
|
|
16404
|
+
this.recover = recover;
|
|
16105
16405
|
}
|
|
16106
16406
|
active = /* @__PURE__ */ new Map();
|
|
16107
16407
|
lastStarted = /* @__PURE__ */ new Map();
|
|
@@ -16196,6 +16496,18 @@ var init_controller_monitor = __esm({
|
|
|
16196
16496
|
this.sessions.publish(bindings, projections, commitTime, Date.now());
|
|
16197
16497
|
});
|
|
16198
16498
|
});
|
|
16499
|
+
if (this.recover === void 0 || this.stopped) return;
|
|
16500
|
+
const required = new Set(requirements.map(controllerCapability));
|
|
16501
|
+
const failed = batch.capabilities.filter(
|
|
16502
|
+
(capability) => capability.infrastructure === "failed" && required.has(capability.capability)
|
|
16503
|
+
).map((capability) => capability.capability);
|
|
16504
|
+
if (failed.length > 0)
|
|
16505
|
+
void this.recover(
|
|
16506
|
+
batch.environment,
|
|
16507
|
+
failed,
|
|
16508
|
+
this.active.get(environment.id)?.signal ?? abort.signal
|
|
16509
|
+
).catch(() => {
|
|
16510
|
+
});
|
|
16199
16511
|
}).catch(async (error) => {
|
|
16200
16512
|
if (!this.stopped)
|
|
16201
16513
|
await this.serialize(() => {
|
|
@@ -16219,7 +16531,7 @@ var init_controller_monitor = __esm({
|
|
|
16219
16531
|
async function observeControllerProcess(containerId, name, signal) {
|
|
16220
16532
|
if (!/^[a-f0-9]{12,64}$/.test(containerId) || !/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/.test(name))
|
|
16221
16533
|
throw new Error("Process observation identity is invalid.");
|
|
16222
|
-
const
|
|
16534
|
+
const result = await runControllerProbeStatus(
|
|
16223
16535
|
"docker",
|
|
16224
16536
|
[
|
|
16225
16537
|
"exec",
|
|
@@ -16235,12 +16547,14 @@ async function observeControllerProcess(containerId, name, signal) {
|
|
|
16235
16547
|
],
|
|
16236
16548
|
signal
|
|
16237
16549
|
);
|
|
16238
|
-
|
|
16550
|
+
if (result.code === PROCESS_ABSENT_EXIT) return { kind: "absent" };
|
|
16551
|
+
if (result.code !== 0) throw new Error("Process observation is unavailable.");
|
|
16552
|
+
const identity2 = result.output.trim();
|
|
16239
16553
|
if (!/^[1-9][0-9]* [0-9]+ [a-zA-Z0-9][a-zA-Z0-9._:-]{0,4095}$/.test(identity2))
|
|
16240
16554
|
throw new Error("Process observation is unavailable.");
|
|
16241
|
-
return identity2;
|
|
16555
|
+
return { kind: "present", identity: identity2 };
|
|
16242
16556
|
}
|
|
16243
|
-
var PROCESS_OBSERVATION;
|
|
16557
|
+
var PROCESS_OBSERVATION, PROCESS_ABSENT_EXIT;
|
|
16244
16558
|
var init_controller_process_observation = __esm({
|
|
16245
16559
|
"src/core/controller-process-observation.ts"() {
|
|
16246
16560
|
"use strict";
|
|
@@ -16250,7 +16564,9 @@ set -euo pipefail
|
|
|
16250
16564
|
name="$1"
|
|
16251
16565
|
[[ "$name" =~ ^[a-zA-Z0-9][a-zA-Z0-9._-]*$ ]] || exit 1
|
|
16252
16566
|
file="${"$"}{DEVROUTER_PROCESS_STATE_DIR:-/tmp}/devrouter-process-$name.state"
|
|
16253
|
-
[
|
|
16567
|
+
[ ! -L "$file" ] || exit 1
|
|
16568
|
+
# A missing marker is positive absence: the expected process was never recorded.
|
|
16569
|
+
[ -f "$file" ] || exit 3
|
|
16254
16570
|
[ "$(stat -c %s "$file")" -le 4096 ] || exit 1
|
|
16255
16571
|
before="$(head -c 4097 "$file")"
|
|
16256
16572
|
read -r pid pgid fingerprint extra <<<"$before"
|
|
@@ -16270,11 +16586,13 @@ identity() {
|
|
|
16270
16586
|
tr '\0' '\n' <"/proc/$pid/environ" | grep -Fx "DEVROUTER_PROCESS_FINGERPRINT=$fingerprint" >/dev/null || return 1
|
|
16271
16587
|
printf '%s' "${"$"}{fields[19]}"
|
|
16272
16588
|
}
|
|
16273
|
-
birth="$(identity)"
|
|
16589
|
+
birth="$(identity)" || exit 3
|
|
16274
16590
|
[ "$birth" = "$(identity)" ] || exit 1
|
|
16275
|
-
[ ! -L "$file" ]
|
|
16591
|
+
[ ! -L "$file" ] || exit 1
|
|
16592
|
+
[ "$before" = "$(head -c 4097 "$file")" ] || exit 3
|
|
16276
16593
|
printf '%s %s %s\n' "$pid" "$birth" "$fingerprint"
|
|
16277
16594
|
`;
|
|
16595
|
+
PROCESS_ABSENT_EXIT = 3;
|
|
16278
16596
|
}
|
|
16279
16597
|
});
|
|
16280
16598
|
|
|
@@ -16490,10 +16808,14 @@ var init_controller_observation = __esm({
|
|
|
16490
16808
|
];
|
|
16491
16809
|
const processes = [];
|
|
16492
16810
|
let processesVerified = healthy;
|
|
16811
|
+
let processesAbsent = false;
|
|
16493
16812
|
if (healthy && requirements.some((value) => value.startsWith("app:"))) {
|
|
16494
16813
|
try {
|
|
16495
|
-
for (const name of profile.managedRuntime.processes)
|
|
16496
|
-
|
|
16814
|
+
for (const name of profile.managedRuntime.processes) {
|
|
16815
|
+
const observation = await observeControllerProcess(primary[0].id, name, signal);
|
|
16816
|
+
if (observation.kind === "absent") processesAbsent = true;
|
|
16817
|
+
else processes.push(observation.identity);
|
|
16818
|
+
}
|
|
16497
16819
|
} catch (error) {
|
|
16498
16820
|
if (signal.aborted) throw error;
|
|
16499
16821
|
processesVerified = false;
|
|
@@ -16507,7 +16829,7 @@ var init_controller_observation = __esm({
|
|
|
16507
16829
|
const routeMatches = routes.some(
|
|
16508
16830
|
(route) => route.workspace === environment.workspace && route.name === app.name && route.host === app.host && route.mode === "proxy" && route.upstreamHost === upstream.upstreamHost && route.port === upstream.port
|
|
16509
16831
|
);
|
|
16510
|
-
const http2 = healthy && routeMatches && processesVerified ? await probeHttpReadiness(
|
|
16832
|
+
const http2 = healthy && routeMatches && processesVerified && !processesAbsent ? await probeHttpReadiness(
|
|
16511
16833
|
app.host,
|
|
16512
16834
|
app.readiness,
|
|
16513
16835
|
signal,
|
|
@@ -16516,7 +16838,7 @@ var init_controller_observation = __esm({
|
|
|
16516
16838
|
) : void 0;
|
|
16517
16839
|
capabilities.push({
|
|
16518
16840
|
capability: controllerCapability(requirement),
|
|
16519
|
-
infrastructure: healthy && routeMatches ? "healthy" : "unknown",
|
|
16841
|
+
infrastructure: healthy && routeMatches ? processesAbsent ? "failed" : "healthy" : "unknown",
|
|
16520
16842
|
application: http2?.ok ? "verified" : http2?.classification === "application-contract" ? "unready" : "unverified",
|
|
16521
16843
|
observedAtMs: 0,
|
|
16522
16844
|
validForMs: 15e3
|
|
@@ -16524,13 +16846,15 @@ var init_controller_observation = __esm({
|
|
|
16524
16846
|
}
|
|
16525
16847
|
if (processesVerified && requirements.some((value) => value.startsWith("app:"))) {
|
|
16526
16848
|
try {
|
|
16527
|
-
for (let index = 0; index < profile.managedRuntime.processes.length; index++)
|
|
16528
|
-
|
|
16849
|
+
for (let index = 0; index < profile.managedRuntime.processes.length; index++) {
|
|
16850
|
+
const recheck = await observeControllerProcess(
|
|
16529
16851
|
primary[0].id,
|
|
16530
16852
|
profile.managedRuntime.processes[index],
|
|
16531
16853
|
signal
|
|
16532
|
-
)
|
|
16854
|
+
);
|
|
16855
|
+
if (recheck.kind !== "present" || processes[index] !== recheck.identity)
|
|
16533
16856
|
throw new Error("Observation process changed.");
|
|
16857
|
+
}
|
|
16534
16858
|
} catch (error) {
|
|
16535
16859
|
if (signal.aborted) throw error;
|
|
16536
16860
|
for (const capability of capabilities)
|
|
@@ -16841,12 +17165,19 @@ async function runController(options) {
|
|
|
16841
17165
|
let serial = Promise.resolve();
|
|
16842
17166
|
const monotonic = () => Math.floor(performance.now());
|
|
16843
17167
|
let fatal;
|
|
16844
|
-
const monitor = options.collect ? new ControllerMonitor(
|
|
16845
|
-
|
|
16846
|
-
|
|
16847
|
-
|
|
16848
|
-
|
|
16849
|
-
|
|
17168
|
+
const monitor = options.collect ? new ControllerMonitor(
|
|
17169
|
+
sessions,
|
|
17170
|
+
options.collect,
|
|
17171
|
+
(operation) => {
|
|
17172
|
+
const pending = serial.then(operation);
|
|
17173
|
+
serial = pending.catch(() => {
|
|
17174
|
+
});
|
|
17175
|
+
return pending;
|
|
17176
|
+
},
|
|
17177
|
+
void 0,
|
|
17178
|
+
void 0,
|
|
17179
|
+
operations?.recover
|
|
17180
|
+
) : void 0;
|
|
16850
17181
|
const server = import_node_net4.default.createServer((socket) => {
|
|
16851
17182
|
if (sockets.size >= 32) {
|
|
16852
17183
|
socket.destroy();
|
|
@@ -17920,8 +18251,8 @@ function buildOnboardingPrompt(options = {}) {
|
|
|
17920
18251
|
"- Lifecycle: after one-time setup, use `devrouter ensure .` for both primary and linked checkouts; never branch on checkout kind or use live verify as startup. Managed consumer images contain no devrouter package/helper: ensure delivers its matching helper at runtime and invokes an exact captured snapshot of the repository-owned post-start adapter. Keep `.devrouter.yml` as the only consumer-side version pin. Use `devrouter stop .` for a non-destructive pause, `devrouter stop . --delete` only for explicit exact-owner cleanup without removing the checkout, and `devrouter exec . -- <command...>` for container commands. Never substitute raw DevPod/Devsy mutations; they bypass the machine-global ownership lock, which serializes provider mutations in a fair arrival-order queue and lets contenders wait up to thirty minutes with throttled stderr progress before failing with the queue position or holder PID and true durations. `workspace up` creates linked worktrees; destructive worktree removal and GC remain ledger-scoped.",
|
|
17921
18252
|
"- After a drained interrupted ensure, manual exec can use an exactly proven retained Devsy runtime with the plain local Docker command. Revalidate identity for each tooling command until later preparation reconciles startup; preserve configuration, data and interrupted history. This does not repair absent stop baselines or provide OOM protection.",
|
|
17922
18253
|
"- Busy ensure and exec wait up to thirty minutes for a positively identified active lifecycle worker on the same checkout, with stderr progress. Do not stop healthy work to make room. Commands stay serial; timeout or cancellation before admission leaves prior work untouched. A lifecycle fence change cancels admission, and uncertain execution is never replayed.",
|
|
17923
|
-
"- Ensure reconciles interrupted or never-dispatched ensure after positive worker drainage. Unknown arbitrary exec never replays; use explicit stop to reconcile that uncertainty. Preserve journals and generated configuration if evidence is unavailable. This does not prevent OOM
|
|
17924
|
-
"- Capacity admission is opt-in: only an operator-enabled capacity policy with an enrolled checkout routes ensure and exec through controller admission. The CLI follows the decision with bounded reconnecting waits that survive controller restarts, worker results are journalled atomically, and stop bypasses admission. Without an enabled policy lifecycle behavior is unchanged.",
|
|
18254
|
+
"- Ensure reconciles interrupted or never-dispatched ensure after positive worker drainage. Unknown arbitrary exec never replays; use explicit stop to reconcile that uncertainty. Preserve journals and generated configuration if evidence is unavailable. This does not detect or prevent OOM; bounded automatic recovery runs only when the operator enables the capacity recovery policy.",
|
|
18255
|
+
"- Capacity admission is opt-in: only an operator-enabled capacity policy with an enrolled checkout routes ensure and exec through controller admission. The CLI follows the decision with bounded reconnecting waits that survive controller restarts, worker results are journalled atomically, and stop bypasses admission. Without an enabled policy lifecycle behavior is unchanged. An optional recovery block adds bounded automatic recovery: one journal-admitted corrective ensure per positively failed required capability, bounded by per-scope restart counts, an aggregate corrective-action cap and a rolling observation window. It is inert unless enabled.",
|
|
17925
18256
|
"- A lock that cannot prove its owner fails closed with the failing inspection stage, the exact lock path, and a portable reproduction command; run the canonical command in a permitted host context. No identity fallback is attempted, and `devrouter controller` prints the underlying cause on stderr while keeping the stable `controller-unavailable` JSON on stdout.",
|
|
17926
18257
|
"- Devsy agent readiness: run `devrouter setup --yes --workspace-runtime devsy` once so Devrouter acquires and verifies the pinned agent in its own machine cache. `doctor` reports `ready`, `missing`, `stale`, or `invalid` without network access, and `ensure` fails before the provider queue when readiness is not `ready`. An explicit `DEVSY_AGENT_BINARY` remains authoritative and must match a pinned official asset.",
|
|
17927
18258
|
"- A managed adapter plus `postCreateCommand` requires `waitFor` exactly `postCreateCommand` or `postStartCommand`; managed selective config preserves lifecycle fields and changes only `runServices`.",
|
|
@@ -20400,7 +20731,7 @@ async function buildDoctorReport(options = {}) {
|
|
|
20400
20731
|
const config = runtimeConfig.config;
|
|
20401
20732
|
loadedConfig = config;
|
|
20402
20733
|
loadedWorkspace = runtimeConfig.workspace;
|
|
20403
|
-
const cliVersion = true ? "0.0.
|
|
20734
|
+
const cliVersion = true ? "0.0.73" : "0.0.0-dev";
|
|
20404
20735
|
const configVersion = config.devrouter?.version;
|
|
20405
20736
|
if (configVersion && cliVersion !== "0.0.0-dev" && compareSemver(configVersion, cliVersion) > 0) {
|
|
20406
20737
|
addCheck(checks, {
|
|
@@ -25830,7 +26161,7 @@ var init_version = __esm({
|
|
|
25830
26161
|
|
|
25831
26162
|
// src/cli.ts
|
|
25832
26163
|
var import_commander = require("commander");
|
|
25833
|
-
var CLI_VERSION = true ? "0.0.
|
|
26164
|
+
var CLI_VERSION = true ? "0.0.73" : "0.0.0-dev";
|
|
25834
26165
|
var VERSION_FLAGS = /* @__PURE__ */ new Set(["-V", "--version"]);
|
|
25835
26166
|
function withErrorHandling(action2) {
|
|
25836
26167
|
return async (...args) => {
|
package/package.json
CHANGED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Upgrade to devrouter 0.0.72
|
|
2
|
+
|
|
3
|
+
This patch release lets capacity-enrolled checkouts keep working past the
|
|
4
|
+
128-entry lifecycle journal cap. Previously the journal refused every later
|
|
5
|
+
`ensure` and `exec` once it filled, because history rollover ran only under
|
|
6
|
+
manual execution policy. Enrolled repositories now roll over under the same
|
|
7
|
+
conservative guards as manual ones. No schema, configuration, or CLI flag
|
|
8
|
+
changes.
|
|
9
|
+
|
|
10
|
+
## Task
|
|
11
|
+
|
|
12
|
+
Install the published release and update `.devrouter.yml` devrouter.version to
|
|
13
|
+
0.0.72. No configuration change is required.
|
|
14
|
+
|
|
15
|
+
## Validation
|
|
16
|
+
|
|
17
|
+
On a repository with an enabled capacity policy enrollment whose journal is at
|
|
18
|
+
or near its cap, run `devrouter ensure <path> --json` and confirm the operation
|
|
19
|
+
is admitted and dispatched instead of failing with a blocked lifecycle
|
|
20
|
+
transition. `devrouter exec <path> -- echo ok` must complete, and
|
|
21
|
+
`devrouter stop <path>` must end with the runtime stopped and zero routes.
|
|
22
|
+
|
|
23
|
+
## Report
|
|
24
|
+
|
|
25
|
+
Report the installed CLI version, the enrolled repository, the journal entry
|
|
26
|
+
count before and after the operation, and the final stopped status with the
|
|
27
|
+
route count.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Upgrade to devrouter 0.0.73
|
|
2
|
+
|
|
3
|
+
This patch release adds opt-in bounded automatic recovery and fixes three
|
|
4
|
+
capacity-managed defects found while dogfooding the eLearning canary.
|
|
5
|
+
|
|
6
|
+
- Capacity-enrolled `exec` now returns the wrapped command's output. The
|
|
7
|
+
controller captured a supervised worker's stdout and stderr into its bounded
|
|
8
|
+
buffer and the client exposed it through `onOutput`, but the CLI never
|
|
9
|
+
forwarded it, so `devrouter exec <path> -- <command>` returned only the exit
|
|
10
|
+
status. The controller-supervised path now streams each bounded page to the
|
|
11
|
+
caller and prints one notice if the buffer dropped earlier bytes.
|
|
12
|
+
- A capacity-enrolled operation keeps its effect authority while the controller
|
|
13
|
+
cannot observe capacity. Renewal previously wrote a zero deadline before each
|
|
14
|
+
attempt and again on failure, so one missing, stale, or unknown sample revoked
|
|
15
|
+
authority mid-start and later commands failed with `Capacity effect authority
|
|
16
|
+
is absent or stale.` Renewal now extends only when every reserved domain has a
|
|
17
|
+
usable sample. Ownership evidence also compares environment identity instead
|
|
18
|
+
of whole journal records, so the controller rewriting its own bookkeeping no
|
|
19
|
+
longer reports `unknown` for a healthy runtime.
|
|
20
|
+
- The observation collector reports a required managed process that has
|
|
21
|
+
positively exited as a failed capability instead of `unknown`, so bounded
|
|
22
|
+
recovery can act on it. Ambiguous or unreadable process state stays `unknown`.
|
|
23
|
+
- Optional bounded automatic recovery. A new `recovery` block in the capacity
|
|
24
|
+
policy enables a supervisor that reacts to a positively failed required
|
|
25
|
+
capability with one journal-admitted corrective `ensure`, bounded by
|
|
26
|
+
per-scope restart counts, an aggregate corrective-action cap and a rolling
|
|
27
|
+
observation window. The block is absent by default. Parking and controlled
|
|
28
|
+
resume remain model-only, and there is no OOM detection: a container-local OOM
|
|
29
|
+
still surfaces only as a failed capability.
|
|
30
|
+
|
|
31
|
+
No `.devrouter.yml` schema, CLI flag, or default behavior changes.
|
|
32
|
+
|
|
33
|
+
## Task
|
|
34
|
+
|
|
35
|
+
Install the published release and update `.devrouter.yml` devrouter.version to
|
|
36
|
+
0.0.73. No configuration change is required. Enabling bounded recovery is
|
|
37
|
+
optional: add a `recovery` block to the operator capacity policy only when you
|
|
38
|
+
want automatic corrective ensures.
|
|
39
|
+
|
|
40
|
+
## Validation
|
|
41
|
+
|
|
42
|
+
On a capacity-enrolled checkout, run
|
|
43
|
+
`devrouter exec <path> -- bash -lc 'echo checked'` and confirm the command's
|
|
44
|
+
output appears. Confirm a failing command still returns its non-zero exit
|
|
45
|
+
status, and that `devrouter ensure <path>` reports the environment ready with
|
|
46
|
+
its routes published. If you enabled recovery, confirm an environment whose
|
|
47
|
+
required process is positively absent is corrected by one admitted `ensure`
|
|
48
|
+
within the configured bound.
|
|
49
|
+
|
|
50
|
+
## Report
|
|
51
|
+
|
|
52
|
+
Report the installed CLI version, the enrolled repository, the observed `exec`
|
|
53
|
+
output and exit code, the final `ensure` readiness with its route count, and
|
|
54
|
+
whether the recovery policy was enabled.
|