@proagentstore/cli 0.4.94 → 0.4.95

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.
@@ -21,9 +21,9 @@ export class CodingRuntime {
21
21
  * answer rather than restarting across work whose state it cannot prove is idle.
22
22
  */
23
23
  liveWork() {
24
- return [...this.sessions.values()].flatMap((s) => {
24
+ return [...this.sessions.entries()].flatMap(([sessionId, s]) => {
25
25
  const engine = s.clientType ?? "";
26
- const label = engine === "claude" ? "a Claude coding session (resumes after a restart)" : `a ${engine || "coding"} turn (lost on a restart)`;
26
+ const consequence = engine === "claude" ? "Claude conversation resumes after a restart" : `${engine || "coding"} turn is lost on a restart`;
27
27
  try {
28
28
  // `alive: false` is an observed inactive session. Any other answer (including a
29
29
  // malformed older implementation) is unknown and must fail closed below.
@@ -34,13 +34,13 @@ export class CodingRuntime {
34
34
  if (alive === true && state === "idle")
35
35
  return [];
36
36
  if (alive === true && (state === "thinking" || state === "responding"))
37
- return [label];
37
+ return [`coding session ${sessionId} (${state}): ${consequence}`];
38
38
  // An unrecognised `alive`/`runState` pair is not proof that an engine is idle.
39
- return [label];
39
+ return [`coding session ${sessionId} (${typeof state === "string" ? state : "unknown"}): ${consequence}`];
40
40
  }
41
41
  catch {
42
42
  // Do not turn a failed state probe into permission to restart the machine.
43
- return [label];
43
+ return [`coding session ${sessionId} (unknown): ${consequence}`];
44
44
  }
45
45
  });
46
46
  }
@@ -401,6 +401,19 @@ export class LocalArtifactRuntime {
401
401
  return;
402
402
  if (run.timer)
403
403
  clearTimeout(run.timer);
404
+ // An uploaded source is a bounded, per-run transfer, not runner state. The durable
405
+ // result deliberately contains only its handle and hash; remove the materialized text as
406
+ // soon as the engine no longer needs it instead of retaining it with generic scratch data.
407
+ // `end` is the one terminal path (completion, refusal, timeout, cancellation and launch
408
+ // failure), so a source cannot linger just because a particular failure path was missed.
409
+ if (run.envelope.uploadedSources?.length) {
410
+ try {
411
+ rmSync(join(run.scratch, "sources"), { recursive: true, force: true });
412
+ }
413
+ catch { /* best-effort cleanup */ }
414
+ for (const source of run.envelope.uploadedSources)
415
+ source.text = "";
416
+ }
404
417
  run.state = "ended";
405
418
  run.endedAt = this.now();
406
419
  run.result = {
package/dist/index.js CHANGED
@@ -1269,6 +1269,19 @@ function restarterFrom(env) {
1269
1269
  function supervisorNote(restarter) {
1270
1270
  return restarter === "pags-up-child-only" ? "The `pags up` on this machine predates supervisor restarts (#860): the runner comes back on the new release, but the `pags up` window keeps running its own older code until it is restarted there once." : null;
1271
1271
  }
1272
+ var MAX_DEFERRED_UPDATE_LEASE_MS = 5 * 6e4;
1273
+ function deferredUpdateLease(value, now = Date.now()) {
1274
+ if (!value || typeof value !== "object") return null;
1275
+ const raw = value;
1276
+ const operationId = typeof raw.operationId === "string" ? raw.operationId.trim() : "";
1277
+ const expiresAt = typeof raw.deferUntil === "number" ? raw.deferUntil : NaN;
1278
+ if (!operationId || operationId.length > 128 || !Number.isFinite(expiresAt)) return null;
1279
+ if (expiresAt <= now || expiresAt > now + MAX_DEFERRED_UPDATE_LEASE_MS) return null;
1280
+ return { operationId, expiresAt };
1281
+ }
1282
+ function deferredUpdateLeaseActive(lease, now = Date.now()) {
1283
+ return now < lease.expiresAt;
1284
+ }
1272
1285
  function leaveForRestart(restarter, deps = { spawn: spawn3, exit: (code) => process.exit(code) }) {
1273
1286
  if (restarter === "command") {
1274
1287
  deps.spawn(process.env[RESTART_COMMAND_ENV], { shell: true, detached: true, stdio: "ignore", env: process.env }).unref();
@@ -1864,6 +1877,29 @@ var RelayMutationAdmission = class {
1864
1877
 
1865
1878
  // src/commands/runner/relay.ts
1866
1879
  var MEMBERSHIP_SYNC_PATH = "/pags/membership/sync";
1880
+ function observedRunnerWork(observation) {
1881
+ const sessions = observation.sessions;
1882
+ const work = observation.work;
1883
+ if (!Array.isArray(sessions) || !work || typeof work !== "object") return ["runner-work-observation-unavailable"];
1884
+ const codingTurns = typeof work.codingTurns === "number" && Number.isInteger(work.codingTurns) && work.codingTurns >= 0 ? work.codingTurns : null;
1885
+ const localRuns = typeof work.localRuns === "number" && Number.isInteger(work.localRuns) && work.localRuns >= 0 ? work.localRuns : null;
1886
+ const detail = Array.isArray(work.detail) && work.detail.every((item) => typeof item === "string") ? work.detail : null;
1887
+ if (codingTurns === null || localRuns === null || detail === null) return ["runner-work-observation-unavailable"];
1888
+ const activeCoding = sessions.flatMap((session) => {
1889
+ if (session?.alive === false) return [];
1890
+ const id = typeof session?.sessionId === "string" && session.sessionId ? session.sessionId : "unidentified-session";
1891
+ const state = typeof session?.runState === "string" && session.runState ? session.runState : "unknown";
1892
+ if (session?.alive === true && state === "idle") return [];
1893
+ return [`coding session ${id} (${state})`];
1894
+ });
1895
+ const unmatchedCodingTurns = Math.max(0, codingTurns - activeCoding.length);
1896
+ const localDetail = detail.slice(codingTurns);
1897
+ return [
1898
+ ...activeCoding,
1899
+ ...Array.from({ length: unmatchedCodingTurns }, (_v, i) => `coding-turn-unmatched-${i + 1}`),
1900
+ ...Array.from({ length: localRuns }, (_v, i) => localDetail[i] || `local-run-${i + 1}`)
1901
+ ];
1902
+ }
1867
1903
  var CLI_CONTROL_PATHS = /* @__PURE__ */ new Set([MEMBERSHIP_SYNC_PATH, RUNNER_UPDATE_PATH]);
1868
1904
  async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force = false, watchInstances = false) {
1869
1905
  const apiBase = pagsApiBase(opts.apiBase).replace(/^http/, "ws");
@@ -1967,14 +2003,7 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
1967
2003
  }),
1968
2004
  requestRunner("GET", "/health", { url: localUrl, token: runnerToken, instanceId: instanceIds[0] })
1969
2005
  ]);
1970
- const observationsUnavailable = sessionsResult.status !== "fulfilled" || healthResult.status !== "fulfilled" || !healthResult.value.work;
1971
- const sessions = sessionsResult.status === "fulfilled" ? sessionsResult.value : { sessions: [] };
1972
- const health = healthResult.status === "fulfilled" ? healthResult.value : {};
1973
- const activeCoding = (sessions.sessions ?? []).filter((s) => s.alive !== false && (s.alive !== true || !["idle", "thinking", "responding"].includes(s.runState ?? "") || s.runState !== "idle")).map((s) => s.sessionId || "coding-session-state-unavailable");
1974
- const localRuns = health.work?.localRuns ?? 0;
1975
- const codingTurns = health.work?.codingTurns ?? 0;
1976
- const unmatchedCodingTurns = Math.max(0, codingTurns - activeCoding.length);
1977
- const localDetail = health.work?.detail?.slice(codingTurns) ?? [];
2006
+ const busy = sessionsResult.status === "fulfilled" && healthResult.status === "fulfilled" ? observedRunnerWork({ sessions: sessionsResult.value.sessions, work: healthResult.value.work }) : ["runner-work-observation-unavailable"];
1978
2007
  return {
1979
2008
  current: CLI_VERSION,
1980
2009
  latest: await latestPublishedVersion(),
@@ -1982,14 +2011,14 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
1982
2011
  restarter: restarterFrom(process.env),
1983
2012
  // A restart ends browser/application work too. Give these durable names to the same
1984
2013
  // planner so neither manual nor automatic update can mistake them for an idle machine.
1985
- busy: observationsUnavailable ? ["runner-work-observation-unavailable"] : [...activeCoding, ...Array.from({ length: unmatchedCodingTurns }, (_v, i) => `coding-turn-unmatched-${i + 1}`), ...Array.from({ length: localRuns }, (_v, i) => localDetail[i] || `local-run-${i + 1}`)]
2014
+ busy
1986
2015
  };
1987
2016
  };
1988
2017
  let updateInstallInFlight = false;
1989
2018
  let installedPendingRestart = null;
1990
2019
  const relayMutationAdmission = new RelayMutationAdmission();
1991
- const installAndRestart = async (plan, automatic = false) => {
1992
- if (updateInstallInFlight) return false;
2020
+ const installAndRestart = async (plan, automatic = false, lease) => {
2021
+ if (updateInstallInFlight || lease && !deferredUpdateLeaseActive(lease)) return false;
1993
2022
  updateInstallInFlight = true;
1994
2023
  if (automatic && (!await refreshAutomaticPolicy() || !autoUpdatePolicy.autoUpdate)) {
1995
2024
  updateInstallInFlight = false;
@@ -2005,9 +2034,14 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2005
2034
  throw error;
2006
2035
  }
2007
2036
  }
2037
+ await relayMutationAdmission.drain();
2038
+ await new Promise((resolve5) => setTimeout(resolve5, 500));
2039
+ if (lease && !deferredUpdateLeaseActive(lease)) {
2040
+ relayMutationAdmission.resume();
2041
+ updateInstallInFlight = false;
2042
+ return false;
2043
+ }
2008
2044
  if (automatic) {
2009
- await relayMutationAdmission.drain();
2010
- await new Promise((resolve5) => setTimeout(resolve5, 500));
2011
2045
  if (!await refreshAutomaticPolicy() || !autoUpdatePolicy.autoUpdate) {
2012
2046
  relayMutationAdmission.resume();
2013
2047
  updateInstallInFlight = false;
@@ -2022,15 +2056,22 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2022
2056
  }
2023
2057
  writeLine(`Installed ${plan.latest} \u2014 restarting; every agent re-attaches on the way back up.`);
2024
2058
  for (const id of [...attached.keys()]) detach(id);
2025
- leaveForRestart(plan.restarter);
2026
2059
  return true;
2027
2060
  };
2028
2061
  let updateWaiting = false;
2029
- const updateWhenIdle = (automatic = false) => {
2030
- if (updateWaiting) return;
2062
+ let deferredManualLease = null;
2063
+ const updateWhenIdle = (automatic = false, lease) => {
2064
+ if (updateWaiting || !automatic && !lease) return false;
2031
2065
  updateWaiting = true;
2032
- const until = Date.now() + 60 * 6e4;
2066
+ if (!automatic) deferredManualLease = lease;
2067
+ const until = automatic ? Date.now() + 60 * 6e4 : lease.expiresAt;
2033
2068
  const tick = () => setTimeout(async () => {
2069
+ if (!automatic && (!deferredManualLease || !deferredUpdateLeaseActive(deferredManualLease))) {
2070
+ writeLine("runner_update: the deferred operation lease expired; no delayed restart was performed.");
2071
+ deferredManualLease = null;
2072
+ updateWaiting = false;
2073
+ return;
2074
+ }
2034
2075
  if (automatic && (!await refreshAutomaticPolicy() || !autoUpdatePolicy.autoUpdate)) {
2035
2076
  saveAutoUpdateStatus("offline", { reason: "The owner policy could not be refreshed; automatic update is deferred." });
2036
2077
  updateWaiting = false;
@@ -2043,24 +2084,30 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2043
2084
  return;
2044
2085
  }
2045
2086
  if (automatic) saveAutoUpdateStatus("running", { latestVersion: plan.latest, reason: void 0 });
2046
- const installed = await installAndRestart(plan, automatic).catch((e) => {
2087
+ const installed = await installAndRestart(plan, automatic, automatic ? void 0 : deferredManualLease ?? void 0).catch((e) => {
2047
2088
  if (automatic) saveAutoUpdateStatus("failure", { reason: e instanceof Error ? e.message : String(e) });
2048
2089
  writeError(`runner_update: install failed \u2014 ${e instanceof Error ? e.message : String(e)}`);
2049
2090
  return false;
2050
2091
  });
2051
- if (automatic && !installed && Date.now() < until) {
2092
+ if (!installed && Date.now() < until) {
2052
2093
  saveAutoUpdateStatus("waiting-for-idle", { latestVersion: plan.latest });
2053
2094
  tick();
2054
- } else updateWaiting = false;
2095
+ } else {
2096
+ deferredManualLease = null;
2097
+ updateWaiting = false;
2098
+ if (installed) leaveForRestart(plan.restarter);
2099
+ }
2055
2100
  } else if (plan.action === "wait" && Date.now() < until) {
2056
2101
  if (automatic) saveAutoUpdateStatus("waiting-for-idle", { latestVersion: plan.latest, reason: void 0 });
2057
2102
  tick();
2058
2103
  } else {
2059
2104
  if (automatic && plan.action === "refused") saveAutoUpdateStatus("unsupported", { reason: plan.reason });
2105
+ deferredManualLease = null;
2060
2106
  updateWaiting = false;
2061
2107
  }
2062
2108
  }, 15e3).unref();
2063
2109
  tick();
2110
+ return true;
2064
2111
  };
2065
2112
  let automaticTimer = null;
2066
2113
  let automaticChecking = false;
@@ -2140,7 +2187,11 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2140
2187
  plan: planRunnerUpdate,
2141
2188
  // This bridge is deliberately the same function used by explicit runner_update. It owns npm,
2142
2189
  // a fresh authoritative policy, the mutation drain, final local-work observation and restart.
2143
- installAndRestart,
2190
+ installAndRestart: async (plan, automatic) => {
2191
+ const installed = await installAndRestart(plan, automatic);
2192
+ if (installed) setTimeout(() => leaveForRestart(plan.restarter), 0).unref();
2193
+ return installed;
2194
+ },
2144
2195
  status: saveAutoUpdateStatus
2145
2196
  });
2146
2197
  scheduleAutomaticUpdate = (initial = false, cancel = false) => automaticUpdateController.onPolicy(cancel ? "cancel" : initial ? "start" : "keep");
@@ -2150,15 +2201,18 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2150
2201
  const plan = planRunnerUpdate(await updateFacts());
2151
2202
  if (dryRun || plan.action === "up-to-date" || plan.action === "refused") return { status: 200, result: { ...plan, dryRun } };
2152
2203
  if (plan.action === "wait") {
2153
- updateWhenIdle();
2154
- return { status: 200, result: { ...plan, detail: "Restarts itself as soon as these engines finish their turns \u2014 no run is cut off." } };
2204
+ const lease = deferredUpdateLease(body);
2205
+ if (!lease) return { status: 400, result: { error: "A busy runner_update needs a current operation lease; no delayed restart was scheduled." } };
2206
+ if (!updateWhenIdle(false, lease)) return { status: 409, result: { error: "A runner update is already waiting for active work to finish." } };
2207
+ 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." } };
2155
2208
  }
2156
2209
  try {
2157
- if (!await automaticUpdateController.installManually(plan)) return { status: 409, result: { error: "A runner update is already installing or restarting." } };
2210
+ if (!await installAndRestart(plan)) return { status: 409, result: { error: "A runner update is already installing or restarting." } };
2158
2211
  } catch (e) {
2159
2212
  return { status: 500, result: { error: `npm could not install ${plan.latest}: ${e instanceof Error ? e.message : String(e)}` } };
2160
2213
  }
2161
2214
  const supervisor = supervisorNote(plan.restarter);
2215
+ setTimeout(() => leaveForRestart(plan.restarter), 25).unref();
2162
2216
  return { status: 200, result: { action: "restarting", current: plan.current, latest: plan.latest, restartedBy: plan.restarter, ...supervisor ? { supervisor } : {} } };
2163
2217
  };
2164
2218
  const answerControl = async (path, body) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proagentstore/cli",
3
- "version": "0.4.94",
3
+ "version": "0.4.95",
4
4
  "description": "CLI for creating, publishing, and running ProAgentStore agents",
5
5
  "license": "MIT",
6
6
  "type": "module",