@proagentstore/cli 0.4.93 → 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.
@@ -13,16 +13,35 @@ export class CodingRuntime {
13
13
  reposBaseDir;
14
14
  sessions = new Map();
15
15
  /**
16
- * Coding sessions that are running here (#896).
16
+ * Coding turns that are running here (#896, #1007).
17
17
  *
18
- * A Claude session survives a restart — it is re-spawned with `--resume <session_id>` and only
19
- * the turn in flight is cut — but a Codex, Grok or raw-engine session restarts cold and its turn
20
- * is gone. Both are named, because the owner is the one deciding whether to take the machine.
18
+ * A retained session is a conversation, not necessarily work in flight: a healthy engine remains
19
+ * available at `runState: "idle"` between turns. Runner updates must protect only turns that can
20
+ * be interrupted. If this runtime cannot observe a session's state, retain the conservative busy
21
+ * answer rather than restarting across work whose state it cannot prove is idle.
21
22
  */
22
23
  liveWork() {
23
- return [...this.sessions.values()].map((s) => {
24
+ return [...this.sessions.entries()].flatMap(([sessionId, s]) => {
24
25
  const engine = s.clientType ?? "";
25
- return 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
+ try {
28
+ // `alive: false` is an observed inactive session. Any other answer (including a
29
+ // malformed older implementation) is unknown and must fail closed below.
30
+ const alive = s.alive;
31
+ if (alive === false)
32
+ return [];
33
+ const state = s.runState();
34
+ if (alive === true && state === "idle")
35
+ return [];
36
+ if (alive === true && (state === "thinking" || state === "responding"))
37
+ return [`coding session ${sessionId} (${state}): ${consequence}`];
38
+ // An unrecognised `alive`/`runState` pair is not proof that an engine is idle.
39
+ return [`coding session ${sessionId} (${typeof state === "string" ? state : "unknown"}): ${consequence}`];
40
+ }
41
+ catch {
42
+ // Do not turn a failed state probe into permission to restart the machine.
43
+ return [`coding session ${sessionId} (unknown): ${consequence}`];
44
+ }
26
45
  });
27
46
  }
28
47
  /** Background cold-start clones, one per folder (#858). */
@@ -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,12 +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 && s.runState && s.runState !== "idle").map((s) => s.sessionId);
1974
- const localRuns = health.work?.localRuns ?? 0;
1975
- const codingTurns = health.work?.codingTurns ?? 0;
2006
+ const busy = sessionsResult.status === "fulfilled" && healthResult.status === "fulfilled" ? observedRunnerWork({ sessions: sessionsResult.value.sessions, work: healthResult.value.work }) : ["runner-work-observation-unavailable"];
1976
2007
  return {
1977
2008
  current: CLI_VERSION,
1978
2009
  latest: await latestPublishedVersion(),
@@ -1980,14 +2011,14 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
1980
2011
  restarter: restarterFrom(process.env),
1981
2012
  // A restart ends browser/application work too. Give these durable names to the same
1982
2013
  // planner so neither manual nor automatic update can mistake them for an idle machine.
1983
- busy: observationsUnavailable ? ["runner-work-observation-unavailable"] : [...activeCoding, ...Array.from({ length: codingTurns }, (_v, i) => `coding-turn-${i + 1}`), ...Array.from({ length: localRuns }, (_v, i) => health.work?.detail?.[i] || `local-run-${i + 1}`)]
2014
+ busy
1984
2015
  };
1985
2016
  };
1986
2017
  let updateInstallInFlight = false;
1987
2018
  let installedPendingRestart = null;
1988
2019
  const relayMutationAdmission = new RelayMutationAdmission();
1989
- const installAndRestart = async (plan, automatic = false) => {
1990
- if (updateInstallInFlight) return false;
2020
+ const installAndRestart = async (plan, automatic = false, lease) => {
2021
+ if (updateInstallInFlight || lease && !deferredUpdateLeaseActive(lease)) return false;
1991
2022
  updateInstallInFlight = true;
1992
2023
  if (automatic && (!await refreshAutomaticPolicy() || !autoUpdatePolicy.autoUpdate)) {
1993
2024
  updateInstallInFlight = false;
@@ -2003,9 +2034,14 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2003
2034
  throw error;
2004
2035
  }
2005
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
+ }
2006
2044
  if (automatic) {
2007
- await relayMutationAdmission.drain();
2008
- await new Promise((resolve5) => setTimeout(resolve5, 500));
2009
2045
  if (!await refreshAutomaticPolicy() || !autoUpdatePolicy.autoUpdate) {
2010
2046
  relayMutationAdmission.resume();
2011
2047
  updateInstallInFlight = false;
@@ -2020,15 +2056,22 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2020
2056
  }
2021
2057
  writeLine(`Installed ${plan.latest} \u2014 restarting; every agent re-attaches on the way back up.`);
2022
2058
  for (const id of [...attached.keys()]) detach(id);
2023
- leaveForRestart(plan.restarter);
2024
2059
  return true;
2025
2060
  };
2026
2061
  let updateWaiting = false;
2027
- const updateWhenIdle = (automatic = false) => {
2028
- if (updateWaiting) return;
2062
+ let deferredManualLease = null;
2063
+ const updateWhenIdle = (automatic = false, lease) => {
2064
+ if (updateWaiting || !automatic && !lease) return false;
2029
2065
  updateWaiting = true;
2030
- const until = Date.now() + 60 * 6e4;
2066
+ if (!automatic) deferredManualLease = lease;
2067
+ const until = automatic ? Date.now() + 60 * 6e4 : lease.expiresAt;
2031
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
+ }
2032
2075
  if (automatic && (!await refreshAutomaticPolicy() || !autoUpdatePolicy.autoUpdate)) {
2033
2076
  saveAutoUpdateStatus("offline", { reason: "The owner policy could not be refreshed; automatic update is deferred." });
2034
2077
  updateWaiting = false;
@@ -2041,24 +2084,30 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2041
2084
  return;
2042
2085
  }
2043
2086
  if (automatic) saveAutoUpdateStatus("running", { latestVersion: plan.latest, reason: void 0 });
2044
- const installed = await installAndRestart(plan, automatic).catch((e) => {
2087
+ const installed = await installAndRestart(plan, automatic, automatic ? void 0 : deferredManualLease ?? void 0).catch((e) => {
2045
2088
  if (automatic) saveAutoUpdateStatus("failure", { reason: e instanceof Error ? e.message : String(e) });
2046
2089
  writeError(`runner_update: install failed \u2014 ${e instanceof Error ? e.message : String(e)}`);
2047
2090
  return false;
2048
2091
  });
2049
- if (automatic && !installed && Date.now() < until) {
2092
+ if (!installed && Date.now() < until) {
2050
2093
  saveAutoUpdateStatus("waiting-for-idle", { latestVersion: plan.latest });
2051
2094
  tick();
2052
- } else updateWaiting = false;
2095
+ } else {
2096
+ deferredManualLease = null;
2097
+ updateWaiting = false;
2098
+ if (installed) leaveForRestart(plan.restarter);
2099
+ }
2053
2100
  } else if (plan.action === "wait" && Date.now() < until) {
2054
2101
  if (automatic) saveAutoUpdateStatus("waiting-for-idle", { latestVersion: plan.latest, reason: void 0 });
2055
2102
  tick();
2056
2103
  } else {
2057
2104
  if (automatic && plan.action === "refused") saveAutoUpdateStatus("unsupported", { reason: plan.reason });
2105
+ deferredManualLease = null;
2058
2106
  updateWaiting = false;
2059
2107
  }
2060
2108
  }, 15e3).unref();
2061
2109
  tick();
2110
+ return true;
2062
2111
  };
2063
2112
  let automaticTimer = null;
2064
2113
  let automaticChecking = false;
@@ -2138,7 +2187,11 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2138
2187
  plan: planRunnerUpdate,
2139
2188
  // This bridge is deliberately the same function used by explicit runner_update. It owns npm,
2140
2189
  // a fresh authoritative policy, the mutation drain, final local-work observation and restart.
2141
- 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
+ },
2142
2195
  status: saveAutoUpdateStatus
2143
2196
  });
2144
2197
  scheduleAutomaticUpdate = (initial = false, cancel = false) => automaticUpdateController.onPolicy(cancel ? "cancel" : initial ? "start" : "keep");
@@ -2148,15 +2201,18 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2148
2201
  const plan = planRunnerUpdate(await updateFacts());
2149
2202
  if (dryRun || plan.action === "up-to-date" || plan.action === "refused") return { status: 200, result: { ...plan, dryRun } };
2150
2203
  if (plan.action === "wait") {
2151
- updateWhenIdle();
2152
- 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." } };
2153
2208
  }
2154
2209
  try {
2155
- 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." } };
2156
2211
  } catch (e) {
2157
2212
  return { status: 500, result: { error: `npm could not install ${plan.latest}: ${e instanceof Error ? e.message : String(e)}` } };
2158
2213
  }
2159
2214
  const supervisor = supervisorNote(plan.restarter);
2215
+ setTimeout(() => leaveForRestart(plan.restarter), 25).unref();
2160
2216
  return { status: 200, result: { action: "restarting", current: plan.current, latest: plan.latest, restartedBy: plan.restarter, ...supervisor ? { supervisor } : {} } };
2161
2217
  };
2162
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.93",
3
+ "version": "0.4.95",
4
4
  "description": "CLI for creating, publishing, and running ProAgentStore agents",
5
5
  "license": "MIT",
6
6
  "type": "module",