@proagentstore/cli 0.4.109 → 0.4.111

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.
Files changed (2) hide show
  1. package/dist/index.js +305 -282
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -689,16 +689,16 @@ function describeCandidate(candidate, nowMs) {
689
689
  const agents = `${candidate.agentCount} agent${candidate.agentCount === 1 ? "" : "s"}`;
690
690
  return `${relativeAge(candidate.lastSeenAt, nowMs)} \xB7 ${agents}`;
691
691
  }
692
- function parseSelection(input, count) {
692
+ function parseSelection(input, count2) {
693
693
  const raw = input.trim().toLowerCase();
694
694
  if (!raw || raw === "n" || raw === "no" || raw === "s" || raw === "skip" || raw === "none") return { kind: "skip" };
695
- if (raw === "a" || raw === "all") return { kind: "pick", indices: Array.from({ length: count }, (_, i) => i) };
695
+ if (raw === "a" || raw === "all") return { kind: "pick", indices: Array.from({ length: count2 }, (_, i) => i) };
696
696
  const parts = raw.split(/[\s,]+/).filter(Boolean);
697
697
  const indices = [];
698
698
  for (const part of parts) {
699
699
  if (!/^\d+$/.test(part)) return { kind: "invalid", message: `"${part}" is not a number.` };
700
700
  const n = Number.parseInt(part, 10);
701
- if (n < 1 || n > count) return { kind: "invalid", message: `${n} is not on the list (1\u2013${count}).` };
701
+ if (n < 1 || n > count2) return { kind: "invalid", message: `${n} is not on the list (1\u2013${count2}).` };
702
702
  if (!indices.includes(n - 1)) indices.push(n - 1);
703
703
  }
704
704
  return indices.length ? { kind: "pick", indices } : { kind: "skip" };
@@ -1632,6 +1632,13 @@ function autoUpdateStatusWire(status) {
1632
1632
  failure: "failed"
1633
1633
  }[status] ?? status;
1634
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
+ const verifiedAt = typeof r.verifiedAt === "string" && r.verifiedAt ? r.verifiedAt : void 0;
1640
+ return { target: r.target, attachments: [...r.attachments], at: r.at, ...verifiedAt ? { verifiedAt } : {} };
1641
+ }
1635
1642
  var statuses = /* @__PURE__ */ new Set([
1636
1643
  "enabled",
1637
1644
  "checking",
@@ -1668,12 +1675,14 @@ function parseAutoUpdatePolicy(value) {
1668
1675
  const latestVersion = raw.latestVersion ?? raw.latest_version;
1669
1676
  const lastAttemptAt = raw.lastAttemptAt ?? raw.last_attempt_at;
1670
1677
  const reason = raw.reason;
1678
+ const recovery = parseRecovery(raw.recovery);
1671
1679
  return {
1672
1680
  autoUpdate,
1673
1681
  ...status ? { status } : {},
1674
1682
  ...typeof latestVersion === "string" ? { latestVersion } : {},
1675
1683
  ...typeof lastAttemptAt === "string" ? { lastAttemptAt } : {},
1676
- ...typeof reason === "string" ? { reason } : {}
1684
+ ...typeof reason === "string" ? { reason } : {},
1685
+ ...recovery ? { recovery } : {}
1677
1686
  };
1678
1687
  }
1679
1688
  function policyFromResponse(value) {
@@ -1729,8 +1738,9 @@ function policyScheduleAction(previous, next, wasAuthoritative) {
1729
1738
  return "keep";
1730
1739
  }
1731
1740
  function acceptPolicyResponse(local, wasAuthoritative, response) {
1732
- const remote = policyFromResponse(response);
1733
- if (!remote) return null;
1741
+ const parsed = policyFromResponse(response);
1742
+ if (!parsed) return null;
1743
+ const { recovery: _fromService, ...remote } = parsed;
1734
1744
  const action = policyScheduleAction(local, remote, wasAuthoritative);
1735
1745
  const ownerTransition = action === "cancel" || !local.autoUpdate;
1736
1746
  return { policy: ownerTransition ? { ...local, ...remote } : { ...local, autoUpdate: remote.autoUpdate }, action };
@@ -1755,18 +1765,48 @@ function mayAutomaticallyRestart(input) {
1755
1765
  var MAX_OBSERVATIONS = 5;
1756
1766
  var IDLE_RETRY_MS = 15e3;
1757
1767
  var OBSERVATION_TIMEOUT_MS = 3e4;
1758
- function updateVerdict(attempt, facts) {
1759
- const target = attempt.status === "running" || attempt.status === "restarting" ? attempt.latestVersion : void 0;
1768
+ async function boundedWork(work, bound = {}) {
1769
+ const abort = new AbortController();
1770
+ let timer;
1771
+ const expired = new Promise((resolve5) => {
1772
+ timer = (bound.timer ?? ((fn, ms) => setTimeout(fn, ms)))(() => {
1773
+ abort.abort();
1774
+ resolve5(null);
1775
+ }, bound.ms ?? OBSERVATION_TIMEOUT_MS);
1776
+ timer.unref?.();
1777
+ });
1778
+ const running = work(abort.signal);
1779
+ running.catch(() => void 0);
1780
+ try {
1781
+ return await Promise.race([running, expired]);
1782
+ } finally {
1783
+ if (timer) (bound.clear ?? ((t) => clearTimeout(t)))(timer);
1784
+ }
1785
+ }
1786
+ function updateVerdict(attempt, facts, attachedNow = []) {
1760
1787
  if (!isVersion(facts.current) || !isVersion(facts.latest)) return { kind: "unverified", reason: `the running (${facts.current}) or published (${facts.latest}) version could not be read` };
1761
- if (target === void 0) return { kind: "current" };
1762
- if (!isVersion(target)) return { kind: "unverified", reason: `the attempted update's target (${target}) could not be read` };
1763
- if (olderThan(facts.current, target)) return { kind: "unverified", reason: `the runner came back on ${facts.current}, not the attempted ${target}` };
1788
+ const cycle = attempt.recovery;
1789
+ if (cycle?.verifiedAt) return { kind: "completed" };
1790
+ if (!cycle) {
1791
+ 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" };
1792
+ return { kind: "current" };
1793
+ }
1794
+ if (!isVersion(cycle.target)) return { kind: "unverified", reason: `the attempted update's target (${cycle.target}) could not be read` };
1795
+ if (olderThan(facts.current, cycle.target)) return { kind: "unverified", reason: `the runner came back on ${facts.current}, not the attempted ${cycle.target}` };
1764
1796
  const observed = facts.observations ?? [];
1765
- const answered = (source) => observed.some((o) => o.source === source && o.success);
1766
1797
  if (!observed.length) return { kind: "unverified", reason: `the runner is on ${facts.current}, but no health look was taken` };
1767
- if (!answered("sessions") || !answered("health")) {
1768
- return { kind: "unverified", reason: `the runner is on ${facts.current}, but its local runtime answered only partly (sessions ${answered("sessions") ? "ok" : "failed"}, health ${answered("health") ? "ok" : "failed"})` };
1769
- }
1798
+ const look = (source) => {
1799
+ const o = observed.filter((x) => x.source === source).at(-1);
1800
+ return !o ? "missing" : o.malformed ? "malformed" : o.success ? "ok" : "failed";
1801
+ };
1802
+ const sessions = look("sessions");
1803
+ const health = look("health");
1804
+ if (sessions !== "ok" || health !== "ok" || facts.busy.includes("runner-work-observation-unavailable")) {
1805
+ return { kind: "unverified", reason: `the runner is on ${facts.current}, but its local work look was not readable (sessions ${sessions}, health ${health})` };
1806
+ }
1807
+ const back = new Set(attachedNow);
1808
+ const missing = cycle.attachments.filter((id) => !back.has(id));
1809
+ 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` };
1770
1810
  return { kind: "verified" };
1771
1811
  }
1772
1812
  function blockerClassFor(waitingFor) {
@@ -1865,6 +1905,7 @@ var AutomaticUpdateController = class {
1865
1905
  */
1866
1906
  offline() {
1867
1907
  if (!this.retryable()) return;
1908
+ this.checkAt = this.now();
1868
1909
  this.failures++;
1869
1910
  this.deps.status("offline", { reason: "The owner policy could not be refreshed; automatic update is deferred." });
1870
1911
  this.schedule();
@@ -1880,23 +1921,8 @@ var AutomaticUpdateController = class {
1880
1921
  * this resolves null — the race has settled, so the late result cannot overwrite a newer one or reach planning,
1881
1922
  * install or restart. A late rejection is absorbed rather than left unhandled.
1882
1923
  */
1883
- async bounded(work) {
1884
- const abort = new AbortController();
1885
- let timer;
1886
- const expired = new Promise((resolve5) => {
1887
- timer = this.boundTimer(() => {
1888
- abort.abort();
1889
- resolve5(null);
1890
- }, OBSERVATION_TIMEOUT_MS);
1891
- timer.unref?.();
1892
- });
1893
- const running = work(abort.signal);
1894
- running.catch(() => void 0);
1895
- try {
1896
- return await Promise.race([running, expired]);
1897
- } finally {
1898
- if (timer) this.clearTimer(timer);
1899
- }
1924
+ bounded(work) {
1925
+ return boundedWork(work, { timer: this.boundTimer, clear: this.clearTimer });
1900
1926
  }
1901
1927
  async decide() {
1902
1928
  const facts = await this.bounded((signal) => this.deps.facts(signal));
@@ -1912,14 +1938,21 @@ var AutomaticUpdateController = class {
1912
1938
  const plan = this.deps.plan(facts);
1913
1939
  this.decisionAt = this.now();
1914
1940
  this.blockerClass = plan.action === "wait" ? blockerClassFor(plan.waitingFor) : null;
1915
- if (plan.action === "up-to-date") {
1916
- const verdict = updateVerdict(this.deps.policy().value, facts);
1941
+ const policy = this.deps.policy().value;
1942
+ const open = policy.recovery && !policy.recovery.verifiedAt ? policy.recovery : void 0;
1943
+ const reached = !!open && isVersion(open.target) && isVersion(facts.current) && !olderThan(facts.current, open.target);
1944
+ if (plan.action === "up-to-date" || reached) {
1945
+ const verdict = updateVerdict(policy, facts, this.deps.attachments?.() ?? []);
1917
1946
  if (verdict.kind === "unverified") {
1918
1947
  this.failures++;
1919
1948
  this.deps.status("failure", { reason: `Not verified: ${verdict.reason}.` });
1949
+ } else if (verdict.kind === "unknown") {
1950
+ this.failures = 0;
1951
+ this.deps.status("enabled", { latestVersion: facts.latest ?? void 0, reason: `Not verified: ${verdict.reason}.` });
1920
1952
  } else {
1921
1953
  this.failures = 0;
1922
- this.deps.status(verdict.kind === "verified" ? "verified-success" : "enabled", { latestVersion: facts.latest ?? void 0, reason: void 0 });
1954
+ const resolved = verdict.kind === "verified" && policy.recovery ? { recovery: { ...policy.recovery, verifiedAt: new Date(this.now()).toISOString() } } : {};
1955
+ this.deps.status(verdict.kind === "current" ? "enabled" : "verified-success", { latestVersion: facts.latest ?? void 0, reason: void 0, ...resolved });
1923
1956
  }
1924
1957
  this.schedule();
1925
1958
  return;
@@ -1934,14 +1967,17 @@ var AutomaticUpdateController = class {
1934
1967
  this.defer();
1935
1968
  return;
1936
1969
  }
1937
- this.deps.status("running", { latestVersion: plan.latest, reason: void 0 });
1938
- const restarted = await this.install(plan, true);
1970
+ const target = open && isVersion(open.target) ? open.target : plan.latest;
1971
+ const attempt = { ...plan, latest: target };
1972
+ const recovery = open ?? { target, attachments: [...this.deps.attachments?.() ?? []], at: new Date(this.now()).toISOString() };
1973
+ this.deps.status("running", { latestVersion: target, reason: void 0, recovery });
1974
+ const restarted = await this.install(attempt, true);
1939
1975
  if (restarted) {
1940
- this.deps.status("restarting", { latestVersion: plan.latest, reason: void 0 });
1976
+ this.deps.status("restarting", { latestVersion: target, reason: void 0 });
1941
1977
  return;
1942
1978
  }
1943
1979
  this.blockerClass = "idle_wait";
1944
- this.defer(plan.latest);
1980
+ this.defer(target);
1945
1981
  }
1946
1982
  defer(latest) {
1947
1983
  if (this.waiting) return;
@@ -2005,14 +2041,17 @@ var RelayMutationAdmission = class {
2005
2041
  };
2006
2042
 
2007
2043
  // src/commands/runner/runner-work-observation.ts
2044
+ var count = (v) => typeof v === "number" && Number.isInteger(v) && v >= 0;
2045
+ function isWorkShape(work) {
2046
+ if (!work || typeof work !== "object" || Array.isArray(work)) return false;
2047
+ const w2 = work;
2048
+ return count(w2.codingTurns) && count(w2.localRuns) && Array.isArray(w2.detail) && w2.detail.every((item) => typeof item === "string");
2049
+ }
2008
2050
  function observedRunnerWork(observation) {
2009
2051
  const sessions = observation.sessions;
2010
2052
  const work = observation.work;
2011
- if (!Array.isArray(sessions) || !work || typeof work !== "object") return ["runner-work-observation-unavailable"];
2012
- const codingTurns = typeof work.codingTurns === "number" && Number.isInteger(work.codingTurns) && work.codingTurns >= 0 ? work.codingTurns : null;
2013
- const localRuns = typeof work.localRuns === "number" && Number.isInteger(work.localRuns) && work.localRuns >= 0 ? work.localRuns : null;
2014
- const detail = Array.isArray(work.detail) && work.detail.every((item) => typeof item === "string") ? work.detail : null;
2015
- if (codingTurns === null || localRuns === null || detail === null) return ["runner-work-observation-unavailable"];
2053
+ if (!Array.isArray(sessions) || !isWorkShape(work)) return ["runner-work-observation-unavailable"];
2054
+ const { codingTurns, localRuns, detail } = work;
2016
2055
  const activeCoding = sessions.flatMap((session) => {
2017
2056
  if (session?.alive === false) return [];
2018
2057
  const id = typeof session?.sessionId === "string" && session.sessionId ? session.sessionId : "unidentified-session";
@@ -2028,6 +2067,155 @@ function observedRunnerWork(observation) {
2028
2067
  ...Array.from({ length: localRuns }, (_v, i) => localDetail[i] || `local-run-${i + 1}`)
2029
2068
  ];
2030
2069
  }
2070
+ function readWorkLook(sessions, health, sampledAt) {
2071
+ const sessionsOk = sessions.status === "fulfilled" && Array.isArray(sessions.value?.sessions);
2072
+ const healthOk = health.status === "fulfilled" && isWorkShape(health.value?.work);
2073
+ const busy = sessions.status === "fulfilled" && health.status === "fulfilled" ? observedRunnerWork({ sessions: sessions.value?.sessions, work: health.value?.work }) : ["runner-work-observation-unavailable"];
2074
+ return {
2075
+ busy,
2076
+ observations: [
2077
+ { source: "sessions", success: sessionsOk, sampledAt, ...sessions.status === "fulfilled" && !sessionsOk ? { malformed: true } : {} },
2078
+ { source: "health", success: healthOk, sampledAt, ...health.status === "fulfilled" && !healthOk ? { malformed: true } : {} }
2079
+ ]
2080
+ };
2081
+ }
2082
+
2083
+ // src/commands/runner/update-bridge.ts
2084
+ var PolicyCell = class {
2085
+ constructor(policy, persist, enabled = true) {
2086
+ this.policy = policy;
2087
+ this.persist = persist;
2088
+ this.enabled = enabled;
2089
+ }
2090
+ policy;
2091
+ persist;
2092
+ enabled;
2093
+ authoritative = false;
2094
+ refreshing = false;
2095
+ fence = new ResponseFence();
2096
+ /** The status writer every update path uses. */
2097
+ record(status, extra = {}) {
2098
+ this.policy = { ...this.policy, ...extra, status, lastAttemptAt: (/* @__PURE__ */ new Date()).toISOString() };
2099
+ if (this.enabled) this.persist(this.policy);
2100
+ }
2101
+ /** The number a registration/heartbeat takes as it is SENT. */
2102
+ next() {
2103
+ return this.fence.next();
2104
+ }
2105
+ /**
2106
+ * Fold one service answer in. It is VALIDATED before it is ordered (#1020): an answer that carries no readable
2107
+ * policy must not advance the fence, or it would suppress a valid older OFF still in flight. The owner's OFF ends
2108
+ * any recovery cycle.
2109
+ */
2110
+ accept(response, sentAs) {
2111
+ if (!this.enabled) return null;
2112
+ const accepted = acceptPolicyResponse(this.policy, this.authoritative, response);
2113
+ if (!accepted || !this.fence.accepts(sentAs)) return null;
2114
+ this.policy = accepted.action === "cancel" ? { ...accepted.policy, recovery: void 0 } : accepted.policy;
2115
+ this.authoritative = true;
2116
+ this.persist(this.policy);
2117
+ return accepted.action;
2118
+ }
2119
+ };
2120
+ function createPolicyChannel(deps) {
2121
+ const registered = /* @__PURE__ */ new Set();
2122
+ const act = (action) => {
2123
+ if (action === "cancel") deps.onAction("cancel");
2124
+ else if (action === "start" && !deps.cell.refreshing) deps.onAction("start");
2125
+ };
2126
+ const accept = (response, sentAs) => act(deps.cell.accept(response, sentAs));
2127
+ const register = async (id, force = deps.force, signal) => {
2128
+ const sent = deps.cell.next();
2129
+ try {
2130
+ const result = await deps.post(`/v1/instances/${apiPathSegment(id)}/runtime`, deps.body(id, force), signal);
2131
+ if (signal?.aborted) return false;
2132
+ accept(result, sent);
2133
+ registered.add(id);
2134
+ return true;
2135
+ } catch (e) {
2136
+ if (signal?.aborted) return false;
2137
+ registered.delete(id);
2138
+ deps.onError(id, e instanceof Error ? e.message : String(e));
2139
+ return false;
2140
+ }
2141
+ };
2142
+ const refresh = async (signal) => {
2143
+ const id = registered.values().next().value ?? [...deps.held()][0];
2144
+ if (!id) return false;
2145
+ deps.cell.authoritative = false;
2146
+ deps.cell.refreshing = true;
2147
+ try {
2148
+ return await register(id, deps.force, signal) && deps.cell.authoritative;
2149
+ } finally {
2150
+ deps.cell.refreshing = false;
2151
+ }
2152
+ };
2153
+ return { registered, register, refresh, accept };
2154
+ }
2155
+ var BoundExpired = class extends Error {
2156
+ boundExpired = true;
2157
+ };
2158
+ function createInstallBridge(deps) {
2159
+ let inFlight = false;
2160
+ let installedPendingRestart = null;
2161
+ const within = (work) => boundedWork(work, deps.bound);
2162
+ const release = (drained) => {
2163
+ if (drained) deps.admission.resume();
2164
+ inFlight = false;
2165
+ };
2166
+ const expired = (drained, what) => {
2167
+ release(drained);
2168
+ throw new BoundExpired(`${what} did not answer within ${(deps.bound?.ms ?? 3e4) / 1e3}s; nothing restarted, and it will be retried.`);
2169
+ };
2170
+ const run2 = async (plan, automatic = false, lease) => {
2171
+ if (inFlight || lease && !deps.leaseActive(lease)) return false;
2172
+ inFlight = true;
2173
+ if (automatic) {
2174
+ const fresh = await within((signal) => deps.refresh(signal));
2175
+ if (fresh === null) expired(false, "The owner policy refresh before installing");
2176
+ if (!fresh || !deps.cell.policy.autoUpdate) {
2177
+ release(false);
2178
+ return false;
2179
+ }
2180
+ }
2181
+ if (installedPendingRestart !== plan.latest) {
2182
+ deps.log(`Updating ${plan.current} \u2192 ${plan.latest} (runner_update)\u2026`);
2183
+ try {
2184
+ await deps.install(plan.latest);
2185
+ installedPendingRestart = plan.latest;
2186
+ } catch (error) {
2187
+ release(false);
2188
+ throw error;
2189
+ }
2190
+ }
2191
+ await deps.admission.drain();
2192
+ await (deps.settle ?? (() => new Promise((resolve5) => setTimeout(resolve5, 500))))();
2193
+ if (lease && !deps.leaseActive(lease)) {
2194
+ release(true);
2195
+ return false;
2196
+ }
2197
+ if (automatic) {
2198
+ const fresh = await within((signal) => deps.refresh(signal));
2199
+ if (fresh === null) expired(true, "The owner policy refresh after installing");
2200
+ if (!fresh || !deps.cell.policy.autoUpdate) {
2201
+ release(true);
2202
+ return false;
2203
+ }
2204
+ }
2205
+ const final = await within((signal) => deps.facts(signal));
2206
+ if (final === null) expired(true, "The final local work look before restarting");
2207
+ const facts = final;
2208
+ const workObserved = !facts.busy.includes("runner-work-observation-unavailable");
2209
+ if (facts.busy.length || automatic && !mayAutomaticallyRestart({ authoritative: deps.cell.authoritative, enabled: deps.cell.policy.autoUpdate, workObserved, busy: facts.busy })) {
2210
+ release(true);
2211
+ return false;
2212
+ }
2213
+ deps.log(`Installed ${plan.latest} \u2014 restarting; every agent re-attaches on the way back up.`);
2214
+ deps.detachAll();
2215
+ return true;
2216
+ };
2217
+ return { run: run2, inFlight: () => inFlight, installedPendingRestart: () => installedPendingRestart };
2218
+ }
2031
2219
 
2032
2220
  // src/commands/runner/relay.ts
2033
2221
  var MEMBERSHIP_SYNC_PATH = "/pags/membership/sync";
@@ -2039,84 +2227,43 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2039
2227
  const runnerNode = hostname3();
2040
2228
  const machine = loadMachineIdentity(runnerNode);
2041
2229
  const autoUpdateKey = machine.id ? autoUpdatePolicyKey(machine.id) : "";
2042
- let autoUpdatePolicy = autoUpdateKey ? loadAutoUpdatePolicy(autoUpdateKey) ?? { autoUpdate: false } : { autoUpdate: false };
2043
- let autoUpdatePolicyAuthoritative = false;
2044
- let refreshingAutomaticPolicy = false;
2230
+ const cell = new PolicyCell(autoUpdateKey ? loadAutoUpdatePolicy(autoUpdateKey) ?? { autoUpdate: false } : { autoUpdate: false }, (p) => saveAutoUpdatePolicy(autoUpdateKey, p), !!autoUpdateKey);
2045
2231
  let scheduleAutomaticUpdate = () => void 0;
2046
2232
  let autoUpdateDiagnostics = () => ({});
2047
- const saveAutoUpdateStatus = (status, extra = {}) => {
2048
- autoUpdatePolicy = {
2049
- ...autoUpdatePolicy,
2050
- ...extra,
2051
- status,
2052
- lastAttemptAt: (/* @__PURE__ */ new Date()).toISOString()
2053
- };
2054
- if (autoUpdateKey) saveAutoUpdatePolicy(autoUpdateKey, autoUpdatePolicy);
2055
- };
2056
- const policyFence = new ResponseFence();
2057
- const acceptAutoUpdatePolicy = (response, sent) => {
2058
- if (!autoUpdateKey || !policyFence.accepts(sent)) return;
2059
- const accepted = acceptPolicyResponse(autoUpdatePolicy, autoUpdatePolicyAuthoritative, response);
2060
- if (!accepted) return;
2061
- autoUpdatePolicy = accepted.policy;
2062
- autoUpdatePolicyAuthoritative = true;
2063
- if (autoUpdateKey) saveAutoUpdatePolicy(autoUpdateKey, autoUpdatePolicy);
2064
- if (accepted.action === "cancel") {
2065
- scheduleAutomaticUpdate(false, true);
2066
- return;
2067
- }
2068
- if (refreshingAutomaticPolicy) return;
2069
- if (accepted.action === "start") {
2070
- scheduleAutomaticUpdate(true);
2071
- }
2072
- };
2233
+ const autoUpdateStatusBody = () => ({
2234
+ status: autoUpdateStatusWire(cell.policy.status),
2235
+ latestVersion: cell.policy.latestVersion,
2236
+ error: cell.policy.reason,
2237
+ ...autoUpdateDiagnostics()
2238
+ });
2073
2239
  const capabilities = await requestRunner("GET", "/capabilities", { url: localUrl, token: runnerToken, instanceId: instanceIds[0] });
2074
2240
  const caps = Array.isArray(capabilities.capabilities) ? capabilities.capabilities.filter((item) => typeof item === "string") : [];
2075
- const registered = /* @__PURE__ */ new Set();
2076
2241
  let lastRegisterError = "";
2077
- const registerRuntime = async (id, forceClaim = force, signal) => {
2078
- const sent = policyFence.next();
2079
- try {
2080
- const result = await requestPags("POST", `/v1/instances/${apiPathSegment(id)}/runtime`, opts, {
2081
- endpointUrl: localUrl,
2082
- token: runnerToken,
2083
- placement: "local",
2084
- capabilities: caps,
2085
- runnerVersion: CLI_VERSION,
2086
- runnerNode,
2087
- machineId: machine.id,
2088
- machineNames: machine.names,
2089
- force: forceClaim,
2090
- autoUpdateStatus: {
2091
- status: autoUpdateStatusWire(autoUpdatePolicy.status),
2092
- latestVersion: autoUpdatePolicy.latestVersion,
2093
- error: autoUpdatePolicy.reason,
2094
- ...autoUpdateDiagnostics()
2095
- }
2096
- }, signal);
2097
- if (signal?.aborted) return false;
2098
- acceptAutoUpdatePolicy(result, sent);
2099
- registered.add(id);
2100
- return true;
2101
- } catch (e) {
2102
- const msg = e instanceof Error ? e.message : String(e);
2103
- registered.delete(id);
2242
+ const channel = createPolicyChannel({
2243
+ cell,
2244
+ post: (path, body, signal) => requestPags("POST", path, opts, body, signal),
2245
+ body: (_id, forceClaim) => ({
2246
+ endpointUrl: localUrl,
2247
+ token: runnerToken,
2248
+ placement: "local",
2249
+ capabilities: caps,
2250
+ runnerVersion: CLI_VERSION,
2251
+ runnerNode,
2252
+ machineId: machine.id,
2253
+ machineNames: machine.names,
2254
+ force: forceClaim,
2255
+ autoUpdateStatus: autoUpdateStatusBody()
2256
+ }),
2257
+ force,
2258
+ held: () => attached.keys(),
2259
+ onError: (id, msg) => {
2104
2260
  lastRegisterError = msg;
2105
2261
  writeError(`register ${id.slice(0, 8)}\u2026 failed: ${msg}`);
2106
- return false;
2107
- }
2108
- };
2109
- const refreshAutomaticPolicy = async (signal) => {
2110
- const id = registered.values().next().value;
2111
- if (!id) return false;
2112
- autoUpdatePolicyAuthoritative = false;
2113
- refreshingAutomaticPolicy = true;
2114
- try {
2115
- return await registerRuntime(id, force, signal) && autoUpdatePolicyAuthoritative;
2116
- } finally {
2117
- refreshingAutomaticPolicy = false;
2118
- }
2119
- };
2262
+ },
2263
+ onAction: (action) => scheduleAutomaticUpdate(action === "start", action === "cancel")
2264
+ });
2265
+ const registered = channel.registered;
2266
+ const registerRuntime = (id, forceClaim = force) => channel.register(id, forceClaim);
2120
2267
  const attached = /* @__PURE__ */ new Map();
2121
2268
  const processStartedAt = Date.now();
2122
2269
  const starts24h = recordRunnerStart(processStartedAt);
@@ -2138,13 +2285,7 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2138
2285
  }, void 0, signal),
2139
2286
  requestRunner("GET", "/health", { url: localUrl, token: runnerToken, instanceId: instanceIds[0] }, void 0, signal)
2140
2287
  ]);
2141
- const busy = sessionsResult.status === "fulfilled" && healthResult.status === "fulfilled" ? observedRunnerWork({ sessions: sessionsResult.value.sessions, work: healthResult.value.work }) : ["runner-work-observation-unavailable"];
2142
- const sampledAt = Date.now();
2143
- const observations = [
2144
- // An answer is a success only when it has the shape the guard reads; malformed is a failed look.
2145
- { source: "sessions", success: sessionsResult.status === "fulfilled" && Array.isArray(sessionsResult.value.sessions), sampledAt },
2146
- { source: "health", success: healthResult.status === "fulfilled" && !!healthResult.value.work && typeof healthResult.value.work === "object", sampledAt }
2147
- ];
2288
+ const { busy, observations } = readWorkLook(sessionsResult, healthResult, Date.now());
2148
2289
  return {
2149
2290
  observations,
2150
2291
  current: CLI_VERSION,
@@ -2156,94 +2297,47 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2156
2297
  busy
2157
2298
  };
2158
2299
  };
2159
- let updateInstallInFlight = false;
2160
- let installedPendingRestart = null;
2161
2300
  const relayMutationAdmission = new RelayMutationAdmission();
2162
- const installAndRestart = async (plan, automatic = false, lease) => {
2163
- if (updateInstallInFlight || lease && !deferredUpdateLeaseActive(lease)) return false;
2164
- updateInstallInFlight = true;
2165
- if (automatic && (!await refreshAutomaticPolicy() || !autoUpdatePolicy.autoUpdate)) {
2166
- updateInstallInFlight = false;
2167
- return false;
2168
- }
2169
- if (installedPendingRestart !== plan.latest) {
2170
- writeLine(`Updating ${plan.current} \u2192 ${plan.latest} (runner_update)\u2026`);
2171
- try {
2172
- await installVersion(plan.latest);
2173
- installedPendingRestart = plan.latest;
2174
- } catch (error) {
2175
- updateInstallInFlight = false;
2176
- throw error;
2177
- }
2178
- }
2179
- await relayMutationAdmission.drain();
2180
- await new Promise((resolve5) => setTimeout(resolve5, 500));
2181
- if (lease && !deferredUpdateLeaseActive(lease)) {
2182
- relayMutationAdmission.resume();
2183
- updateInstallInFlight = false;
2184
- return false;
2185
- }
2186
- if (automatic) {
2187
- if (!await refreshAutomaticPolicy() || !autoUpdatePolicy.autoUpdate) {
2188
- relayMutationAdmission.resume();
2189
- updateInstallInFlight = false;
2190
- return false;
2191
- }
2192
- }
2193
- const finalFacts = await updateFacts();
2194
- if (finalFacts.busy.length || automatic && !mayAutomaticallyRestart({ authoritative: autoUpdatePolicyAuthoritative, enabled: autoUpdatePolicy.autoUpdate, workObserved: !finalFacts.busy.includes("runner-work-observation-unavailable"), busy: finalFacts.busy })) {
2195
- relayMutationAdmission.resume();
2196
- updateInstallInFlight = false;
2197
- return false;
2198
- }
2199
- writeLine(`Installed ${plan.latest} \u2014 restarting; every agent re-attaches on the way back up.`);
2200
- for (const id of [...attached.keys()]) detach(id);
2201
- return true;
2202
- };
2301
+ const bridge = createInstallBridge({
2302
+ cell,
2303
+ refresh: channel.refresh,
2304
+ facts: updateFacts,
2305
+ install: installVersion,
2306
+ admission: relayMutationAdmission,
2307
+ leaseActive: deferredUpdateLeaseActive,
2308
+ detachAll: () => {
2309
+ for (const id of [...attached.keys()]) detach(id);
2310
+ },
2311
+ log: writeLine
2312
+ });
2203
2313
  let updateWaiting = false;
2204
2314
  let deferredManualLease = null;
2205
- const updateWhenIdle = (automatic = false, lease) => {
2206
- if (updateWaiting || !automatic && !lease) return false;
2315
+ const updateWhenIdle = (lease) => {
2316
+ if (updateWaiting) return false;
2207
2317
  updateWaiting = true;
2208
- if (!automatic) deferredManualLease = lease;
2209
- const until = automatic ? Date.now() + 60 * 6e4 : lease.expiresAt;
2318
+ deferredManualLease = lease;
2210
2319
  const tick = () => setTimeout(async () => {
2211
- if (!automatic && (!deferredManualLease || !deferredUpdateLeaseActive(deferredManualLease))) {
2320
+ if (!deferredManualLease || !deferredUpdateLeaseActive(deferredManualLease)) {
2212
2321
  writeLine("runner_update: the deferred operation lease expired; no delayed restart was performed.");
2213
2322
  deferredManualLease = null;
2214
2323
  updateWaiting = false;
2215
2324
  return;
2216
2325
  }
2217
- if (automatic && (!await refreshAutomaticPolicy() || !autoUpdatePolicy.autoUpdate)) {
2218
- saveAutoUpdateStatus("offline", { reason: "The owner policy could not be refreshed; automatic update is deferred." });
2219
- updateWaiting = false;
2220
- return;
2221
- }
2222
- const plan = planRunnerUpdate(await updateFacts());
2223
- if (plan.action === "update") {
2224
- if (automatic && !autoUpdatePolicy.autoUpdate) {
2225
- updateWaiting = false;
2226
- return;
2227
- }
2228
- if (automatic) saveAutoUpdateStatus("running", { latestVersion: plan.latest, reason: void 0 });
2229
- const installed = await installAndRestart(plan, automatic, automatic ? void 0 : deferredManualLease ?? void 0).catch((e) => {
2230
- if (automatic) saveAutoUpdateStatus("failure", { reason: e instanceof Error ? e.message : String(e) });
2326
+ const facts = await boundedWork((signal) => updateFacts(signal));
2327
+ const plan = facts ? planRunnerUpdate(facts) : null;
2328
+ if (plan?.action === "update") {
2329
+ const installed = await bridge.run(plan, false, deferredManualLease).catch((e) => {
2231
2330
  writeError(`runner_update: install failed \u2014 ${e instanceof Error ? e.message : String(e)}`);
2232
2331
  return false;
2233
2332
  });
2234
- if (!installed && Date.now() < until) {
2235
- saveAutoUpdateStatus("waiting-for-idle", { latestVersion: plan.latest });
2236
- tick();
2237
- } else {
2333
+ if (!installed && Date.now() < lease.expiresAt) tick();
2334
+ else {
2238
2335
  deferredManualLease = null;
2239
2336
  updateWaiting = false;
2240
2337
  if (installed) leaveForRestart(plan.restarter);
2241
2338
  }
2242
- } else if (plan.action === "wait" && Date.now() < until) {
2243
- if (automatic) saveAutoUpdateStatus("waiting-for-idle", { latestVersion: plan.latest, reason: void 0 });
2244
- tick();
2245
- } else {
2246
- if (automatic && plan.action === "refused") saveAutoUpdateStatus("unsupported", { reason: plan.reason });
2339
+ } else if ((!plan || plan.action === "wait") && Date.now() < lease.expiresAt) tick();
2340
+ else {
2247
2341
  deferredManualLease = null;
2248
2342
  updateWaiting = false;
2249
2343
  }
@@ -2251,107 +2345,41 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2251
2345
  tick();
2252
2346
  return true;
2253
2347
  };
2254
- let automaticTimer = null;
2255
- let automaticChecking = false;
2256
- let automaticFailures = 0;
2257
- const runAutomaticUpdateCheck = async () => {
2258
- if (!autoUpdatePolicy.autoUpdate || !autoUpdatePolicyAuthoritative || automaticChecking || updateWaiting) return;
2259
- automaticChecking = true;
2260
- try {
2261
- if (!await refreshAutomaticPolicy() || !autoUpdatePolicy.autoUpdate) {
2262
- saveAutoUpdateStatus("offline", { reason: "The owner policy could not be refreshed; automatic update is deferred." });
2263
- scheduleAutomaticUpdate();
2264
- return;
2265
- }
2266
- saveAutoUpdateStatus("checking", { reason: void 0 });
2267
- const facts = await updateFacts();
2268
- if (!autoUpdatePolicy.autoUpdate || !autoUpdatePolicyAuthoritative) return;
2269
- if (!facts.latest) {
2270
- automaticFailures++;
2271
- saveAutoUpdateStatus("failure", { reason: "Could not check the trusted npm registry for a newer CLI release." });
2272
- scheduleAutomaticUpdate();
2273
- return;
2274
- }
2275
- automaticFailures = 0;
2276
- const plan = planRunnerUpdate(facts);
2277
- if (plan.action === "up-to-date") {
2278
- saveAutoUpdateStatus("verified-success", { latestVersion: facts.latest, reason: void 0 });
2279
- scheduleAutomaticUpdate();
2280
- return;
2281
- }
2282
- if (plan.action === "refused") {
2283
- saveAutoUpdateStatus("unsupported", { latestVersion: facts.latest, reason: plan.reason });
2284
- scheduleAutomaticUpdate();
2285
- return;
2286
- }
2287
- if (plan.action === "wait") {
2288
- saveAutoUpdateStatus("waiting-for-idle", { latestVersion: plan.latest, reason: void 0 });
2289
- updateWhenIdle(true);
2290
- return;
2291
- }
2292
- if (!autoUpdatePolicy.autoUpdate || !autoUpdatePolicyAuthoritative) return;
2293
- saveAutoUpdateStatus("running", { latestVersion: plan.latest, reason: void 0 });
2294
- try {
2295
- const installed = await installAndRestart(plan, true);
2296
- if (!installed) {
2297
- saveAutoUpdateStatus("waiting-for-idle", { latestVersion: plan.latest });
2298
- updateWhenIdle(true);
2299
- return;
2300
- }
2301
- saveAutoUpdateStatus("restarting", { latestVersion: plan.latest, reason: void 0 });
2302
- } catch (e) {
2303
- automaticFailures++;
2304
- saveAutoUpdateStatus("failure", { latestVersion: plan.latest, reason: e instanceof Error ? e.message : String(e) });
2305
- scheduleAutomaticUpdate();
2306
- }
2307
- } finally {
2308
- automaticChecking = false;
2309
- }
2310
- };
2311
- scheduleAutomaticUpdate = (initial = false, cancel = false) => {
2312
- if (cancel || !autoUpdatePolicy.autoUpdate || !autoUpdatePolicyAuthoritative) {
2313
- if (automaticTimer) clearTimeout(automaticTimer);
2314
- automaticTimer = null;
2315
- return;
2316
- }
2317
- if (automaticTimer || updateWaiting) return;
2318
- const delay = initial ? Math.round(5e3 + Math.random() * 25e3) : nextAutoUpdateDelayMs(automaticFailures);
2319
- automaticTimer = setTimeout(() => {
2320
- automaticTimer = null;
2321
- void runAutomaticUpdateCheck();
2322
- }, delay);
2323
- automaticTimer.unref();
2324
- };
2325
2348
  const automaticUpdateController = new AutomaticUpdateController({
2326
- policy: () => ({ value: autoUpdatePolicy, authoritative: autoUpdatePolicyAuthoritative }),
2327
- refreshPolicy: refreshAutomaticPolicy,
2349
+ policy: () => ({ value: cell.policy, authoritative: cell.authoritative }),
2350
+ refreshPolicy: channel.refresh,
2328
2351
  facts: updateFacts,
2329
2352
  plan: planRunnerUpdate,
2330
- // This bridge is deliberately the same function used by explicit runner_update. It owns npm,
2331
- // a fresh authoritative policy, the mutation drain, final local-work observation and restart.
2353
+ // The same bridge an explicit runner_update uses: npm, a fresh authoritative policy, the mutation drain, the
2354
+ // final local-work observation and restart — each wait bounded.
2332
2355
  installAndRestart: async (plan, automatic) => {
2333
- const installed = await installAndRestart(plan, automatic);
2356
+ const installed = await bridge.run(plan, automatic);
2334
2357
  if (installed) setTimeout(() => leaveForRestart(plan.restarter), 0).unref();
2335
2358
  return installed;
2336
2359
  },
2337
- status: saveAutoUpdateStatus
2360
+ status: (status, extra) => cell.record(status, extra),
2361
+ // The agents held now: attached AND registered — recorded with an install, required back after it.
2362
+ attachments: () => [...attached.keys()].filter((id) => registered.has(id))
2338
2363
  });
2339
2364
  autoUpdateDiagnostics = () => automaticUpdateController.diagnostics();
2340
2365
  scheduleAutomaticUpdate = (initial = false, cancel = false) => automaticUpdateController.onPolicy(cancel ? "cancel" : initial ? "start" : "keep");
2341
- if (autoUpdatePolicyAuthoritative && autoUpdatePolicy.autoUpdate) scheduleAutomaticUpdate(true);
2366
+ if (cell.authoritative && cell.policy.autoUpdate) scheduleAutomaticUpdate(true);
2342
2367
  const answerUpdate = async (body) => {
2343
2368
  const dryRun = body?.dryRun === true;
2344
- const plan = planRunnerUpdate(await updateFacts());
2369
+ const facts = await boundedWork((signal) => updateFacts(signal));
2370
+ if (!facts) return { status: 504, result: { error: "The local work and registry look did not answer within 30s; nothing was installed or restarted." } };
2371
+ const plan = planRunnerUpdate(facts);
2345
2372
  if (dryRun || plan.action === "up-to-date" || plan.action === "refused") return { status: 200, result: { ...plan, dryRun } };
2346
2373
  if (plan.action === "wait") {
2347
2374
  const lease = deferredUpdateLease(body);
2348
2375
  if (!lease) return { status: 400, result: { error: "A busy runner_update needs a current operation lease; no delayed restart was scheduled." } };
2349
- if (!updateWhenIdle(false, lease)) return { status: 409, result: { error: "A runner update is already waiting for active work to finish." } };
2376
+ if (!updateWhenIdle(lease)) return { status: 409, result: { error: "A runner update is already waiting for active work to finish." } };
2350
2377
  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." } };
2351
2378
  }
2352
2379
  try {
2353
- if (!await installAndRestart(plan)) return { status: 409, result: { error: "A runner update is already installing or restarting." } };
2380
+ if (!await bridge.run(plan)) return { status: 409, result: { error: "A runner update is already installing or restarting." } };
2354
2381
  } catch (e) {
2382
+ if (e instanceof BoundExpired) return { status: 504, result: { error: e.message } };
2355
2383
  return { status: 500, result: { error: `npm could not install ${plan.latest}: ${e instanceof Error ? e.message : String(e)}` } };
2356
2384
  }
2357
2385
  const supervisor = supervisorNote(plan.restarter);
@@ -2465,19 +2493,14 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2465
2493
  });
2466
2494
  for (const id of [...attached.keys()]) {
2467
2495
  try {
2468
- const sent = policyFence.next();
2496
+ const sent = cell.next();
2469
2497
  const response = await requestPags("POST", `/v1/instances/${apiPathSegment(id)}/runtime/heartbeat`, opts, {
2470
2498
  runnerNode,
2471
2499
  machineId: machine.id,
2472
2500
  resources,
2473
- autoUpdateStatus: {
2474
- status: autoUpdateStatusWire(autoUpdatePolicy.status),
2475
- latestVersion: autoUpdatePolicy.latestVersion,
2476
- error: autoUpdatePolicy.reason,
2477
- ...autoUpdateDiagnostics()
2478
- }
2501
+ autoUpdateStatus: autoUpdateStatusBody()
2479
2502
  });
2480
- acceptAutoUpdatePolicy(response, sent);
2503
+ channel.accept(response, sent);
2481
2504
  } catch (e) {
2482
2505
  failure = e instanceof Error ? e.message : String(e);
2483
2506
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proagentstore/cli",
3
- "version": "0.4.109",
3
+ "version": "0.4.111",
4
4
  "description": "CLI for creating, publishing, and running ProAgentStore agents",
5
5
  "license": "MIT",
6
6
  "type": "module",