@proagentstore/cli 0.4.100 → 0.4.102

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.
@@ -424,8 +424,9 @@ export class HeadlessSession {
424
424
  }
425
425
  if (this.procAlive)
426
426
  return;
427
- // A new process has read none of what the old one was sent.
427
+ // A new process has read none of what the old one was sent, and owns none of its half-read line.
428
428
  this.pendingInputs = [];
429
+ this.buf = "";
429
430
  const args = this.adapter.buildLaunchArgs(this.cmdArgs, this.claudeSessionId);
430
431
  const proc = spawn(this.cmdBin, args, {
431
432
  cwd: this.config.workDir,
@@ -446,8 +447,17 @@ export class HeadlessSession {
446
447
  });
447
448
  // Swallow EPIPE when writing to a process that just exited (one-shot turn).
448
449
  proc.stdin?.on("error", () => { });
449
- proc.stdout?.on("data", (d) => this.onStdout(d.toString("utf8")));
450
+ // FENCED to this process (#1019). A stopped process can outlive its replacement — it may ignore the
451
+ // signal, or flush buffered output on its way out — and its late `result` would otherwise be read as
452
+ // the NEW process's turn end: crediting the new turn's input as consumed, filing its report and
453
+ // marking it idle while it is still working.
454
+ proc.stdout?.on("data", (d) => {
455
+ if (this.proc === proc)
456
+ this.onStdout(d.toString("utf8"));
457
+ });
450
458
  proc.stderr?.on("data", (d) => {
459
+ if (this.proc !== proc)
460
+ return;
451
461
  this.lastOutputAt = Date.now();
452
462
  this.sawOutputSinceInput = true;
453
463
  const text = d.toString("utf8").trim();
@@ -591,6 +601,7 @@ export class HeadlessSession {
591
601
  }
592
602
  }
593
603
  this.proc = proc;
604
+ this.buf = ""; // the replaced turn's half-written line is not this turn's (#1019)
594
605
  // Without an 'error' listener a spawn failure (binary not on PATH) is an uncaught
595
606
  // exception that takes the whole runner down, not just this session.
596
607
  proc.on("error", (err) => {
@@ -604,8 +615,15 @@ export class HeadlessSession {
604
615
  // real reason instead of retrying a binary that does not exist.
605
616
  this.spawnFailed = true;
606
617
  });
607
- proc.stdout?.on("data", (d) => this.onStdout(d.toString()));
608
- proc.stderr?.on("data", (d) => this.onStdout(d.toString()));
618
+ // Fenced like the persistent process (#1019): a turn this one replaced must not speak for it.
619
+ proc.stdout?.on("data", (d) => {
620
+ if (this.proc === proc)
621
+ this.onStdout(d.toString());
622
+ });
623
+ proc.stderr?.on("data", (d) => {
624
+ if (this.proc === proc)
625
+ this.onStdout(d.toString());
626
+ });
609
627
  // The enforced ceiling (#391). `unref` so a pending timer can never keep the runner's
610
628
  // event loop alive past its own shutdown.
611
629
  const maxTurnMs = this.config.maxTurnMs ?? MAX_ONE_SHOT_TURN_MS;
package/dist/index.js CHANGED
@@ -1730,11 +1730,18 @@ function mayAutomaticallyRestart(input) {
1730
1730
  }
1731
1731
 
1732
1732
  // src/commands/runner/auto-update-controller.ts
1733
+ var MAX_OBSERVATIONS = 5;
1734
+ var IDLE_RETRY_MS = 15e3;
1735
+ function blockerClassFor(waitingFor) {
1736
+ if (waitingFor.includes("runner-work-observation-unavailable")) return "observation_failure";
1737
+ return waitingFor.length ? "active_sessions" : "unknown";
1738
+ }
1733
1739
  var AutomaticUpdateController = class {
1734
1740
  constructor(deps) {
1735
1741
  this.deps = deps;
1736
1742
  this.setTimer = deps.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
1737
1743
  this.clearTimer = deps.clearTimer ?? ((timer) => clearTimeout(timer));
1744
+ this.now = deps.now ?? Date.now;
1738
1745
  }
1739
1746
  deps;
1740
1747
  timer = null;
@@ -1742,8 +1749,18 @@ var AutomaticUpdateController = class {
1742
1749
  waiting = false;
1743
1750
  installing = false;
1744
1751
  failures = 0;
1752
+ checkAt = null;
1753
+ decisionAt = null;
1754
+ nextRetryDueAt = null;
1755
+ blockerClass = null;
1756
+ observations = [];
1757
+ now;
1745
1758
  setTimer;
1746
1759
  clearTimer;
1760
+ /** The controller's own record, safe to report: times, a normalized class, and observation outcomes. */
1761
+ diagnostics() {
1762
+ return { checkAt: this.checkAt, decisionAt: this.decisionAt, nextRetryDueAt: this.nextRetryDueAt, blockerClass: this.blockerClass, observations: [...this.observations] };
1763
+ }
1747
1764
  /** Called only for a true policy transition. `keep` deliberately leaves a due heartbeat timer alone. */
1748
1765
  onPolicy(action) {
1749
1766
  if (action === "cancel") return this.cancel();
@@ -1756,10 +1773,10 @@ var AutomaticUpdateController = class {
1756
1773
  this.checking = true;
1757
1774
  try {
1758
1775
  if (!await this.deps.refreshPolicy() || !this.enabled()) {
1759
- this.deps.status("offline", { reason: "The owner policy could not be refreshed; automatic update is deferred." });
1776
+ this.offline();
1760
1777
  return;
1761
1778
  }
1762
- await this.decide(false);
1779
+ await this.decide();
1763
1780
  } catch (error) {
1764
1781
  this.fail(error);
1765
1782
  } finally {
@@ -1779,23 +1796,40 @@ var AutomaticUpdateController = class {
1779
1796
  const delay = initial ? Math.round(5e3 + (this.deps.random ?? Math.random)() * 25e3) : nextAutoUpdateDelayMs(this.failures, this.deps.random);
1780
1797
  this.timer = this.setTimer(() => {
1781
1798
  this.timer = null;
1799
+ this.nextRetryDueAt = null;
1782
1800
  void this.runNow();
1783
1801
  }, delay);
1784
1802
  this.timer.unref?.();
1803
+ this.nextRetryDueAt = this.now() + delay;
1785
1804
  }
1786
1805
  cancel() {
1787
1806
  if (this.timer) this.clearTimer(this.timer);
1788
1807
  this.timer = null;
1808
+ this.nextRetryDueAt = null;
1809
+ }
1810
+ /**
1811
+ * The owner policy could not be re-read, so nothing installs (fail closed) — but the controller keeps
1812
+ * a timer (#1020). It used to report `offline` and stop, leaving an enabled runner with no callback
1813
+ * until the next policy transition. `schedule` itself refuses once the policy is disabled.
1814
+ */
1815
+ offline() {
1816
+ this.failures++;
1817
+ this.deps.status("offline", { reason: "The owner policy could not be refreshed; automatic update is deferred." });
1818
+ this.schedule();
1789
1819
  }
1790
1820
  fail(error) {
1791
1821
  this.failures++;
1792
1822
  this.deps.status("failure", { reason: error instanceof Error ? error.message : String(error) });
1793
1823
  this.schedule();
1794
1824
  }
1795
- async decide(deferred) {
1825
+ async decide() {
1796
1826
  const facts = await this.deps.facts();
1827
+ this.checkAt = this.now();
1828
+ if (facts.observations?.length) this.observations = [...this.observations, ...facts.observations].slice(-MAX_OBSERVATIONS);
1797
1829
  if (!this.enabled()) return;
1798
1830
  const plan = this.deps.plan(facts);
1831
+ this.decisionAt = this.now();
1832
+ this.blockerClass = plan.action === "wait" ? blockerClassFor(plan.waitingFor) : null;
1799
1833
  if (plan.action === "up-to-date") {
1800
1834
  this.failures = 0;
1801
1835
  this.deps.status("verified-success", { latestVersion: facts.latest ?? void 0, reason: void 0 });
@@ -1814,28 +1848,34 @@ var AutomaticUpdateController = class {
1814
1848
  }
1815
1849
  this.deps.status("running", { latestVersion: plan.latest, reason: void 0 });
1816
1850
  const restarted = await this.install(plan, true);
1817
- if (restarted) this.deps.status("restarting", { latestVersion: plan.latest, reason: void 0 });
1818
- else if (!deferred) this.defer(plan.latest);
1851
+ if (restarted) {
1852
+ this.deps.status("restarting", { latestVersion: plan.latest, reason: void 0 });
1853
+ return;
1854
+ }
1855
+ this.blockerClass = "idle_wait";
1856
+ this.defer(plan.latest);
1819
1857
  }
1820
1858
  defer(latest) {
1821
1859
  if (this.waiting) return;
1822
1860
  this.waiting = true;
1823
1861
  this.timer = this.setTimer(() => {
1824
1862
  this.timer = null;
1863
+ this.nextRetryDueAt = null;
1825
1864
  void (async () => {
1826
1865
  this.waiting = false;
1827
1866
  try {
1828
1867
  if (!await this.deps.refreshPolicy() || !this.enabled()) {
1829
- this.deps.status("offline", { reason: "The owner policy could not be refreshed; automatic update is deferred." });
1868
+ this.offline();
1830
1869
  return;
1831
1870
  }
1832
- await this.decide(true);
1871
+ await this.decide();
1833
1872
  } catch (error) {
1834
1873
  this.fail(error);
1835
1874
  }
1836
1875
  })();
1837
- }, 15e3);
1876
+ }, IDLE_RETRY_MS);
1838
1877
  this.timer.unref?.();
1878
+ this.nextRetryDueAt = this.now() + IDLE_RETRY_MS;
1839
1879
  this.deps.status("waiting-for-idle", latest ? { latestVersion: latest } : void 0);
1840
1880
  }
1841
1881
  async install(plan, automatic) {
@@ -1914,6 +1954,7 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
1914
1954
  let autoUpdatePolicyAuthoritative = false;
1915
1955
  let refreshingAutomaticPolicy = false;
1916
1956
  let scheduleAutomaticUpdate = () => void 0;
1957
+ let autoUpdateDiagnostics = () => ({});
1917
1958
  const saveAutoUpdateStatus = (status, extra = {}) => {
1918
1959
  autoUpdatePolicy = {
1919
1960
  ...autoUpdatePolicy,
@@ -1959,7 +2000,8 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
1959
2000
  autoUpdateStatus: {
1960
2001
  status: autoUpdateStatusWire(autoUpdatePolicy.status),
1961
2002
  latestVersion: autoUpdatePolicy.latestVersion,
1962
- error: autoUpdatePolicy.reason
2003
+ error: autoUpdatePolicy.reason,
2004
+ ...autoUpdateDiagnostics()
1963
2005
  }
1964
2006
  });
1965
2007
  acceptAutoUpdatePolicy(result);
@@ -2006,7 +2048,14 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2006
2048
  requestRunner("GET", "/health", { url: localUrl, token: runnerToken, instanceId: instanceIds[0] })
2007
2049
  ]);
2008
2050
  const busy = sessionsResult.status === "fulfilled" && healthResult.status === "fulfilled" ? observedRunnerWork({ sessions: sessionsResult.value.sessions, work: healthResult.value.work }) : ["runner-work-observation-unavailable"];
2051
+ const sampledAt = Date.now();
2052
+ const observations = [
2053
+ // An answer is a success only when it has the shape the guard reads; malformed is a failed look.
2054
+ { source: "sessions", success: sessionsResult.status === "fulfilled" && Array.isArray(sessionsResult.value.sessions), sampledAt },
2055
+ { source: "health", success: healthResult.status === "fulfilled" && !!healthResult.value.work && typeof healthResult.value.work === "object", sampledAt }
2056
+ ];
2009
2057
  return {
2058
+ observations,
2010
2059
  current: CLI_VERSION,
2011
2060
  latest: await latestPublishedVersion(),
2012
2061
  fromSource: runsFromSource(),
@@ -2196,6 +2245,7 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2196
2245
  },
2197
2246
  status: saveAutoUpdateStatus
2198
2247
  });
2248
+ autoUpdateDiagnostics = () => automaticUpdateController.diagnostics();
2199
2249
  scheduleAutomaticUpdate = (initial = false, cancel = false) => automaticUpdateController.onPolicy(cancel ? "cancel" : initial ? "start" : "keep");
2200
2250
  if (autoUpdatePolicyAuthoritative && autoUpdatePolicy.autoUpdate) scheduleAutomaticUpdate(true);
2201
2251
  const answerUpdate = async (body) => {
@@ -2331,7 +2381,8 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2331
2381
  autoUpdateStatus: {
2332
2382
  status: autoUpdateStatusWire(autoUpdatePolicy.status),
2333
2383
  latestVersion: autoUpdatePolicy.latestVersion,
2334
- error: autoUpdatePolicy.reason
2384
+ error: autoUpdatePolicy.reason,
2385
+ ...autoUpdateDiagnostics()
2335
2386
  }
2336
2387
  });
2337
2388
  acceptAutoUpdatePolicy(response);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proagentstore/cli",
3
- "version": "0.4.100",
3
+ "version": "0.4.102",
4
4
  "description": "CLI for creating, publishing, and running ProAgentStore agents",
5
5
  "license": "MIT",
6
6
  "type": "module",