@proagentstore/cli 0.4.109 → 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.
Files changed (2) hide show
  1. package/dist/index.js +292 -278
  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,12 @@ 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
+ return { target: r.target, attachments: [...r.attachments], at: r.at };
1640
+ }
1635
1641
  var statuses = /* @__PURE__ */ new Set([
1636
1642
  "enabled",
1637
1643
  "checking",
@@ -1668,12 +1674,14 @@ function parseAutoUpdatePolicy(value) {
1668
1674
  const latestVersion = raw.latestVersion ?? raw.latest_version;
1669
1675
  const lastAttemptAt = raw.lastAttemptAt ?? raw.last_attempt_at;
1670
1676
  const reason = raw.reason;
1677
+ const recovery = parseRecovery(raw.recovery);
1671
1678
  return {
1672
1679
  autoUpdate,
1673
1680
  ...status ? { status } : {},
1674
1681
  ...typeof latestVersion === "string" ? { latestVersion } : {},
1675
1682
  ...typeof lastAttemptAt === "string" ? { lastAttemptAt } : {},
1676
- ...typeof reason === "string" ? { reason } : {}
1683
+ ...typeof reason === "string" ? { reason } : {},
1684
+ ...recovery ? { recovery } : {}
1677
1685
  };
1678
1686
  }
1679
1687
  function policyFromResponse(value) {
@@ -1729,8 +1737,9 @@ function policyScheduleAction(previous, next, wasAuthoritative) {
1729
1737
  return "keep";
1730
1738
  }
1731
1739
  function acceptPolicyResponse(local, wasAuthoritative, response) {
1732
- const remote = policyFromResponse(response);
1733
- if (!remote) return null;
1740
+ const parsed = policyFromResponse(response);
1741
+ if (!parsed) return null;
1742
+ const { recovery: _fromService, ...remote } = parsed;
1734
1743
  const action = policyScheduleAction(local, remote, wasAuthoritative);
1735
1744
  const ownerTransition = action === "cancel" || !local.autoUpdate;
1736
1745
  return { policy: ownerTransition ? { ...local, ...remote } : { ...local, autoUpdate: remote.autoUpdate }, action };
@@ -1755,18 +1764,47 @@ function mayAutomaticallyRestart(input) {
1755
1764
  var MAX_OBSERVATIONS = 5;
1756
1765
  var IDLE_RETRY_MS = 15e3;
1757
1766
  var OBSERVATION_TIMEOUT_MS = 3e4;
1758
- function updateVerdict(attempt, facts) {
1759
- const target = attempt.status === "running" || attempt.status === "restarting" ? attempt.latestVersion : void 0;
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 = []) {
1760
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` };
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}` };
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}` };
1764
1794
  const observed = facts.observations ?? [];
1765
- const answered = (source) => observed.some((o) => o.source === source && o.success);
1766
1795
  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
- }
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` };
1770
1808
  return { kind: "verified" };
1771
1809
  }
1772
1810
  function blockerClassFor(waitingFor) {
@@ -1865,6 +1903,7 @@ var AutomaticUpdateController = class {
1865
1903
  */
1866
1904
  offline() {
1867
1905
  if (!this.retryable()) return;
1906
+ this.checkAt = this.now();
1868
1907
  this.failures++;
1869
1908
  this.deps.status("offline", { reason: "The owner policy could not be refreshed; automatic update is deferred." });
1870
1909
  this.schedule();
@@ -1880,23 +1919,8 @@ var AutomaticUpdateController = class {
1880
1919
  * this resolves null — the race has settled, so the late result cannot overwrite a newer one or reach planning,
1881
1920
  * install or restart. A late rejection is absorbed rather than left unhandled.
1882
1921
  */
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
- }
1922
+ bounded(work) {
1923
+ return boundedWork(work, { timer: this.boundTimer, clear: this.clearTimer });
1900
1924
  }
1901
1925
  async decide() {
1902
1926
  const facts = await this.bounded((signal) => this.deps.facts(signal));
@@ -1913,13 +1937,16 @@ var AutomaticUpdateController = class {
1913
1937
  this.decisionAt = this.now();
1914
1938
  this.blockerClass = plan.action === "wait" ? blockerClassFor(plan.waitingFor) : null;
1915
1939
  if (plan.action === "up-to-date") {
1916
- const verdict = updateVerdict(this.deps.policy().value, facts);
1940
+ const verdict = updateVerdict(this.deps.policy().value, facts, this.deps.attachments?.() ?? []);
1917
1941
  if (verdict.kind === "unverified") {
1918
1942
  this.failures++;
1919
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}.` });
1920
1947
  } else {
1921
1948
  this.failures = 0;
1922
- this.deps.status(verdict.kind === "verified" ? "verified-success" : "enabled", { latestVersion: facts.latest ?? void 0, reason: void 0 });
1949
+ this.deps.status(verdict.kind === "verified" ? "verified-success" : "enabled", { latestVersion: facts.latest ?? void 0, reason: void 0, recovery: void 0 });
1923
1950
  }
1924
1951
  this.schedule();
1925
1952
  return;
@@ -1934,7 +1961,7 @@ var AutomaticUpdateController = class {
1934
1961
  this.defer();
1935
1962
  return;
1936
1963
  }
1937
- 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() } });
1938
1965
  const restarted = await this.install(plan, true);
1939
1966
  if (restarted) {
1940
1967
  this.deps.status("restarting", { latestVersion: plan.latest, reason: void 0 });
@@ -2005,14 +2032,17 @@ var RelayMutationAdmission = class {
2005
2032
  };
2006
2033
 
2007
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
+ }
2008
2041
  function observedRunnerWork(observation) {
2009
2042
  const sessions = observation.sessions;
2010
2043
  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"];
2044
+ if (!Array.isArray(sessions) || !isWorkShape(work)) return ["runner-work-observation-unavailable"];
2045
+ const { codingTurns, localRuns, detail } = work;
2016
2046
  const activeCoding = sessions.flatMap((session) => {
2017
2047
  if (session?.alive === false) return [];
2018
2048
  const id = typeof session?.sessionId === "string" && session.sessionId ? session.sessionId : "unidentified-session";
@@ -2028,6 +2058,155 @@ function observedRunnerWork(observation) {
2028
2058
  ...Array.from({ length: localRuns }, (_v, i) => localDetail[i] || `local-run-${i + 1}`)
2029
2059
  ];
2030
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
+ }
2031
2210
 
2032
2211
  // src/commands/runner/relay.ts
2033
2212
  var MEMBERSHIP_SYNC_PATH = "/pags/membership/sync";
@@ -2039,84 +2218,43 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2039
2218
  const runnerNode = hostname3();
2040
2219
  const machine = loadMachineIdentity(runnerNode);
2041
2220
  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;
2221
+ const cell = new PolicyCell(autoUpdateKey ? loadAutoUpdatePolicy(autoUpdateKey) ?? { autoUpdate: false } : { autoUpdate: false }, (p) => saveAutoUpdatePolicy(autoUpdateKey, p), !!autoUpdateKey);
2045
2222
  let scheduleAutomaticUpdate = () => void 0;
2046
2223
  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
- };
2224
+ const autoUpdateStatusBody = () => ({
2225
+ status: autoUpdateStatusWire(cell.policy.status),
2226
+ latestVersion: cell.policy.latestVersion,
2227
+ error: cell.policy.reason,
2228
+ ...autoUpdateDiagnostics()
2229
+ });
2073
2230
  const capabilities = await requestRunner("GET", "/capabilities", { url: localUrl, token: runnerToken, instanceId: instanceIds[0] });
2074
2231
  const caps = Array.isArray(capabilities.capabilities) ? capabilities.capabilities.filter((item) => typeof item === "string") : [];
2075
- const registered = /* @__PURE__ */ new Set();
2076
2232
  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);
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) => {
2104
2251
  lastRegisterError = msg;
2105
2252
  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
- };
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);
2120
2258
  const attached = /* @__PURE__ */ new Map();
2121
2259
  const processStartedAt = Date.now();
2122
2260
  const starts24h = recordRunnerStart(processStartedAt);
@@ -2138,13 +2276,7 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2138
2276
  }, void 0, signal),
2139
2277
  requestRunner("GET", "/health", { url: localUrl, token: runnerToken, instanceId: instanceIds[0] }, void 0, signal)
2140
2278
  ]);
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
- ];
2279
+ const { busy, observations } = readWorkLook(sessionsResult, healthResult, Date.now());
2148
2280
  return {
2149
2281
  observations,
2150
2282
  current: CLI_VERSION,
@@ -2156,94 +2288,47 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2156
2288
  busy
2157
2289
  };
2158
2290
  };
2159
- let updateInstallInFlight = false;
2160
- let installedPendingRestart = null;
2161
2291
  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
- };
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
+ });
2203
2304
  let updateWaiting = false;
2204
2305
  let deferredManualLease = null;
2205
- const updateWhenIdle = (automatic = false, lease) => {
2206
- if (updateWaiting || !automatic && !lease) return false;
2306
+ const updateWhenIdle = (lease) => {
2307
+ if (updateWaiting) return false;
2207
2308
  updateWaiting = true;
2208
- if (!automatic) deferredManualLease = lease;
2209
- const until = automatic ? Date.now() + 60 * 6e4 : lease.expiresAt;
2309
+ deferredManualLease = lease;
2210
2310
  const tick = () => setTimeout(async () => {
2211
- if (!automatic && (!deferredManualLease || !deferredUpdateLeaseActive(deferredManualLease))) {
2311
+ if (!deferredManualLease || !deferredUpdateLeaseActive(deferredManualLease)) {
2212
2312
  writeLine("runner_update: the deferred operation lease expired; no delayed restart was performed.");
2213
2313
  deferredManualLease = null;
2214
2314
  updateWaiting = false;
2215
2315
  return;
2216
2316
  }
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) });
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) => {
2231
2321
  writeError(`runner_update: install failed \u2014 ${e instanceof Error ? e.message : String(e)}`);
2232
2322
  return false;
2233
2323
  });
2234
- if (!installed && Date.now() < until) {
2235
- saveAutoUpdateStatus("waiting-for-idle", { latestVersion: plan.latest });
2236
- tick();
2237
- } else {
2324
+ if (!installed && Date.now() < lease.expiresAt) tick();
2325
+ else {
2238
2326
  deferredManualLease = null;
2239
2327
  updateWaiting = false;
2240
2328
  if (installed) leaveForRestart(plan.restarter);
2241
2329
  }
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 });
2330
+ } else if ((!plan || plan.action === "wait") && Date.now() < lease.expiresAt) tick();
2331
+ else {
2247
2332
  deferredManualLease = null;
2248
2333
  updateWaiting = false;
2249
2334
  }
@@ -2251,107 +2336,41 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2251
2336
  tick();
2252
2337
  return true;
2253
2338
  };
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
2339
  const automaticUpdateController = new AutomaticUpdateController({
2326
- policy: () => ({ value: autoUpdatePolicy, authoritative: autoUpdatePolicyAuthoritative }),
2327
- refreshPolicy: refreshAutomaticPolicy,
2340
+ policy: () => ({ value: cell.policy, authoritative: cell.authoritative }),
2341
+ refreshPolicy: channel.refresh,
2328
2342
  facts: updateFacts,
2329
2343
  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.
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.
2332
2346
  installAndRestart: async (plan, automatic) => {
2333
- const installed = await installAndRestart(plan, automatic);
2347
+ const installed = await bridge.run(plan, automatic);
2334
2348
  if (installed) setTimeout(() => leaveForRestart(plan.restarter), 0).unref();
2335
2349
  return installed;
2336
2350
  },
2337
- status: saveAutoUpdateStatus
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))
2338
2354
  });
2339
2355
  autoUpdateDiagnostics = () => automaticUpdateController.diagnostics();
2340
2356
  scheduleAutomaticUpdate = (initial = false, cancel = false) => automaticUpdateController.onPolicy(cancel ? "cancel" : initial ? "start" : "keep");
2341
- if (autoUpdatePolicyAuthoritative && autoUpdatePolicy.autoUpdate) scheduleAutomaticUpdate(true);
2357
+ if (cell.authoritative && cell.policy.autoUpdate) scheduleAutomaticUpdate(true);
2342
2358
  const answerUpdate = async (body) => {
2343
2359
  const dryRun = body?.dryRun === true;
2344
- const plan = planRunnerUpdate(await updateFacts());
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);
2345
2363
  if (dryRun || plan.action === "up-to-date" || plan.action === "refused") return { status: 200, result: { ...plan, dryRun } };
2346
2364
  if (plan.action === "wait") {
2347
2365
  const lease = deferredUpdateLease(body);
2348
2366
  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." } };
2367
+ if (!updateWhenIdle(lease)) return { status: 409, result: { error: "A runner update is already waiting for active work to finish." } };
2350
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." } };
2351
2369
  }
2352
2370
  try {
2353
- if (!await installAndRestart(plan)) return { status: 409, result: { error: "A runner update is already installing or restarting." } };
2371
+ if (!await bridge.run(plan)) return { status: 409, result: { error: "A runner update is already installing or restarting." } };
2354
2372
  } catch (e) {
2373
+ if (e instanceof BoundExpired) return { status: 504, result: { error: e.message } };
2355
2374
  return { status: 500, result: { error: `npm could not install ${plan.latest}: ${e instanceof Error ? e.message : String(e)}` } };
2356
2375
  }
2357
2376
  const supervisor = supervisorNote(plan.restarter);
@@ -2465,19 +2484,14 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2465
2484
  });
2466
2485
  for (const id of [...attached.keys()]) {
2467
2486
  try {
2468
- const sent = policyFence.next();
2487
+ const sent = cell.next();
2469
2488
  const response = await requestPags("POST", `/v1/instances/${apiPathSegment(id)}/runtime/heartbeat`, opts, {
2470
2489
  runnerNode,
2471
2490
  machineId: machine.id,
2472
2491
  resources,
2473
- autoUpdateStatus: {
2474
- status: autoUpdateStatusWire(autoUpdatePolicy.status),
2475
- latestVersion: autoUpdatePolicy.latestVersion,
2476
- error: autoUpdatePolicy.reason,
2477
- ...autoUpdateDiagnostics()
2478
- }
2492
+ autoUpdateStatus: autoUpdateStatusBody()
2479
2493
  });
2480
- acceptAutoUpdatePolicy(response, sent);
2494
+ channel.accept(response, sent);
2481
2495
  } catch (e) {
2482
2496
  failure = e instanceof Error ? e.message : String(e);
2483
2497
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proagentstore/cli",
3
- "version": "0.4.109",
3
+ "version": "0.4.110",
4
4
  "description": "CLI for creating, publishing, and running ProAgentStore agents",
5
5
  "license": "MIT",
6
6
  "type": "module",