@proagentstore/cli 0.4.108 → 0.4.110
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/bootstrap.js +1 -1
- package/dist/{chunk-JOWYSZOL.js → chunk-RGNU4W6B.js} +6 -2
- package/dist/index.js +346 -260
- package/package.json +1 -1
package/dist/bootstrap.js
CHANGED
|
@@ -12,6 +12,9 @@ var KEEP = 2;
|
|
|
12
12
|
function payloadRoot() {
|
|
13
13
|
return process.env.PAGS_CLI_CACHE || join(homedir(), ".config", "proagentstore", "cli");
|
|
14
14
|
}
|
|
15
|
+
function isVersion(v) {
|
|
16
|
+
return typeof v === "string" && /^(\d+)\.(\d+)\.(\d+)/.test(v.trim());
|
|
17
|
+
}
|
|
15
18
|
function olderThan(a, b) {
|
|
16
19
|
const parse = (v) => /^(\d+)\.(\d+)\.(\d+)/.exec(v.trim())?.slice(1).map(Number);
|
|
17
20
|
const x = parse(a);
|
|
@@ -44,9 +47,9 @@ function cachedPayloads(root = payloadRoot()) {
|
|
|
44
47
|
function newestPayload(payloads) {
|
|
45
48
|
return payloads.reduce((best, p) => !best || olderThan(best.version, p.version) ? p : best, null);
|
|
46
49
|
}
|
|
47
|
-
async function latestPublishedVersion(timeoutMs = 3e4) {
|
|
50
|
+
async function latestPublishedVersion(timeoutMs = 3e4, signal) {
|
|
48
51
|
try {
|
|
49
|
-
const { stdout } = await run("npm", ["view", CLI_PACKAGE, "version"], { timeout: timeoutMs });
|
|
52
|
+
const { stdout } = await run("npm", ["view", CLI_PACKAGE, "version"], { timeout: timeoutMs, signal });
|
|
50
53
|
return stdout.trim() || null;
|
|
51
54
|
} catch {
|
|
52
55
|
return null;
|
|
@@ -88,6 +91,7 @@ export {
|
|
|
88
91
|
CLI_PACKAGE,
|
|
89
92
|
BOOTSTRAP_ENV,
|
|
90
93
|
NO_SELF_UPDATE_ENV,
|
|
94
|
+
isVersion,
|
|
91
95
|
olderThan,
|
|
92
96
|
payloadAt,
|
|
93
97
|
cachedPayloads,
|
package/dist/index.js
CHANGED
|
@@ -3,9 +3,10 @@ import {
|
|
|
3
3
|
BOOTSTRAP_ENV,
|
|
4
4
|
CLI_PACKAGE,
|
|
5
5
|
installPayload,
|
|
6
|
+
isVersion,
|
|
6
7
|
latestPublishedVersion,
|
|
7
8
|
olderThan
|
|
8
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-RGNU4W6B.js";
|
|
9
10
|
|
|
10
11
|
// src/index.ts
|
|
11
12
|
import { createRequire as createRequire3 } from "module";
|
|
@@ -688,16 +689,16 @@ function describeCandidate(candidate, nowMs) {
|
|
|
688
689
|
const agents = `${candidate.agentCount} agent${candidate.agentCount === 1 ? "" : "s"}`;
|
|
689
690
|
return `${relativeAge(candidate.lastSeenAt, nowMs)} \xB7 ${agents}`;
|
|
690
691
|
}
|
|
691
|
-
function parseSelection(input,
|
|
692
|
+
function parseSelection(input, count2) {
|
|
692
693
|
const raw = input.trim().toLowerCase();
|
|
693
694
|
if (!raw || raw === "n" || raw === "no" || raw === "s" || raw === "skip" || raw === "none") return { kind: "skip" };
|
|
694
|
-
if (raw === "a" || raw === "all") return { kind: "pick", indices: Array.from({ length:
|
|
695
|
+
if (raw === "a" || raw === "all") return { kind: "pick", indices: Array.from({ length: count2 }, (_, i) => i) };
|
|
695
696
|
const parts = raw.split(/[\s,]+/).filter(Boolean);
|
|
696
697
|
const indices = [];
|
|
697
698
|
for (const part of parts) {
|
|
698
699
|
if (!/^\d+$/.test(part)) return { kind: "invalid", message: `"${part}" is not a number.` };
|
|
699
700
|
const n = Number.parseInt(part, 10);
|
|
700
|
-
if (n < 1 || n >
|
|
701
|
+
if (n < 1 || n > count2) return { kind: "invalid", message: `${n} is not on the list (1\u2013${count2}).` };
|
|
701
702
|
if (!indices.includes(n - 1)) indices.push(n - 1);
|
|
702
703
|
}
|
|
703
704
|
return indices.length ? { kind: "pick", indices } : { kind: "skip" };
|
|
@@ -832,7 +833,7 @@ function buildRuntimeRegistrationBody(opts, capabilities = []) {
|
|
|
832
833
|
machineNames: machine.names
|
|
833
834
|
};
|
|
834
835
|
}
|
|
835
|
-
async function requestRunner(method, path, opts, body) {
|
|
836
|
+
async function requestRunner(method, path, opts, body, signal) {
|
|
836
837
|
const headers = {
|
|
837
838
|
...runnerRequestHeaders(opts)
|
|
838
839
|
};
|
|
@@ -840,7 +841,8 @@ async function requestRunner(method, path, opts, body) {
|
|
|
840
841
|
const res = await fetch(`${runnerBaseUrl(opts.url)}${path}`, {
|
|
841
842
|
method,
|
|
842
843
|
headers,
|
|
843
|
-
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
844
|
+
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
845
|
+
signal
|
|
844
846
|
});
|
|
845
847
|
const { text, data } = await readResponse(res);
|
|
846
848
|
if (!res.ok) {
|
|
@@ -849,7 +851,7 @@ async function requestRunner(method, path, opts, body) {
|
|
|
849
851
|
}
|
|
850
852
|
return data;
|
|
851
853
|
}
|
|
852
|
-
async function requestPags(method, path, opts, body) {
|
|
854
|
+
async function requestPags(method, path, opts, body, signal) {
|
|
853
855
|
const headers = {
|
|
854
856
|
...pagsHeaders(opts.pagsToken)
|
|
855
857
|
};
|
|
@@ -860,7 +862,8 @@ async function requestPags(method, path, opts, body) {
|
|
|
860
862
|
const res = await fetch(`${pagsApiBase(opts.apiBase)}${path}`, {
|
|
861
863
|
method,
|
|
862
864
|
headers,
|
|
863
|
-
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
865
|
+
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
866
|
+
signal
|
|
864
867
|
});
|
|
865
868
|
const { text, data } = await readResponse(res);
|
|
866
869
|
if (!res.ok) {
|
|
@@ -1629,6 +1632,12 @@ function autoUpdateStatusWire(status) {
|
|
|
1629
1632
|
failure: "failed"
|
|
1630
1633
|
}[status] ?? status;
|
|
1631
1634
|
}
|
|
1635
|
+
function parseRecovery(raw) {
|
|
1636
|
+
const r = raw;
|
|
1637
|
+
if (!r || typeof r.target !== "string" || r.target.length > 80 || typeof r.at !== "string" || !Array.isArray(r.attachments)) return void 0;
|
|
1638
|
+
if (r.attachments.length > 200 || !r.attachments.every((a) => typeof a === "string" && a.length > 0 && a.length <= 128)) return void 0;
|
|
1639
|
+
return { target: r.target, attachments: [...r.attachments], at: r.at };
|
|
1640
|
+
}
|
|
1632
1641
|
var statuses = /* @__PURE__ */ new Set([
|
|
1633
1642
|
"enabled",
|
|
1634
1643
|
"checking",
|
|
@@ -1665,12 +1674,14 @@ function parseAutoUpdatePolicy(value) {
|
|
|
1665
1674
|
const latestVersion = raw.latestVersion ?? raw.latest_version;
|
|
1666
1675
|
const lastAttemptAt = raw.lastAttemptAt ?? raw.last_attempt_at;
|
|
1667
1676
|
const reason = raw.reason;
|
|
1677
|
+
const recovery = parseRecovery(raw.recovery);
|
|
1668
1678
|
return {
|
|
1669
1679
|
autoUpdate,
|
|
1670
1680
|
...status ? { status } : {},
|
|
1671
1681
|
...typeof latestVersion === "string" ? { latestVersion } : {},
|
|
1672
1682
|
...typeof lastAttemptAt === "string" ? { lastAttemptAt } : {},
|
|
1673
|
-
...typeof reason === "string" ? { reason } : {}
|
|
1683
|
+
...typeof reason === "string" ? { reason } : {},
|
|
1684
|
+
...recovery ? { recovery } : {}
|
|
1674
1685
|
};
|
|
1675
1686
|
}
|
|
1676
1687
|
function policyFromResponse(value) {
|
|
@@ -1726,12 +1737,25 @@ function policyScheduleAction(previous, next, wasAuthoritative) {
|
|
|
1726
1737
|
return "keep";
|
|
1727
1738
|
}
|
|
1728
1739
|
function acceptPolicyResponse(local, wasAuthoritative, response) {
|
|
1729
|
-
const
|
|
1730
|
-
if (!
|
|
1740
|
+
const parsed = policyFromResponse(response);
|
|
1741
|
+
if (!parsed) return null;
|
|
1742
|
+
const { recovery: _fromService, ...remote } = parsed;
|
|
1731
1743
|
const action = policyScheduleAction(local, remote, wasAuthoritative);
|
|
1732
1744
|
const ownerTransition = action === "cancel" || !local.autoUpdate;
|
|
1733
1745
|
return { policy: ownerTransition ? { ...local, ...remote } : { ...local, autoUpdate: remote.autoUpdate }, action };
|
|
1734
1746
|
}
|
|
1747
|
+
var ResponseFence = class {
|
|
1748
|
+
sent = 0;
|
|
1749
|
+
accepted = 0;
|
|
1750
|
+
next() {
|
|
1751
|
+
return ++this.sent;
|
|
1752
|
+
}
|
|
1753
|
+
accepts(sentAs) {
|
|
1754
|
+
if (sentAs < this.accepted) return false;
|
|
1755
|
+
this.accepted = sentAs;
|
|
1756
|
+
return true;
|
|
1757
|
+
}
|
|
1758
|
+
};
|
|
1735
1759
|
function mayAutomaticallyRestart(input) {
|
|
1736
1760
|
return input.authoritative && input.enabled && input.workObserved && input.busy.length === 0;
|
|
1737
1761
|
}
|
|
@@ -1739,6 +1763,50 @@ function mayAutomaticallyRestart(input) {
|
|
|
1739
1763
|
// src/commands/runner/auto-update-controller.ts
|
|
1740
1764
|
var MAX_OBSERVATIONS = 5;
|
|
1741
1765
|
var IDLE_RETRY_MS = 15e3;
|
|
1766
|
+
var OBSERVATION_TIMEOUT_MS = 3e4;
|
|
1767
|
+
async function boundedWork(work, bound = {}) {
|
|
1768
|
+
const abort = new AbortController();
|
|
1769
|
+
let timer;
|
|
1770
|
+
const expired = new Promise((resolve5) => {
|
|
1771
|
+
timer = (bound.timer ?? ((fn, ms) => setTimeout(fn, ms)))(() => {
|
|
1772
|
+
abort.abort();
|
|
1773
|
+
resolve5(null);
|
|
1774
|
+
}, bound.ms ?? OBSERVATION_TIMEOUT_MS);
|
|
1775
|
+
timer.unref?.();
|
|
1776
|
+
});
|
|
1777
|
+
const running = work(abort.signal);
|
|
1778
|
+
running.catch(() => void 0);
|
|
1779
|
+
try {
|
|
1780
|
+
return await Promise.race([running, expired]);
|
|
1781
|
+
} finally {
|
|
1782
|
+
if (timer) (bound.clear ?? ((t) => clearTimeout(t)))(timer);
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
function updateVerdict(attempt, facts, attachedNow = []) {
|
|
1786
|
+
if (!isVersion(facts.current) || !isVersion(facts.latest)) return { kind: "unverified", reason: `the running (${facts.current}) or published (${facts.latest}) version could not be read` };
|
|
1787
|
+
const cycle = attempt.recovery;
|
|
1788
|
+
if (!cycle) {
|
|
1789
|
+
if (attempt.status === "running" || attempt.status === "restarting") return { kind: "unknown", reason: "an update attempt was recorded by an older runner without the record needed to verify it" };
|
|
1790
|
+
return { kind: "current" };
|
|
1791
|
+
}
|
|
1792
|
+
if (!isVersion(cycle.target)) return { kind: "unverified", reason: `the attempted update's target (${cycle.target}) could not be read` };
|
|
1793
|
+
if (olderThan(facts.current, cycle.target)) return { kind: "unverified", reason: `the runner came back on ${facts.current}, not the attempted ${cycle.target}` };
|
|
1794
|
+
const observed = facts.observations ?? [];
|
|
1795
|
+
if (!observed.length) return { kind: "unverified", reason: `the runner is on ${facts.current}, but no health look was taken` };
|
|
1796
|
+
const look = (source) => {
|
|
1797
|
+
const o = observed.filter((x) => x.source === source).at(-1);
|
|
1798
|
+
return !o ? "missing" : o.malformed ? "malformed" : o.success ? "ok" : "failed";
|
|
1799
|
+
};
|
|
1800
|
+
const sessions = look("sessions");
|
|
1801
|
+
const health = look("health");
|
|
1802
|
+
if (sessions !== "ok" || health !== "ok" || facts.busy.includes("runner-work-observation-unavailable")) {
|
|
1803
|
+
return { kind: "unverified", reason: `the runner is on ${facts.current}, but its local work look was not readable (sessions ${sessions}, health ${health})` };
|
|
1804
|
+
}
|
|
1805
|
+
const back = new Set(attachedNow);
|
|
1806
|
+
const missing = cycle.attachments.filter((id) => !back.has(id));
|
|
1807
|
+
if (missing.length) return { kind: "unverified", reason: `the runner is on ${facts.current}, but only ${cycle.attachments.length - missing.length} of its ${cycle.attachments.length} original agents are attached again` };
|
|
1808
|
+
return { kind: "verified" };
|
|
1809
|
+
}
|
|
1742
1810
|
function blockerClassFor(waitingFor) {
|
|
1743
1811
|
if (waitingFor.includes("runner-work-observation-unavailable")) return "observation_failure";
|
|
1744
1812
|
return waitingFor.length ? "active_sessions" : "unknown";
|
|
@@ -1748,6 +1816,7 @@ var AutomaticUpdateController = class {
|
|
|
1748
1816
|
this.deps = deps;
|
|
1749
1817
|
this.setTimer = deps.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
|
|
1750
1818
|
this.clearTimer = deps.clearTimer ?? ((timer) => clearTimeout(timer));
|
|
1819
|
+
this.boundTimer = deps.boundTimer ?? ((fn, ms) => setTimeout(fn, ms));
|
|
1751
1820
|
this.now = deps.now ?? Date.now;
|
|
1752
1821
|
}
|
|
1753
1822
|
deps;
|
|
@@ -1764,6 +1833,7 @@ var AutomaticUpdateController = class {
|
|
|
1764
1833
|
now;
|
|
1765
1834
|
setTimer;
|
|
1766
1835
|
clearTimer;
|
|
1836
|
+
boundTimer;
|
|
1767
1837
|
/** The controller's own record, safe to report: times, a normalized class, and observation outcomes. */
|
|
1768
1838
|
diagnostics() {
|
|
1769
1839
|
return { checkAt: this.checkAt, decisionAt: this.decisionAt, nextRetryDueAt: this.nextRetryDueAt, blockerClass: this.blockerClass, observations: [...this.observations] };
|
|
@@ -1781,7 +1851,7 @@ var AutomaticUpdateController = class {
|
|
|
1781
1851
|
if (this.checking || this.waiting || !this.retryable()) return;
|
|
1782
1852
|
this.checking = true;
|
|
1783
1853
|
try {
|
|
1784
|
-
if (!await this.deps.refreshPolicy() || !this.enabled()) {
|
|
1854
|
+
if (!await this.bounded((signal) => this.deps.refreshPolicy(signal)) || !this.enabled()) {
|
|
1785
1855
|
this.offline();
|
|
1786
1856
|
return;
|
|
1787
1857
|
}
|
|
@@ -1833,6 +1903,7 @@ var AutomaticUpdateController = class {
|
|
|
1833
1903
|
*/
|
|
1834
1904
|
offline() {
|
|
1835
1905
|
if (!this.retryable()) return;
|
|
1906
|
+
this.checkAt = this.now();
|
|
1836
1907
|
this.failures++;
|
|
1837
1908
|
this.deps.status("offline", { reason: "The owner policy could not be refreshed; automatic update is deferred." });
|
|
1838
1909
|
this.schedule();
|
|
@@ -1842,17 +1913,41 @@ var AutomaticUpdateController = class {
|
|
|
1842
1913
|
this.deps.status("failure", { reason: error instanceof Error ? error.message : String(error) });
|
|
1843
1914
|
this.schedule();
|
|
1844
1915
|
}
|
|
1916
|
+
/**
|
|
1917
|
+
* One bounded piece of work (#1020): a policy refresh or a facts look. Past {@link OBSERVATION_TIMEOUT_MS} its
|
|
1918
|
+
* signal is aborted (the relay drops that request's response, so a late answer can never restore authority) and
|
|
1919
|
+
* this resolves null — the race has settled, so the late result cannot overwrite a newer one or reach planning,
|
|
1920
|
+
* install or restart. A late rejection is absorbed rather than left unhandled.
|
|
1921
|
+
*/
|
|
1922
|
+
bounded(work) {
|
|
1923
|
+
return boundedWork(work, { timer: this.boundTimer, clear: this.clearTimer });
|
|
1924
|
+
}
|
|
1845
1925
|
async decide() {
|
|
1846
|
-
const facts = await this.deps.facts();
|
|
1926
|
+
const facts = await this.bounded((signal) => this.deps.facts(signal));
|
|
1847
1927
|
this.checkAt = this.now();
|
|
1928
|
+
if (!facts) {
|
|
1929
|
+
this.failures++;
|
|
1930
|
+
this.deps.status("failure", { reason: `The local work and registry check did not answer within ${OBSERVATION_TIMEOUT_MS / 1e3}s; it will be retried.` });
|
|
1931
|
+
this.schedule();
|
|
1932
|
+
return;
|
|
1933
|
+
}
|
|
1848
1934
|
if (facts.observations?.length) this.observations = [...this.observations, ...facts.observations].slice(-MAX_OBSERVATIONS);
|
|
1849
1935
|
if (!this.enabled()) return this.offline();
|
|
1850
1936
|
const plan = this.deps.plan(facts);
|
|
1851
1937
|
this.decisionAt = this.now();
|
|
1852
1938
|
this.blockerClass = plan.action === "wait" ? blockerClassFor(plan.waitingFor) : null;
|
|
1853
1939
|
if (plan.action === "up-to-date") {
|
|
1854
|
-
this.
|
|
1855
|
-
|
|
1940
|
+
const verdict = updateVerdict(this.deps.policy().value, facts, this.deps.attachments?.() ?? []);
|
|
1941
|
+
if (verdict.kind === "unverified") {
|
|
1942
|
+
this.failures++;
|
|
1943
|
+
this.deps.status("failure", { reason: `Not verified: ${verdict.reason}.` });
|
|
1944
|
+
} else if (verdict.kind === "unknown") {
|
|
1945
|
+
this.failures = 0;
|
|
1946
|
+
this.deps.status("enabled", { latestVersion: facts.latest ?? void 0, reason: `Not verified: ${verdict.reason}.` });
|
|
1947
|
+
} else {
|
|
1948
|
+
this.failures = 0;
|
|
1949
|
+
this.deps.status(verdict.kind === "verified" ? "verified-success" : "enabled", { latestVersion: facts.latest ?? void 0, reason: void 0, recovery: void 0 });
|
|
1950
|
+
}
|
|
1856
1951
|
this.schedule();
|
|
1857
1952
|
return;
|
|
1858
1953
|
}
|
|
@@ -1866,7 +1961,7 @@ var AutomaticUpdateController = class {
|
|
|
1866
1961
|
this.defer();
|
|
1867
1962
|
return;
|
|
1868
1963
|
}
|
|
1869
|
-
this.deps.status("running", { latestVersion: plan.latest, reason: void 0 });
|
|
1964
|
+
this.deps.status("running", { latestVersion: plan.latest, reason: void 0, recovery: { target: plan.latest, attachments: [...this.deps.attachments?.() ?? []], at: new Date(this.now()).toISOString() } });
|
|
1870
1965
|
const restarted = await this.install(plan, true);
|
|
1871
1966
|
if (restarted) {
|
|
1872
1967
|
this.deps.status("restarting", { latestVersion: plan.latest, reason: void 0 });
|
|
@@ -1885,7 +1980,7 @@ var AutomaticUpdateController = class {
|
|
|
1885
1980
|
void (async () => {
|
|
1886
1981
|
this.waiting = false;
|
|
1887
1982
|
try {
|
|
1888
|
-
if (!await this.deps.refreshPolicy() || !this.enabled()) {
|
|
1983
|
+
if (!await this.bounded((signal) => this.deps.refreshPolicy(signal)) || !this.enabled()) {
|
|
1889
1984
|
this.offline();
|
|
1890
1985
|
return;
|
|
1891
1986
|
}
|
|
@@ -1937,14 +2032,17 @@ var RelayMutationAdmission = class {
|
|
|
1937
2032
|
};
|
|
1938
2033
|
|
|
1939
2034
|
// src/commands/runner/runner-work-observation.ts
|
|
2035
|
+
var count = (v) => typeof v === "number" && Number.isInteger(v) && v >= 0;
|
|
2036
|
+
function isWorkShape(work) {
|
|
2037
|
+
if (!work || typeof work !== "object" || Array.isArray(work)) return false;
|
|
2038
|
+
const w2 = work;
|
|
2039
|
+
return count(w2.codingTurns) && count(w2.localRuns) && Array.isArray(w2.detail) && w2.detail.every((item) => typeof item === "string");
|
|
2040
|
+
}
|
|
1940
2041
|
function observedRunnerWork(observation) {
|
|
1941
2042
|
const sessions = observation.sessions;
|
|
1942
2043
|
const work = observation.work;
|
|
1943
|
-
if (!Array.isArray(sessions) || !work
|
|
1944
|
-
const
|
|
1945
|
-
const localRuns = typeof work.localRuns === "number" && Number.isInteger(work.localRuns) && work.localRuns >= 0 ? work.localRuns : null;
|
|
1946
|
-
const detail = Array.isArray(work.detail) && work.detail.every((item) => typeof item === "string") ? work.detail : null;
|
|
1947
|
-
if (codingTurns === null || localRuns === null || detail === null) return ["runner-work-observation-unavailable"];
|
|
2044
|
+
if (!Array.isArray(sessions) || !isWorkShape(work)) return ["runner-work-observation-unavailable"];
|
|
2045
|
+
const { codingTurns, localRuns, detail } = work;
|
|
1948
2046
|
const activeCoding = sessions.flatMap((session) => {
|
|
1949
2047
|
if (session?.alive === false) return [];
|
|
1950
2048
|
const id = typeof session?.sessionId === "string" && session.sessionId ? session.sessionId : "unidentified-session";
|
|
@@ -1960,6 +2058,155 @@ function observedRunnerWork(observation) {
|
|
|
1960
2058
|
...Array.from({ length: localRuns }, (_v, i) => localDetail[i] || `local-run-${i + 1}`)
|
|
1961
2059
|
];
|
|
1962
2060
|
}
|
|
2061
|
+
function readWorkLook(sessions, health, sampledAt) {
|
|
2062
|
+
const sessionsOk = sessions.status === "fulfilled" && Array.isArray(sessions.value?.sessions);
|
|
2063
|
+
const healthOk = health.status === "fulfilled" && isWorkShape(health.value?.work);
|
|
2064
|
+
const busy = sessions.status === "fulfilled" && health.status === "fulfilled" ? observedRunnerWork({ sessions: sessions.value?.sessions, work: health.value?.work }) : ["runner-work-observation-unavailable"];
|
|
2065
|
+
return {
|
|
2066
|
+
busy,
|
|
2067
|
+
observations: [
|
|
2068
|
+
{ source: "sessions", success: sessionsOk, sampledAt, ...sessions.status === "fulfilled" && !sessionsOk ? { malformed: true } : {} },
|
|
2069
|
+
{ source: "health", success: healthOk, sampledAt, ...health.status === "fulfilled" && !healthOk ? { malformed: true } : {} }
|
|
2070
|
+
]
|
|
2071
|
+
};
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
// src/commands/runner/update-bridge.ts
|
|
2075
|
+
var PolicyCell = class {
|
|
2076
|
+
constructor(policy, persist, enabled = true) {
|
|
2077
|
+
this.policy = policy;
|
|
2078
|
+
this.persist = persist;
|
|
2079
|
+
this.enabled = enabled;
|
|
2080
|
+
}
|
|
2081
|
+
policy;
|
|
2082
|
+
persist;
|
|
2083
|
+
enabled;
|
|
2084
|
+
authoritative = false;
|
|
2085
|
+
refreshing = false;
|
|
2086
|
+
fence = new ResponseFence();
|
|
2087
|
+
/** The status writer every update path uses. */
|
|
2088
|
+
record(status, extra = {}) {
|
|
2089
|
+
this.policy = { ...this.policy, ...extra, status, lastAttemptAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
2090
|
+
if (this.enabled) this.persist(this.policy);
|
|
2091
|
+
}
|
|
2092
|
+
/** The number a registration/heartbeat takes as it is SENT. */
|
|
2093
|
+
next() {
|
|
2094
|
+
return this.fence.next();
|
|
2095
|
+
}
|
|
2096
|
+
/**
|
|
2097
|
+
* Fold one service answer in. It is VALIDATED before it is ordered (#1020): an answer that carries no readable
|
|
2098
|
+
* policy must not advance the fence, or it would suppress a valid older OFF still in flight. The owner's OFF ends
|
|
2099
|
+
* any recovery cycle.
|
|
2100
|
+
*/
|
|
2101
|
+
accept(response, sentAs) {
|
|
2102
|
+
if (!this.enabled) return null;
|
|
2103
|
+
const accepted = acceptPolicyResponse(this.policy, this.authoritative, response);
|
|
2104
|
+
if (!accepted || !this.fence.accepts(sentAs)) return null;
|
|
2105
|
+
this.policy = accepted.action === "cancel" ? { ...accepted.policy, recovery: void 0 } : accepted.policy;
|
|
2106
|
+
this.authoritative = true;
|
|
2107
|
+
this.persist(this.policy);
|
|
2108
|
+
return accepted.action;
|
|
2109
|
+
}
|
|
2110
|
+
};
|
|
2111
|
+
function createPolicyChannel(deps) {
|
|
2112
|
+
const registered = /* @__PURE__ */ new Set();
|
|
2113
|
+
const act = (action) => {
|
|
2114
|
+
if (action === "cancel") deps.onAction("cancel");
|
|
2115
|
+
else if (action === "start" && !deps.cell.refreshing) deps.onAction("start");
|
|
2116
|
+
};
|
|
2117
|
+
const accept = (response, sentAs) => act(deps.cell.accept(response, sentAs));
|
|
2118
|
+
const register = async (id, force = deps.force, signal) => {
|
|
2119
|
+
const sent = deps.cell.next();
|
|
2120
|
+
try {
|
|
2121
|
+
const result = await deps.post(`/v1/instances/${apiPathSegment(id)}/runtime`, deps.body(id, force), signal);
|
|
2122
|
+
if (signal?.aborted) return false;
|
|
2123
|
+
accept(result, sent);
|
|
2124
|
+
registered.add(id);
|
|
2125
|
+
return true;
|
|
2126
|
+
} catch (e) {
|
|
2127
|
+
if (signal?.aborted) return false;
|
|
2128
|
+
registered.delete(id);
|
|
2129
|
+
deps.onError(id, e instanceof Error ? e.message : String(e));
|
|
2130
|
+
return false;
|
|
2131
|
+
}
|
|
2132
|
+
};
|
|
2133
|
+
const refresh = async (signal) => {
|
|
2134
|
+
const id = registered.values().next().value ?? [...deps.held()][0];
|
|
2135
|
+
if (!id) return false;
|
|
2136
|
+
deps.cell.authoritative = false;
|
|
2137
|
+
deps.cell.refreshing = true;
|
|
2138
|
+
try {
|
|
2139
|
+
return await register(id, deps.force, signal) && deps.cell.authoritative;
|
|
2140
|
+
} finally {
|
|
2141
|
+
deps.cell.refreshing = false;
|
|
2142
|
+
}
|
|
2143
|
+
};
|
|
2144
|
+
return { registered, register, refresh, accept };
|
|
2145
|
+
}
|
|
2146
|
+
var BoundExpired = class extends Error {
|
|
2147
|
+
boundExpired = true;
|
|
2148
|
+
};
|
|
2149
|
+
function createInstallBridge(deps) {
|
|
2150
|
+
let inFlight = false;
|
|
2151
|
+
let installedPendingRestart = null;
|
|
2152
|
+
const within = (work) => boundedWork(work, deps.bound);
|
|
2153
|
+
const release = (drained) => {
|
|
2154
|
+
if (drained) deps.admission.resume();
|
|
2155
|
+
inFlight = false;
|
|
2156
|
+
};
|
|
2157
|
+
const expired = (drained, what) => {
|
|
2158
|
+
release(drained);
|
|
2159
|
+
throw new BoundExpired(`${what} did not answer within ${(deps.bound?.ms ?? 3e4) / 1e3}s; nothing restarted, and it will be retried.`);
|
|
2160
|
+
};
|
|
2161
|
+
const run2 = async (plan, automatic = false, lease) => {
|
|
2162
|
+
if (inFlight || lease && !deps.leaseActive(lease)) return false;
|
|
2163
|
+
inFlight = true;
|
|
2164
|
+
if (automatic) {
|
|
2165
|
+
const fresh = await within((signal) => deps.refresh(signal));
|
|
2166
|
+
if (fresh === null) expired(false, "The owner policy refresh before installing");
|
|
2167
|
+
if (!fresh || !deps.cell.policy.autoUpdate) {
|
|
2168
|
+
release(false);
|
|
2169
|
+
return false;
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
if (installedPendingRestart !== plan.latest) {
|
|
2173
|
+
deps.log(`Updating ${plan.current} \u2192 ${plan.latest} (runner_update)\u2026`);
|
|
2174
|
+
try {
|
|
2175
|
+
await deps.install(plan.latest);
|
|
2176
|
+
installedPendingRestart = plan.latest;
|
|
2177
|
+
} catch (error) {
|
|
2178
|
+
release(false);
|
|
2179
|
+
throw error;
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
await deps.admission.drain();
|
|
2183
|
+
await (deps.settle ?? (() => new Promise((resolve5) => setTimeout(resolve5, 500))))();
|
|
2184
|
+
if (lease && !deps.leaseActive(lease)) {
|
|
2185
|
+
release(true);
|
|
2186
|
+
return false;
|
|
2187
|
+
}
|
|
2188
|
+
if (automatic) {
|
|
2189
|
+
const fresh = await within((signal) => deps.refresh(signal));
|
|
2190
|
+
if (fresh === null) expired(true, "The owner policy refresh after installing");
|
|
2191
|
+
if (!fresh || !deps.cell.policy.autoUpdate) {
|
|
2192
|
+
release(true);
|
|
2193
|
+
return false;
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
const final = await within((signal) => deps.facts(signal));
|
|
2197
|
+
if (final === null) expired(true, "The final local work look before restarting");
|
|
2198
|
+
const facts = final;
|
|
2199
|
+
const workObserved = !facts.busy.includes("runner-work-observation-unavailable");
|
|
2200
|
+
if (facts.busy.length || automatic && !mayAutomaticallyRestart({ authoritative: deps.cell.authoritative, enabled: deps.cell.policy.autoUpdate, workObserved, busy: facts.busy })) {
|
|
2201
|
+
release(true);
|
|
2202
|
+
return false;
|
|
2203
|
+
}
|
|
2204
|
+
deps.log(`Installed ${plan.latest} \u2014 restarting; every agent re-attaches on the way back up.`);
|
|
2205
|
+
deps.detachAll();
|
|
2206
|
+
return true;
|
|
2207
|
+
};
|
|
2208
|
+
return { run: run2, inFlight: () => inFlight, installedPendingRestart: () => installedPendingRestart };
|
|
2209
|
+
}
|
|
1963
2210
|
|
|
1964
2211
|
// src/commands/runner/relay.ts
|
|
1965
2212
|
var MEMBERSHIP_SYNC_PATH = "/pags/membership/sync";
|
|
@@ -1971,81 +2218,43 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
|
|
|
1971
2218
|
const runnerNode = hostname3();
|
|
1972
2219
|
const machine = loadMachineIdentity(runnerNode);
|
|
1973
2220
|
const autoUpdateKey = machine.id ? autoUpdatePolicyKey(machine.id) : "";
|
|
1974
|
-
|
|
1975
|
-
let autoUpdatePolicyAuthoritative = false;
|
|
1976
|
-
let refreshingAutomaticPolicy = false;
|
|
2221
|
+
const cell = new PolicyCell(autoUpdateKey ? loadAutoUpdatePolicy(autoUpdateKey) ?? { autoUpdate: false } : { autoUpdate: false }, (p) => saveAutoUpdatePolicy(autoUpdateKey, p), !!autoUpdateKey);
|
|
1977
2222
|
let scheduleAutomaticUpdate = () => void 0;
|
|
1978
2223
|
let autoUpdateDiagnostics = () => ({});
|
|
1979
|
-
const
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
};
|
|
1986
|
-
if (autoUpdateKey) saveAutoUpdatePolicy(autoUpdateKey, autoUpdatePolicy);
|
|
1987
|
-
};
|
|
1988
|
-
const acceptAutoUpdatePolicy = (response) => {
|
|
1989
|
-
if (!autoUpdateKey) return;
|
|
1990
|
-
const accepted = acceptPolicyResponse(autoUpdatePolicy, autoUpdatePolicyAuthoritative, response);
|
|
1991
|
-
if (!accepted) return;
|
|
1992
|
-
autoUpdatePolicy = accepted.policy;
|
|
1993
|
-
autoUpdatePolicyAuthoritative = true;
|
|
1994
|
-
if (autoUpdateKey) saveAutoUpdatePolicy(autoUpdateKey, autoUpdatePolicy);
|
|
1995
|
-
if (accepted.action === "cancel") {
|
|
1996
|
-
scheduleAutomaticUpdate(false, true);
|
|
1997
|
-
return;
|
|
1998
|
-
}
|
|
1999
|
-
if (refreshingAutomaticPolicy) return;
|
|
2000
|
-
if (accepted.action === "start") {
|
|
2001
|
-
scheduleAutomaticUpdate(true);
|
|
2002
|
-
}
|
|
2003
|
-
};
|
|
2224
|
+
const autoUpdateStatusBody = () => ({
|
|
2225
|
+
status: autoUpdateStatusWire(cell.policy.status),
|
|
2226
|
+
latestVersion: cell.policy.latestVersion,
|
|
2227
|
+
error: cell.policy.reason,
|
|
2228
|
+
...autoUpdateDiagnostics()
|
|
2229
|
+
});
|
|
2004
2230
|
const capabilities = await requestRunner("GET", "/capabilities", { url: localUrl, token: runnerToken, instanceId: instanceIds[0] });
|
|
2005
2231
|
const caps = Array.isArray(capabilities.capabilities) ? capabilities.capabilities.filter((item) => typeof item === "string") : [];
|
|
2006
|
-
const registered = /* @__PURE__ */ new Set();
|
|
2007
2232
|
let lastRegisterError = "";
|
|
2008
|
-
const
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
});
|
|
2027
|
-
acceptAutoUpdatePolicy(result);
|
|
2028
|
-
registered.add(id);
|
|
2029
|
-
return true;
|
|
2030
|
-
} catch (e) {
|
|
2031
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
2032
|
-
registered.delete(id);
|
|
2233
|
+
const channel = createPolicyChannel({
|
|
2234
|
+
cell,
|
|
2235
|
+
post: (path, body, signal) => requestPags("POST", path, opts, body, signal),
|
|
2236
|
+
body: (_id, forceClaim) => ({
|
|
2237
|
+
endpointUrl: localUrl,
|
|
2238
|
+
token: runnerToken,
|
|
2239
|
+
placement: "local",
|
|
2240
|
+
capabilities: caps,
|
|
2241
|
+
runnerVersion: CLI_VERSION,
|
|
2242
|
+
runnerNode,
|
|
2243
|
+
machineId: machine.id,
|
|
2244
|
+
machineNames: machine.names,
|
|
2245
|
+
force: forceClaim,
|
|
2246
|
+
autoUpdateStatus: autoUpdateStatusBody()
|
|
2247
|
+
}),
|
|
2248
|
+
force,
|
|
2249
|
+
held: () => attached.keys(),
|
|
2250
|
+
onError: (id, msg) => {
|
|
2033
2251
|
lastRegisterError = msg;
|
|
2034
2252
|
writeError(`register ${id.slice(0, 8)}\u2026 failed: ${msg}`);
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
};
|
|
2038
|
-
const
|
|
2039
|
-
|
|
2040
|
-
if (!id) return false;
|
|
2041
|
-
autoUpdatePolicyAuthoritative = false;
|
|
2042
|
-
refreshingAutomaticPolicy = true;
|
|
2043
|
-
try {
|
|
2044
|
-
return await registerRuntime(id) && autoUpdatePolicyAuthoritative;
|
|
2045
|
-
} finally {
|
|
2046
|
-
refreshingAutomaticPolicy = false;
|
|
2047
|
-
}
|
|
2048
|
-
};
|
|
2253
|
+
},
|
|
2254
|
+
onAction: (action) => scheduleAutomaticUpdate(action === "start", action === "cancel")
|
|
2255
|
+
});
|
|
2256
|
+
const registered = channel.registered;
|
|
2257
|
+
const registerRuntime = (id, forceClaim = force) => channel.register(id, forceClaim);
|
|
2049
2258
|
const attached = /* @__PURE__ */ new Map();
|
|
2050
2259
|
const processStartedAt = Date.now();
|
|
2051
2260
|
const starts24h = recordRunnerStart(processStartedAt);
|
|
@@ -2058,26 +2267,20 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
|
|
|
2058
2267
|
writeLine(formatStatusLine({ registration: state, agents, reason: state === "ok" ? void 0 : lastRegisterError }));
|
|
2059
2268
|
};
|
|
2060
2269
|
for (const id of instanceIds) await registerRuntime(id);
|
|
2061
|
-
const updateFacts = async () => {
|
|
2270
|
+
const updateFacts = async (signal) => {
|
|
2062
2271
|
const [sessionsResult, healthResult] = await Promise.allSettled([
|
|
2063
2272
|
requestRunner("GET", "/coding/sessions", {
|
|
2064
2273
|
url: localUrl,
|
|
2065
2274
|
token: runnerToken,
|
|
2066
2275
|
instanceId: instanceIds[0]
|
|
2067
|
-
}),
|
|
2068
|
-
requestRunner("GET", "/health", { url: localUrl, token: runnerToken, instanceId: instanceIds[0] })
|
|
2276
|
+
}, void 0, signal),
|
|
2277
|
+
requestRunner("GET", "/health", { url: localUrl, token: runnerToken, instanceId: instanceIds[0] }, void 0, signal)
|
|
2069
2278
|
]);
|
|
2070
|
-
const busy
|
|
2071
|
-
const sampledAt = Date.now();
|
|
2072
|
-
const observations = [
|
|
2073
|
-
// An answer is a success only when it has the shape the guard reads; malformed is a failed look.
|
|
2074
|
-
{ source: "sessions", success: sessionsResult.status === "fulfilled" && Array.isArray(sessionsResult.value.sessions), sampledAt },
|
|
2075
|
-
{ source: "health", success: healthResult.status === "fulfilled" && !!healthResult.value.work && typeof healthResult.value.work === "object", sampledAt }
|
|
2076
|
-
];
|
|
2279
|
+
const { busy, observations } = readWorkLook(sessionsResult, healthResult, Date.now());
|
|
2077
2280
|
return {
|
|
2078
2281
|
observations,
|
|
2079
2282
|
current: CLI_VERSION,
|
|
2080
|
-
latest: await latestPublishedVersion(),
|
|
2283
|
+
latest: await latestPublishedVersion(void 0, signal),
|
|
2081
2284
|
fromSource: runsFromSource(),
|
|
2082
2285
|
restarter: restarterFrom(process.env),
|
|
2083
2286
|
// A restart ends browser/application work too. Give these durable names to the same
|
|
@@ -2085,94 +2288,47 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
|
|
|
2085
2288
|
busy
|
|
2086
2289
|
};
|
|
2087
2290
|
};
|
|
2088
|
-
let updateInstallInFlight = false;
|
|
2089
|
-
let installedPendingRestart = null;
|
|
2090
2291
|
const relayMutationAdmission = new RelayMutationAdmission();
|
|
2091
|
-
const
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
} catch (error) {
|
|
2104
|
-
updateInstallInFlight = false;
|
|
2105
|
-
throw error;
|
|
2106
|
-
}
|
|
2107
|
-
}
|
|
2108
|
-
await relayMutationAdmission.drain();
|
|
2109
|
-
await new Promise((resolve5) => setTimeout(resolve5, 500));
|
|
2110
|
-
if (lease && !deferredUpdateLeaseActive(lease)) {
|
|
2111
|
-
relayMutationAdmission.resume();
|
|
2112
|
-
updateInstallInFlight = false;
|
|
2113
|
-
return false;
|
|
2114
|
-
}
|
|
2115
|
-
if (automatic) {
|
|
2116
|
-
if (!await refreshAutomaticPolicy() || !autoUpdatePolicy.autoUpdate) {
|
|
2117
|
-
relayMutationAdmission.resume();
|
|
2118
|
-
updateInstallInFlight = false;
|
|
2119
|
-
return false;
|
|
2120
|
-
}
|
|
2121
|
-
}
|
|
2122
|
-
const finalFacts = await updateFacts();
|
|
2123
|
-
if (finalFacts.busy.length || automatic && !mayAutomaticallyRestart({ authoritative: autoUpdatePolicyAuthoritative, enabled: autoUpdatePolicy.autoUpdate, workObserved: !finalFacts.busy.includes("runner-work-observation-unavailable"), busy: finalFacts.busy })) {
|
|
2124
|
-
relayMutationAdmission.resume();
|
|
2125
|
-
updateInstallInFlight = false;
|
|
2126
|
-
return false;
|
|
2127
|
-
}
|
|
2128
|
-
writeLine(`Installed ${plan.latest} \u2014 restarting; every agent re-attaches on the way back up.`);
|
|
2129
|
-
for (const id of [...attached.keys()]) detach(id);
|
|
2130
|
-
return true;
|
|
2131
|
-
};
|
|
2292
|
+
const bridge = createInstallBridge({
|
|
2293
|
+
cell,
|
|
2294
|
+
refresh: channel.refresh,
|
|
2295
|
+
facts: updateFacts,
|
|
2296
|
+
install: installVersion,
|
|
2297
|
+
admission: relayMutationAdmission,
|
|
2298
|
+
leaseActive: deferredUpdateLeaseActive,
|
|
2299
|
+
detachAll: () => {
|
|
2300
|
+
for (const id of [...attached.keys()]) detach(id);
|
|
2301
|
+
},
|
|
2302
|
+
log: writeLine
|
|
2303
|
+
});
|
|
2132
2304
|
let updateWaiting = false;
|
|
2133
2305
|
let deferredManualLease = null;
|
|
2134
|
-
const updateWhenIdle = (
|
|
2135
|
-
if (updateWaiting
|
|
2306
|
+
const updateWhenIdle = (lease) => {
|
|
2307
|
+
if (updateWaiting) return false;
|
|
2136
2308
|
updateWaiting = true;
|
|
2137
|
-
|
|
2138
|
-
const until = automatic ? Date.now() + 60 * 6e4 : lease.expiresAt;
|
|
2309
|
+
deferredManualLease = lease;
|
|
2139
2310
|
const tick = () => setTimeout(async () => {
|
|
2140
|
-
if (!
|
|
2311
|
+
if (!deferredManualLease || !deferredUpdateLeaseActive(deferredManualLease)) {
|
|
2141
2312
|
writeLine("runner_update: the deferred operation lease expired; no delayed restart was performed.");
|
|
2142
2313
|
deferredManualLease = null;
|
|
2143
2314
|
updateWaiting = false;
|
|
2144
2315
|
return;
|
|
2145
2316
|
}
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
}
|
|
2151
|
-
const plan = planRunnerUpdate(await updateFacts());
|
|
2152
|
-
if (plan.action === "update") {
|
|
2153
|
-
if (automatic && !autoUpdatePolicy.autoUpdate) {
|
|
2154
|
-
updateWaiting = false;
|
|
2155
|
-
return;
|
|
2156
|
-
}
|
|
2157
|
-
if (automatic) saveAutoUpdateStatus("running", { latestVersion: plan.latest, reason: void 0 });
|
|
2158
|
-
const installed = await installAndRestart(plan, automatic, automatic ? void 0 : deferredManualLease ?? void 0).catch((e) => {
|
|
2159
|
-
if (automatic) saveAutoUpdateStatus("failure", { reason: e instanceof Error ? e.message : String(e) });
|
|
2317
|
+
const facts = await boundedWork((signal) => updateFacts(signal));
|
|
2318
|
+
const plan = facts ? planRunnerUpdate(facts) : null;
|
|
2319
|
+
if (plan?.action === "update") {
|
|
2320
|
+
const installed = await bridge.run(plan, false, deferredManualLease).catch((e) => {
|
|
2160
2321
|
writeError(`runner_update: install failed \u2014 ${e instanceof Error ? e.message : String(e)}`);
|
|
2161
2322
|
return false;
|
|
2162
2323
|
});
|
|
2163
|
-
if (!installed && Date.now() <
|
|
2164
|
-
|
|
2165
|
-
tick();
|
|
2166
|
-
} else {
|
|
2324
|
+
if (!installed && Date.now() < lease.expiresAt) tick();
|
|
2325
|
+
else {
|
|
2167
2326
|
deferredManualLease = null;
|
|
2168
2327
|
updateWaiting = false;
|
|
2169
2328
|
if (installed) leaveForRestart(plan.restarter);
|
|
2170
2329
|
}
|
|
2171
|
-
} else if (plan.action === "wait" && Date.now() <
|
|
2172
|
-
|
|
2173
|
-
tick();
|
|
2174
|
-
} else {
|
|
2175
|
-
if (automatic && plan.action === "refused") saveAutoUpdateStatus("unsupported", { reason: plan.reason });
|
|
2330
|
+
} else if ((!plan || plan.action === "wait") && Date.now() < lease.expiresAt) tick();
|
|
2331
|
+
else {
|
|
2176
2332
|
deferredManualLease = null;
|
|
2177
2333
|
updateWaiting = false;
|
|
2178
2334
|
}
|
|
@@ -2180,107 +2336,41 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
|
|
|
2180
2336
|
tick();
|
|
2181
2337
|
return true;
|
|
2182
2338
|
};
|
|
2183
|
-
let automaticTimer = null;
|
|
2184
|
-
let automaticChecking = false;
|
|
2185
|
-
let automaticFailures = 0;
|
|
2186
|
-
const runAutomaticUpdateCheck = async () => {
|
|
2187
|
-
if (!autoUpdatePolicy.autoUpdate || !autoUpdatePolicyAuthoritative || automaticChecking || updateWaiting) return;
|
|
2188
|
-
automaticChecking = true;
|
|
2189
|
-
try {
|
|
2190
|
-
if (!await refreshAutomaticPolicy() || !autoUpdatePolicy.autoUpdate) {
|
|
2191
|
-
saveAutoUpdateStatus("offline", { reason: "The owner policy could not be refreshed; automatic update is deferred." });
|
|
2192
|
-
scheduleAutomaticUpdate();
|
|
2193
|
-
return;
|
|
2194
|
-
}
|
|
2195
|
-
saveAutoUpdateStatus("checking", { reason: void 0 });
|
|
2196
|
-
const facts = await updateFacts();
|
|
2197
|
-
if (!autoUpdatePolicy.autoUpdate || !autoUpdatePolicyAuthoritative) return;
|
|
2198
|
-
if (!facts.latest) {
|
|
2199
|
-
automaticFailures++;
|
|
2200
|
-
saveAutoUpdateStatus("failure", { reason: "Could not check the trusted npm registry for a newer CLI release." });
|
|
2201
|
-
scheduleAutomaticUpdate();
|
|
2202
|
-
return;
|
|
2203
|
-
}
|
|
2204
|
-
automaticFailures = 0;
|
|
2205
|
-
const plan = planRunnerUpdate(facts);
|
|
2206
|
-
if (plan.action === "up-to-date") {
|
|
2207
|
-
saveAutoUpdateStatus("verified-success", { latestVersion: facts.latest, reason: void 0 });
|
|
2208
|
-
scheduleAutomaticUpdate();
|
|
2209
|
-
return;
|
|
2210
|
-
}
|
|
2211
|
-
if (plan.action === "refused") {
|
|
2212
|
-
saveAutoUpdateStatus("unsupported", { latestVersion: facts.latest, reason: plan.reason });
|
|
2213
|
-
scheduleAutomaticUpdate();
|
|
2214
|
-
return;
|
|
2215
|
-
}
|
|
2216
|
-
if (plan.action === "wait") {
|
|
2217
|
-
saveAutoUpdateStatus("waiting-for-idle", { latestVersion: plan.latest, reason: void 0 });
|
|
2218
|
-
updateWhenIdle(true);
|
|
2219
|
-
return;
|
|
2220
|
-
}
|
|
2221
|
-
if (!autoUpdatePolicy.autoUpdate || !autoUpdatePolicyAuthoritative) return;
|
|
2222
|
-
saveAutoUpdateStatus("running", { latestVersion: plan.latest, reason: void 0 });
|
|
2223
|
-
try {
|
|
2224
|
-
const installed = await installAndRestart(plan, true);
|
|
2225
|
-
if (!installed) {
|
|
2226
|
-
saveAutoUpdateStatus("waiting-for-idle", { latestVersion: plan.latest });
|
|
2227
|
-
updateWhenIdle(true);
|
|
2228
|
-
return;
|
|
2229
|
-
}
|
|
2230
|
-
saveAutoUpdateStatus("restarting", { latestVersion: plan.latest, reason: void 0 });
|
|
2231
|
-
} catch (e) {
|
|
2232
|
-
automaticFailures++;
|
|
2233
|
-
saveAutoUpdateStatus("failure", { latestVersion: plan.latest, reason: e instanceof Error ? e.message : String(e) });
|
|
2234
|
-
scheduleAutomaticUpdate();
|
|
2235
|
-
}
|
|
2236
|
-
} finally {
|
|
2237
|
-
automaticChecking = false;
|
|
2238
|
-
}
|
|
2239
|
-
};
|
|
2240
|
-
scheduleAutomaticUpdate = (initial = false, cancel = false) => {
|
|
2241
|
-
if (cancel || !autoUpdatePolicy.autoUpdate || !autoUpdatePolicyAuthoritative) {
|
|
2242
|
-
if (automaticTimer) clearTimeout(automaticTimer);
|
|
2243
|
-
automaticTimer = null;
|
|
2244
|
-
return;
|
|
2245
|
-
}
|
|
2246
|
-
if (automaticTimer || updateWaiting) return;
|
|
2247
|
-
const delay = initial ? Math.round(5e3 + Math.random() * 25e3) : nextAutoUpdateDelayMs(automaticFailures);
|
|
2248
|
-
automaticTimer = setTimeout(() => {
|
|
2249
|
-
automaticTimer = null;
|
|
2250
|
-
void runAutomaticUpdateCheck();
|
|
2251
|
-
}, delay);
|
|
2252
|
-
automaticTimer.unref();
|
|
2253
|
-
};
|
|
2254
2339
|
const automaticUpdateController = new AutomaticUpdateController({
|
|
2255
|
-
policy: () => ({ value:
|
|
2256
|
-
refreshPolicy:
|
|
2340
|
+
policy: () => ({ value: cell.policy, authoritative: cell.authoritative }),
|
|
2341
|
+
refreshPolicy: channel.refresh,
|
|
2257
2342
|
facts: updateFacts,
|
|
2258
2343
|
plan: planRunnerUpdate,
|
|
2259
|
-
//
|
|
2260
|
-
//
|
|
2344
|
+
// The same bridge an explicit runner_update uses: npm, a fresh authoritative policy, the mutation drain, the
|
|
2345
|
+
// final local-work observation and restart — each wait bounded.
|
|
2261
2346
|
installAndRestart: async (plan, automatic) => {
|
|
2262
|
-
const installed = await
|
|
2347
|
+
const installed = await bridge.run(plan, automatic);
|
|
2263
2348
|
if (installed) setTimeout(() => leaveForRestart(plan.restarter), 0).unref();
|
|
2264
2349
|
return installed;
|
|
2265
2350
|
},
|
|
2266
|
-
status:
|
|
2351
|
+
status: (status, extra) => cell.record(status, extra),
|
|
2352
|
+
// The agents held now: attached AND registered — recorded with an install, required back after it.
|
|
2353
|
+
attachments: () => [...attached.keys()].filter((id) => registered.has(id))
|
|
2267
2354
|
});
|
|
2268
2355
|
autoUpdateDiagnostics = () => automaticUpdateController.diagnostics();
|
|
2269
2356
|
scheduleAutomaticUpdate = (initial = false, cancel = false) => automaticUpdateController.onPolicy(cancel ? "cancel" : initial ? "start" : "keep");
|
|
2270
|
-
if (
|
|
2357
|
+
if (cell.authoritative && cell.policy.autoUpdate) scheduleAutomaticUpdate(true);
|
|
2271
2358
|
const answerUpdate = async (body) => {
|
|
2272
2359
|
const dryRun = body?.dryRun === true;
|
|
2273
|
-
const
|
|
2360
|
+
const facts = await boundedWork((signal) => updateFacts(signal));
|
|
2361
|
+
if (!facts) return { status: 504, result: { error: "The local work and registry look did not answer within 30s; nothing was installed or restarted." } };
|
|
2362
|
+
const plan = planRunnerUpdate(facts);
|
|
2274
2363
|
if (dryRun || plan.action === "up-to-date" || plan.action === "refused") return { status: 200, result: { ...plan, dryRun } };
|
|
2275
2364
|
if (plan.action === "wait") {
|
|
2276
2365
|
const lease = deferredUpdateLease(body);
|
|
2277
2366
|
if (!lease) return { status: 400, result: { error: "A busy runner_update needs a current operation lease; no delayed restart was scheduled." } };
|
|
2278
|
-
if (!updateWhenIdle(
|
|
2367
|
+
if (!updateWhenIdle(lease)) return { status: 409, result: { error: "A runner update is already waiting for active work to finish." } };
|
|
2279
2368
|
return { status: 200, result: { ...plan, operationId: lease.operationId, deferredUntil: lease.expiresAt, detail: "This operation may restart only before its recorded lease expires; no delayed restart survives an expired or terminal operation." } };
|
|
2280
2369
|
}
|
|
2281
2370
|
try {
|
|
2282
|
-
if (!await
|
|
2371
|
+
if (!await bridge.run(plan)) return { status: 409, result: { error: "A runner update is already installing or restarting." } };
|
|
2283
2372
|
} catch (e) {
|
|
2373
|
+
if (e instanceof BoundExpired) return { status: 504, result: { error: e.message } };
|
|
2284
2374
|
return { status: 500, result: { error: `npm could not install ${plan.latest}: ${e instanceof Error ? e.message : String(e)}` } };
|
|
2285
2375
|
}
|
|
2286
2376
|
const supervisor = supervisorNote(plan.restarter);
|
|
@@ -2394,18 +2484,14 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
|
|
|
2394
2484
|
});
|
|
2395
2485
|
for (const id of [...attached.keys()]) {
|
|
2396
2486
|
try {
|
|
2487
|
+
const sent = cell.next();
|
|
2397
2488
|
const response = await requestPags("POST", `/v1/instances/${apiPathSegment(id)}/runtime/heartbeat`, opts, {
|
|
2398
2489
|
runnerNode,
|
|
2399
2490
|
machineId: machine.id,
|
|
2400
2491
|
resources,
|
|
2401
|
-
autoUpdateStatus:
|
|
2402
|
-
status: autoUpdateStatusWire(autoUpdatePolicy.status),
|
|
2403
|
-
latestVersion: autoUpdatePolicy.latestVersion,
|
|
2404
|
-
error: autoUpdatePolicy.reason,
|
|
2405
|
-
...autoUpdateDiagnostics()
|
|
2406
|
-
}
|
|
2492
|
+
autoUpdateStatus: autoUpdateStatusBody()
|
|
2407
2493
|
});
|
|
2408
|
-
|
|
2494
|
+
channel.accept(response, sent);
|
|
2409
2495
|
} catch (e) {
|
|
2410
2496
|
failure = e instanceof Error ? e.message : String(e);
|
|
2411
2497
|
}
|