@proagentstore/cli 0.4.108 → 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) {
@@ -1732,6 +1735,18 @@ function acceptPolicyResponse(local, wasAuthoritative, response) {
1732
1735
  const ownerTransition = action === "cancel" || !local.autoUpdate;
1733
1736
  return { policy: ownerTransition ? { ...local, ...remote } : { ...local, autoUpdate: remote.autoUpdate }, action };
1734
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
+ };
1735
1750
  function mayAutomaticallyRestart(input) {
1736
1751
  return input.authoritative && input.enabled && input.workObserved && input.busy.length === 0;
1737
1752
  }
@@ -1739,6 +1754,21 @@ function mayAutomaticallyRestart(input) {
1739
1754
  // src/commands/runner/auto-update-controller.ts
1740
1755
  var MAX_OBSERVATIONS = 5;
1741
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
+ }
1742
1772
  function blockerClassFor(waitingFor) {
1743
1773
  if (waitingFor.includes("runner-work-observation-unavailable")) return "observation_failure";
1744
1774
  return waitingFor.length ? "active_sessions" : "unknown";
@@ -1748,6 +1778,7 @@ var AutomaticUpdateController = class {
1748
1778
  this.deps = deps;
1749
1779
  this.setTimer = deps.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
1750
1780
  this.clearTimer = deps.clearTimer ?? ((timer) => clearTimeout(timer));
1781
+ this.boundTimer = deps.boundTimer ?? ((fn, ms) => setTimeout(fn, ms));
1751
1782
  this.now = deps.now ?? Date.now;
1752
1783
  }
1753
1784
  deps;
@@ -1764,6 +1795,7 @@ var AutomaticUpdateController = class {
1764
1795
  now;
1765
1796
  setTimer;
1766
1797
  clearTimer;
1798
+ boundTimer;
1767
1799
  /** The controller's own record, safe to report: times, a normalized class, and observation outcomes. */
1768
1800
  diagnostics() {
1769
1801
  return { checkAt: this.checkAt, decisionAt: this.decisionAt, nextRetryDueAt: this.nextRetryDueAt, blockerClass: this.blockerClass, observations: [...this.observations] };
@@ -1781,7 +1813,7 @@ var AutomaticUpdateController = class {
1781
1813
  if (this.checking || this.waiting || !this.retryable()) return;
1782
1814
  this.checking = true;
1783
1815
  try {
1784
- if (!await this.deps.refreshPolicy() || !this.enabled()) {
1816
+ if (!await this.bounded((signal) => this.deps.refreshPolicy(signal)) || !this.enabled()) {
1785
1817
  this.offline();
1786
1818
  return;
1787
1819
  }
@@ -1842,17 +1874,53 @@ var AutomaticUpdateController = class {
1842
1874
  this.deps.status("failure", { reason: error instanceof Error ? error.message : String(error) });
1843
1875
  this.schedule();
1844
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
+ }
1845
1901
  async decide() {
1846
- const facts = await this.deps.facts();
1902
+ const facts = await this.bounded((signal) => this.deps.facts(signal));
1847
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
+ }
1848
1910
  if (facts.observations?.length) this.observations = [...this.observations, ...facts.observations].slice(-MAX_OBSERVATIONS);
1849
1911
  if (!this.enabled()) return this.offline();
1850
1912
  const plan = this.deps.plan(facts);
1851
1913
  this.decisionAt = this.now();
1852
1914
  this.blockerClass = plan.action === "wait" ? blockerClassFor(plan.waitingFor) : null;
1853
1915
  if (plan.action === "up-to-date") {
1854
- this.failures = 0;
1855
- 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
+ }
1856
1924
  this.schedule();
1857
1925
  return;
1858
1926
  }
@@ -1885,7 +1953,7 @@ var AutomaticUpdateController = class {
1885
1953
  void (async () => {
1886
1954
  this.waiting = false;
1887
1955
  try {
1888
- if (!await this.deps.refreshPolicy() || !this.enabled()) {
1956
+ if (!await this.bounded((signal) => this.deps.refreshPolicy(signal)) || !this.enabled()) {
1889
1957
  this.offline();
1890
1958
  return;
1891
1959
  }
@@ -1985,8 +2053,9 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
1985
2053
  };
1986
2054
  if (autoUpdateKey) saveAutoUpdatePolicy(autoUpdateKey, autoUpdatePolicy);
1987
2055
  };
1988
- const acceptAutoUpdatePolicy = (response) => {
1989
- if (!autoUpdateKey) return;
2056
+ const policyFence = new ResponseFence();
2057
+ const acceptAutoUpdatePolicy = (response, sent) => {
2058
+ if (!autoUpdateKey || !policyFence.accepts(sent)) return;
1990
2059
  const accepted = acceptPolicyResponse(autoUpdatePolicy, autoUpdatePolicyAuthoritative, response);
1991
2060
  if (!accepted) return;
1992
2061
  autoUpdatePolicy = accepted.policy;
@@ -2005,7 +2074,8 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2005
2074
  const caps = Array.isArray(capabilities.capabilities) ? capabilities.capabilities.filter((item) => typeof item === "string") : [];
2006
2075
  const registered = /* @__PURE__ */ new Set();
2007
2076
  let lastRegisterError = "";
2008
- const registerRuntime = async (id, forceClaim = force) => {
2077
+ const registerRuntime = async (id, forceClaim = force, signal) => {
2078
+ const sent = policyFence.next();
2009
2079
  try {
2010
2080
  const result = await requestPags("POST", `/v1/instances/${apiPathSegment(id)}/runtime`, opts, {
2011
2081
  endpointUrl: localUrl,
@@ -2023,8 +2093,9 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2023
2093
  error: autoUpdatePolicy.reason,
2024
2094
  ...autoUpdateDiagnostics()
2025
2095
  }
2026
- });
2027
- acceptAutoUpdatePolicy(result);
2096
+ }, signal);
2097
+ if (signal?.aborted) return false;
2098
+ acceptAutoUpdatePolicy(result, sent);
2028
2099
  registered.add(id);
2029
2100
  return true;
2030
2101
  } catch (e) {
@@ -2035,13 +2106,13 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2035
2106
  return false;
2036
2107
  }
2037
2108
  };
2038
- const refreshAutomaticPolicy = async () => {
2109
+ const refreshAutomaticPolicy = async (signal) => {
2039
2110
  const id = registered.values().next().value;
2040
2111
  if (!id) return false;
2041
2112
  autoUpdatePolicyAuthoritative = false;
2042
2113
  refreshingAutomaticPolicy = true;
2043
2114
  try {
2044
- return await registerRuntime(id) && autoUpdatePolicyAuthoritative;
2115
+ return await registerRuntime(id, force, signal) && autoUpdatePolicyAuthoritative;
2045
2116
  } finally {
2046
2117
  refreshingAutomaticPolicy = false;
2047
2118
  }
@@ -2058,14 +2129,14 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2058
2129
  writeLine(formatStatusLine({ registration: state, agents, reason: state === "ok" ? void 0 : lastRegisterError }));
2059
2130
  };
2060
2131
  for (const id of instanceIds) await registerRuntime(id);
2061
- const updateFacts = async () => {
2132
+ const updateFacts = async (signal) => {
2062
2133
  const [sessionsResult, healthResult] = await Promise.allSettled([
2063
2134
  requestRunner("GET", "/coding/sessions", {
2064
2135
  url: localUrl,
2065
2136
  token: runnerToken,
2066
2137
  instanceId: instanceIds[0]
2067
- }),
2068
- 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)
2069
2140
  ]);
2070
2141
  const busy = sessionsResult.status === "fulfilled" && healthResult.status === "fulfilled" ? observedRunnerWork({ sessions: sessionsResult.value.sessions, work: healthResult.value.work }) : ["runner-work-observation-unavailable"];
2071
2142
  const sampledAt = Date.now();
@@ -2077,7 +2148,7 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2077
2148
  return {
2078
2149
  observations,
2079
2150
  current: CLI_VERSION,
2080
- latest: await latestPublishedVersion(),
2151
+ latest: await latestPublishedVersion(void 0, signal),
2081
2152
  fromSource: runsFromSource(),
2082
2153
  restarter: restarterFrom(process.env),
2083
2154
  // A restart ends browser/application work too. Give these durable names to the same
@@ -2394,6 +2465,7 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2394
2465
  });
2395
2466
  for (const id of [...attached.keys()]) {
2396
2467
  try {
2468
+ const sent = policyFence.next();
2397
2469
  const response = await requestPags("POST", `/v1/instances/${apiPathSegment(id)}/runtime/heartbeat`, opts, {
2398
2470
  runnerNode,
2399
2471
  machineId: machine.id,
@@ -2405,7 +2477,7 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2405
2477
  ...autoUpdateDiagnostics()
2406
2478
  }
2407
2479
  });
2408
- acceptAutoUpdatePolicy(response);
2480
+ acceptAutoUpdatePolicy(response, sent);
2409
2481
  } catch (e) {
2410
2482
  failure = e instanceof Error ? e.message : String(e);
2411
2483
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proagentstore/cli",
3
- "version": "0.4.108",
3
+ "version": "0.4.109",
4
4
  "description": "CLI for creating, publishing, and running ProAgentStore agents",
5
5
  "license": "MIT",
6
6
  "type": "module",