@proagentstore/cli 0.4.107 → 0.4.109

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 CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  newestPayload,
9
9
  olderThan,
10
10
  payloadAt
11
- } from "./chunk-JOWYSZOL.js";
11
+ } from "./chunk-RGNU4W6B.js";
12
12
 
13
13
  // src/bootstrap.ts
14
14
  import { existsSync } from "fs";
@@ -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-JOWYSZOL.js";
9
+ } from "./chunk-RGNU4W6B.js";
9
10
 
10
11
  // src/index.ts
11
12
  import { createRequire as createRequire3 } from "module";
@@ -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) {
@@ -1725,6 +1728,25 @@ function policyScheduleAction(previous, next, wasAuthoritative) {
1725
1728
  if (!wasAuthoritative || !previous.autoUpdate) return "start";
1726
1729
  return "keep";
1727
1730
  }
1731
+ function acceptPolicyResponse(local, wasAuthoritative, response) {
1732
+ const remote = policyFromResponse(response);
1733
+ if (!remote) return null;
1734
+ const action = policyScheduleAction(local, remote, wasAuthoritative);
1735
+ const ownerTransition = action === "cancel" || !local.autoUpdate;
1736
+ return { policy: ownerTransition ? { ...local, ...remote } : { ...local, autoUpdate: remote.autoUpdate }, action };
1737
+ }
1738
+ var ResponseFence = class {
1739
+ sent = 0;
1740
+ accepted = 0;
1741
+ next() {
1742
+ return ++this.sent;
1743
+ }
1744
+ accepts(sentAs) {
1745
+ if (sentAs < this.accepted) return false;
1746
+ this.accepted = sentAs;
1747
+ return true;
1748
+ }
1749
+ };
1728
1750
  function mayAutomaticallyRestart(input) {
1729
1751
  return input.authoritative && input.enabled && input.workObserved && input.busy.length === 0;
1730
1752
  }
@@ -1732,6 +1754,21 @@ function mayAutomaticallyRestart(input) {
1732
1754
  // src/commands/runner/auto-update-controller.ts
1733
1755
  var MAX_OBSERVATIONS = 5;
1734
1756
  var IDLE_RETRY_MS = 15e3;
1757
+ var OBSERVATION_TIMEOUT_MS = 3e4;
1758
+ function updateVerdict(attempt, facts) {
1759
+ const target = attempt.status === "running" || attempt.status === "restarting" ? attempt.latestVersion : void 0;
1760
+ 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}` };
1764
+ const observed = facts.observations ?? [];
1765
+ const answered = (source) => observed.some((o) => o.source === source && o.success);
1766
+ 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
+ }
1770
+ return { kind: "verified" };
1771
+ }
1735
1772
  function blockerClassFor(waitingFor) {
1736
1773
  if (waitingFor.includes("runner-work-observation-unavailable")) return "observation_failure";
1737
1774
  return waitingFor.length ? "active_sessions" : "unknown";
@@ -1741,6 +1778,7 @@ var AutomaticUpdateController = class {
1741
1778
  this.deps = deps;
1742
1779
  this.setTimer = deps.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
1743
1780
  this.clearTimer = deps.clearTimer ?? ((timer) => clearTimeout(timer));
1781
+ this.boundTimer = deps.boundTimer ?? ((fn, ms) => setTimeout(fn, ms));
1744
1782
  this.now = deps.now ?? Date.now;
1745
1783
  }
1746
1784
  deps;
@@ -1757,6 +1795,7 @@ var AutomaticUpdateController = class {
1757
1795
  now;
1758
1796
  setTimer;
1759
1797
  clearTimer;
1798
+ boundTimer;
1760
1799
  /** The controller's own record, safe to report: times, a normalized class, and observation outcomes. */
1761
1800
  diagnostics() {
1762
1801
  return { checkAt: this.checkAt, decisionAt: this.decisionAt, nextRetryDueAt: this.nextRetryDueAt, blockerClass: this.blockerClass, observations: [...this.observations] };
@@ -1774,7 +1813,7 @@ var AutomaticUpdateController = class {
1774
1813
  if (this.checking || this.waiting || !this.retryable()) return;
1775
1814
  this.checking = true;
1776
1815
  try {
1777
- if (!await this.deps.refreshPolicy() || !this.enabled()) {
1816
+ if (!await this.bounded((signal) => this.deps.refreshPolicy(signal)) || !this.enabled()) {
1778
1817
  this.offline();
1779
1818
  return;
1780
1819
  }
@@ -1835,17 +1874,53 @@ var AutomaticUpdateController = class {
1835
1874
  this.deps.status("failure", { reason: error instanceof Error ? error.message : String(error) });
1836
1875
  this.schedule();
1837
1876
  }
1877
+ /**
1878
+ * One bounded piece of work (#1020): a policy refresh or a facts look. Past {@link OBSERVATION_TIMEOUT_MS} its
1879
+ * signal is aborted (the relay drops that request's response, so a late answer can never restore authority) and
1880
+ * this resolves null — the race has settled, so the late result cannot overwrite a newer one or reach planning,
1881
+ * install or restart. A late rejection is absorbed rather than left unhandled.
1882
+ */
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
+ }
1900
+ }
1838
1901
  async decide() {
1839
- const facts = await this.deps.facts();
1902
+ const facts = await this.bounded((signal) => this.deps.facts(signal));
1840
1903
  this.checkAt = this.now();
1904
+ if (!facts) {
1905
+ this.failures++;
1906
+ this.deps.status("failure", { reason: `The local work and registry check did not answer within ${OBSERVATION_TIMEOUT_MS / 1e3}s; it will be retried.` });
1907
+ this.schedule();
1908
+ return;
1909
+ }
1841
1910
  if (facts.observations?.length) this.observations = [...this.observations, ...facts.observations].slice(-MAX_OBSERVATIONS);
1842
1911
  if (!this.enabled()) return this.offline();
1843
1912
  const plan = this.deps.plan(facts);
1844
1913
  this.decisionAt = this.now();
1845
1914
  this.blockerClass = plan.action === "wait" ? blockerClassFor(plan.waitingFor) : null;
1846
1915
  if (plan.action === "up-to-date") {
1847
- this.failures = 0;
1848
- this.deps.status("verified-success", { latestVersion: facts.latest ?? void 0, reason: void 0 });
1916
+ const verdict = updateVerdict(this.deps.policy().value, facts);
1917
+ if (verdict.kind === "unverified") {
1918
+ this.failures++;
1919
+ this.deps.status("failure", { reason: `Not verified: ${verdict.reason}.` });
1920
+ } else {
1921
+ this.failures = 0;
1922
+ this.deps.status(verdict.kind === "verified" ? "verified-success" : "enabled", { latestVersion: facts.latest ?? void 0, reason: void 0 });
1923
+ }
1849
1924
  this.schedule();
1850
1925
  return;
1851
1926
  }
@@ -1878,7 +1953,7 @@ var AutomaticUpdateController = class {
1878
1953
  void (async () => {
1879
1954
  this.waiting = false;
1880
1955
  try {
1881
- if (!await this.deps.refreshPolicy() || !this.enabled()) {
1956
+ if (!await this.bounded((signal) => this.deps.refreshPolicy(signal)) || !this.enabled()) {
1882
1957
  this.offline();
1883
1958
  return;
1884
1959
  }
@@ -1978,20 +2053,20 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
1978
2053
  };
1979
2054
  if (autoUpdateKey) saveAutoUpdatePolicy(autoUpdateKey, autoUpdatePolicy);
1980
2055
  };
1981
- const acceptAutoUpdatePolicy = (response) => {
1982
- if (!autoUpdateKey) return;
1983
- const policy = policyFromResponse(response);
1984
- if (!policy) return;
1985
- const action = policyScheduleAction(autoUpdatePolicy, policy, autoUpdatePolicyAuthoritative);
1986
- autoUpdatePolicy = { ...autoUpdatePolicy, ...policy };
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;
1987
2062
  autoUpdatePolicyAuthoritative = true;
1988
2063
  if (autoUpdateKey) saveAutoUpdatePolicy(autoUpdateKey, autoUpdatePolicy);
1989
- if (action === "cancel") {
2064
+ if (accepted.action === "cancel") {
1990
2065
  scheduleAutomaticUpdate(false, true);
1991
2066
  return;
1992
2067
  }
1993
2068
  if (refreshingAutomaticPolicy) return;
1994
- if (action === "start") {
2069
+ if (accepted.action === "start") {
1995
2070
  scheduleAutomaticUpdate(true);
1996
2071
  }
1997
2072
  };
@@ -1999,7 +2074,8 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
1999
2074
  const caps = Array.isArray(capabilities.capabilities) ? capabilities.capabilities.filter((item) => typeof item === "string") : [];
2000
2075
  const registered = /* @__PURE__ */ new Set();
2001
2076
  let lastRegisterError = "";
2002
- const registerRuntime = async (id, forceClaim = force) => {
2077
+ const registerRuntime = async (id, forceClaim = force, signal) => {
2078
+ const sent = policyFence.next();
2003
2079
  try {
2004
2080
  const result = await requestPags("POST", `/v1/instances/${apiPathSegment(id)}/runtime`, opts, {
2005
2081
  endpointUrl: localUrl,
@@ -2017,8 +2093,9 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2017
2093
  error: autoUpdatePolicy.reason,
2018
2094
  ...autoUpdateDiagnostics()
2019
2095
  }
2020
- });
2021
- acceptAutoUpdatePolicy(result);
2096
+ }, signal);
2097
+ if (signal?.aborted) return false;
2098
+ acceptAutoUpdatePolicy(result, sent);
2022
2099
  registered.add(id);
2023
2100
  return true;
2024
2101
  } catch (e) {
@@ -2029,13 +2106,13 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2029
2106
  return false;
2030
2107
  }
2031
2108
  };
2032
- const refreshAutomaticPolicy = async () => {
2109
+ const refreshAutomaticPolicy = async (signal) => {
2033
2110
  const id = registered.values().next().value;
2034
2111
  if (!id) return false;
2035
2112
  autoUpdatePolicyAuthoritative = false;
2036
2113
  refreshingAutomaticPolicy = true;
2037
2114
  try {
2038
- return await registerRuntime(id) && autoUpdatePolicyAuthoritative;
2115
+ return await registerRuntime(id, force, signal) && autoUpdatePolicyAuthoritative;
2039
2116
  } finally {
2040
2117
  refreshingAutomaticPolicy = false;
2041
2118
  }
@@ -2052,14 +2129,14 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2052
2129
  writeLine(formatStatusLine({ registration: state, agents, reason: state === "ok" ? void 0 : lastRegisterError }));
2053
2130
  };
2054
2131
  for (const id of instanceIds) await registerRuntime(id);
2055
- const updateFacts = async () => {
2132
+ const updateFacts = async (signal) => {
2056
2133
  const [sessionsResult, healthResult] = await Promise.allSettled([
2057
2134
  requestRunner("GET", "/coding/sessions", {
2058
2135
  url: localUrl,
2059
2136
  token: runnerToken,
2060
2137
  instanceId: instanceIds[0]
2061
- }),
2062
- requestRunner("GET", "/health", { url: localUrl, token: runnerToken, instanceId: instanceIds[0] })
2138
+ }, void 0, signal),
2139
+ requestRunner("GET", "/health", { url: localUrl, token: runnerToken, instanceId: instanceIds[0] }, void 0, signal)
2063
2140
  ]);
2064
2141
  const busy = sessionsResult.status === "fulfilled" && healthResult.status === "fulfilled" ? observedRunnerWork({ sessions: sessionsResult.value.sessions, work: healthResult.value.work }) : ["runner-work-observation-unavailable"];
2065
2142
  const sampledAt = Date.now();
@@ -2071,7 +2148,7 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2071
2148
  return {
2072
2149
  observations,
2073
2150
  current: CLI_VERSION,
2074
- latest: await latestPublishedVersion(),
2151
+ latest: await latestPublishedVersion(void 0, signal),
2075
2152
  fromSource: runsFromSource(),
2076
2153
  restarter: restarterFrom(process.env),
2077
2154
  // A restart ends browser/application work too. Give these durable names to the same
@@ -2388,6 +2465,7 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2388
2465
  });
2389
2466
  for (const id of [...attached.keys()]) {
2390
2467
  try {
2468
+ const sent = policyFence.next();
2391
2469
  const response = await requestPags("POST", `/v1/instances/${apiPathSegment(id)}/runtime/heartbeat`, opts, {
2392
2470
  runnerNode,
2393
2471
  machineId: machine.id,
@@ -2399,7 +2477,7 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2399
2477
  ...autoUpdateDiagnostics()
2400
2478
  }
2401
2479
  });
2402
- acceptAutoUpdatePolicy(response);
2480
+ acceptAutoUpdatePolicy(response, sent);
2403
2481
  } catch (e) {
2404
2482
  failure = e instanceof Error ? e.message : String(e);
2405
2483
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proagentstore/cli",
3
- "version": "0.4.107",
3
+ "version": "0.4.109",
4
4
  "description": "CLI for creating, publishing, and running ProAgentStore agents",
5
5
  "license": "MIT",
6
6
  "type": "module",