agent-shim 11.62.2 → 11.62.3

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/cli.cjs CHANGED
@@ -36471,7 +36471,6 @@ async function serveFrontDoor(idleShutdownMinutes, ports, options) {
36471
36471
  return 0;
36472
36472
  };
36473
36473
  for (; ; ) {
36474
- watchdog?.beat();
36475
36474
  if (ports.retire?.aborted === true) {
36476
36475
  return await retireByDrain();
36477
36476
  }
@@ -37032,7 +37031,7 @@ var init_package = __esm({
37032
37031
  "package.json"() {
37033
37032
  package_default = {
37034
37033
  name: "agent-shim",
37035
- version: "11.62.2",
37034
+ version: "11.62.3",
37036
37035
  description: "A profile manager and launcher for Claude Code that lets one person run multiple logins from one machine while controlling what gets shared between them.",
37037
37036
  license: "MIT",
37038
37037
  author: "Joseph Mearman <joseph@mearman.co.uk>",
@@ -136861,12 +136860,19 @@ const { workerData, parentPort } = require("node:worker_threads");
136861
136860
  const fs = require("node:fs");
136862
136861
  const os = require("node:os");
136863
136862
  const inspector = require("node:inspector");
136864
- const { shared, boundMs, checkEveryMs, pauseWaitMs, stateFile, ownPid, action, logFile, respawn, respawnGeneration, respawnMaxGeneration, cooldownStepMs, cooldownCapMs, startLockFile, awaitPidEnv, respawnGenerationEnv, delayEnv, nativeSample } = workerData;
136863
+ const { shared, boundMs, checkEveryMs, pauseWaitMs, stateFile, ownPid, action, logFile, respawn, respawnGeneration, respawnMaxGeneration, cooldownStepMs, cooldownCapMs, startLockFile, awaitPidEnv, respawnGenerationEnv, delayEnv, nativeSample, machineLoad } = workerData;
136865
136864
  const counter = new Int32Array(shared);
136866
136865
  let lastValue = Atomics.load(counter, 0);
136867
136866
  let lastChangeAt = Date.now();
136868
136867
  let lastCheckAt = Date.now();
136868
+ // The processor time the machine's load left the main thread since the counter last moved: what the bound is judged against.
136869
+ let scheduledMs = 0;
136870
+ // Set once a silence has outlasted the bound in wall time while the counted time has not, so the wait is logged once and its end is logged too.
136871
+ let waitingOutStarvation = false;
136869
136872
  let stopped = false;
136873
+ const readLoad = () => machineLoad === undefined ? { loadAverage: os.loadavg()[0], cpus: os.cpus().length } : machineLoad;
136874
+ const processorShare = (reading) => reading.loadAverage <= reading.cpus ? 1 : reading.cpus / reading.loadAverage;
136875
+ const describeLoad = (reading) => reading.loadAverage.toFixed(1) + " across " + String(reading.cpus) + " cpus";
136870
136876
  const record = (message) => {
136871
136877
  try {
136872
136878
  const existing = JSON.parse(fs.readFileSync(stateFile, "utf8"));
@@ -136886,6 +136892,7 @@ const log = (line) => {
136886
136892
  // Nowhere left to report a failed log write to; the process still has to end.
136887
136893
  }
136888
136894
  };
136895
+ const MS_PER_SECOND = 1000;
136889
136896
  // Enough of the stack to read the caller chain from the timer or socket callback down to the call that held the loop.
136890
136897
  const STACK_FRAMES_LOGGED = 12;
136891
136898
  // Asks the main thread where it is, through the inspector: a pause request interrupts JavaScript that is running (a loop, or a synchronous call that can be interrupted), so the top frames name what held the loop. A main thread that cannot be paused within the wait is not at such a point (it is idle in the event loop, which means it was not scheduled, or blocked inside a native call), and the answer says so rather than guessing. \`resume\` lets a thread that is not about to be killed run on again.
@@ -136981,22 +136988,37 @@ const spawnReplacement = () => {
136981
136988
  log("could not start a replacement door: " + (error instanceof Error ? error.message : String(error)));
136982
136989
  }
136983
136990
  };
136984
- // Captures where a main thread that could not be paused is held natively, with the platform's own sampler, so a stall the inspector cannot name still leaves a stack to read. The wait is bounded by the sampling time plus its start-up, and the capture never delays the process ending beyond that.
136991
+ // Captures where a main thread that could not be paused is held natively, with the platform's own sampler, so a stall the inspector cannot name still leaves a stack to read. The wait is the sampling time plus the sampler's start-up and symbolication, stretched by the processor share the machine's load leaves it, and the capture never delays the process ending beyond that. The sampler writes its report only once it has finished, so one ended at the deadline, or one that failed, leaves no file, and the log says which rather than naming a file that is not there.
136985
136992
  const takeNativeSample = () => new Promise((resolve) => {
136986
136993
  if (nativeSample === undefined) { resolve(); return; }
136987
136994
  try {
136988
136995
  fs.mkdirSync(nativeSample.directory, { recursive: true });
136989
136996
  const file = require("node:path").join(nativeSample.directory, String(ownPid) + ".txt");
136990
136997
  const args = nativeSample.args.map((arg) => arg.replace("{pid}", String(ownPid)).replace("{seconds}", String(nativeSample.seconds)).replace("{file}", file));
136991
- const child = require("node:child_process").spawn(nativeSample.command, args, { stdio: "ignore" });
136992
- const giveUp = setTimeout(() => { child.kill(); log("the native sampler did not finish in time"); resolve(); }, (nativeSample.seconds + 2) * 1000);
136993
- child.once("error", (error) => { clearTimeout(giveUp); log("could not sample the main thread natively: " + error.message); resolve(); });
136994
- child.once("close", () => {
136998
+ const child = require("node:child_process").spawn(nativeSample.command, args, { stdio: ["ignore", "ignore", "pipe"] });
136999
+ let stderr = "";
137000
+ child.stderr.setEncoding("utf8");
137001
+ child.stderr.on("data", (chunk) => { stderr += chunk; });
137002
+ let settled = false;
137003
+ const settle = (line) => {
137004
+ if (settled) return;
137005
+ settled = true;
136995
137006
  clearTimeout(giveUp);
136996
- log("native stack of the blocked main thread written to " + file);
137007
+ log(line);
137008
+ resolve();
137009
+ };
137010
+ const deadlineMs = Math.ceil((nativeSample.seconds * MS_PER_SECOND + nativeSample.overheadMs) / processorShare(readLoad()));
137011
+ const giveUp = setTimeout(() => { child.kill(); settle("the native sampler did not finish within " + String(deadlineMs) + "ms, so no native stack was written"); }, deadlineMs);
137012
+ child.once("error", (error) => { settle("could not sample the main thread natively: " + error.message); });
137013
+ child.once("close", (code, signal) => {
137014
+ if (code !== 0 || !fs.existsSync(file)) {
137015
+ const detail = stderr.trim();
137016
+ settle("the native sampler " + (signal === null ? "exited with status " + String(code) : "was ended by " + signal) + " and wrote no native stack" + (detail === "" ? "" : ": " + detail));
137017
+ return;
137018
+ }
136997
137019
  const kept = fs.readdirSync(nativeSample.directory).filter((name) => name.endsWith(".txt")).map((name) => ({ name, at: fs.statSync(require("node:path").join(nativeSample.directory, name)).mtimeMs })).sort((a, b) => b.at - a.at);
136998
137020
  for (const old of kept.slice(nativeSample.keep)) fs.rmSync(require("node:path").join(nativeSample.directory, old.name), { force: true });
136999
- resolve();
137021
+ settle("native stack of the blocked main thread written to " + file);
137000
137022
  });
137001
137023
  } catch (error) {
137002
137024
  log("could not sample the main thread natively: " + (error instanceof Error ? error.message : String(error)));
@@ -137010,29 +137032,40 @@ const timer = setInterval(() => {
137010
137032
  lastCheckAt = now;
137011
137033
  const value = Atomics.load(counter, 0);
137012
137034
  if (value !== lastValue || sinceLastCheck > checkEveryMs * 2) {
137035
+ if (waitingOutStarvation && value !== lastValue) log("the front door's event loop ran again after " + String(now - lastChangeAt) + "ms; the machine's load had starved it, so the watchdog kept the door");
137036
+ waitingOutStarvation = false;
137013
137037
  lastValue = value;
137014
137038
  lastChangeAt = now;
137039
+ scheduledMs = 0;
137015
137040
  return;
137016
137041
  }
137017
- if (now - lastChangeAt >= boundMs) {
137018
- const message = "the front door's event loop did not run for " + String(now - lastChangeAt) + "ms; its watchdog ended the process so the next launch starts a fresh door on the same ports";
137019
- stopped = true;
137020
- clearInterval(timer);
137021
- record(message);
137022
- log(message);
137023
- const [oneMinute, fiveMinutes, fifteenMinutes] = os.loadavg();
137024
- log("machine load at the stall: " + oneMinute.toFixed(1) + " (1m), " + fiveMinutes.toFixed(1) + " (5m), " + fifteenMinutes.toFixed(1) + " (15m) across " + String(os.cpus().length) + " cpus");
137025
- spawnReplacement();
137026
- describeMainThread(pauseWaitMs, action !== "kill-process").then(async (where) => {
137027
- log(where);
137028
- if (where.startsWith("the main thread could not be paused")) await takeNativeSample();
137029
- if (action === "kill-process") {
137030
- process.kill(ownPid, "SIGKILL");
137031
- } else {
137032
- parentPort.postMessage({ stalled: true, message });
137033
- }
137034
- });
137042
+ const reading = readLoad();
137043
+ scheduledMs += sinceLastCheck * processorShare(reading);
137044
+ const silentMs = now - lastChangeAt;
137045
+ if (scheduledMs < boundMs) {
137046
+ if (silentMs >= boundMs && !waitingOutStarvation) {
137047
+ waitingOutStarvation = true;
137048
+ log("the front door's event loop has not run for " + String(silentMs) + "ms, but the machine's load (" + describeLoad(reading) + ") left it only " + String(Math.round(scheduledMs)) + "ms of processor time, so the watchdog waits until it has had " + String(boundMs) + "ms rather than replacing a door the machine did not schedule");
137049
+ }
137050
+ return;
137035
137051
  }
137052
+ const message = "the front door's event loop did not run for " + String(silentMs) + "ms, " + String(Math.round(scheduledMs)) + "ms of it with a processor to run on; its watchdog ended the process so the next launch starts a fresh door on the same ports";
137053
+ stopped = true;
137054
+ clearInterval(timer);
137055
+ record(message);
137056
+ log(message);
137057
+ const [oneMinute, fiveMinutes, fifteenMinutes] = os.loadavg();
137058
+ log("machine load at the stall: " + oneMinute.toFixed(1) + " (1m), " + fiveMinutes.toFixed(1) + " (5m), " + fifteenMinutes.toFixed(1) + " (15m) across " + String(os.cpus().length) + " cpus");
137059
+ spawnReplacement();
137060
+ describeMainThread(pauseWaitMs, action !== "kill-process").then(async (where) => {
137061
+ log(where);
137062
+ if (where.startsWith("the main thread could not be paused")) await takeNativeSample();
137063
+ if (action === "kill-process") {
137064
+ process.kill(ownPid, "SIGKILL");
137065
+ } else {
137066
+ parentPort.postMessage({ stalled: true, message });
137067
+ }
137068
+ });
137036
137069
  }, checkEveryMs);
137037
137070
  parentPort.on("message", (message) => {
137038
137071
  if (message === "stop") {
@@ -137047,9 +137080,13 @@ function startThreadWatchdog(params) {
137047
137080
  const counter = new Int32Array(shared);
137048
137081
  const worker = new import_node_worker_threads2.Worker(WATCHDOG_SOURCE, {
137049
137082
  eval: true,
137050
- workerData: { shared, boundMs: params.boundMs, checkEveryMs: params.checkEveryMs, pauseWaitMs: params.pauseWaitMs, stateFile: params.stateFile, ownPid: params.ownPid, action: params.action, logFile: params.logFile, respawn: params.respawn, respawnGeneration: params.respawnGeneration, respawnMaxGeneration: params.respawnMaxGeneration, cooldownStepMs: params.cooldownStepMs, cooldownCapMs: params.cooldownCapMs, startLockFile: params.startLockFile, awaitPidEnv: DOOR_AWAIT_PID_ENV, respawnGenerationEnv: DOOR_RESPAWN_GENERATION_ENV, delayEnv: DOOR_RESPAWN_DELAY_ENV, nativeSample: params.nativeSample }
137083
+ workerData: { shared, boundMs: params.boundMs, checkEveryMs: params.checkEveryMs, pauseWaitMs: params.pauseWaitMs, stateFile: params.stateFile, ownPid: params.ownPid, action: params.action, logFile: params.logFile, respawn: params.respawn, respawnGeneration: params.respawnGeneration, respawnMaxGeneration: params.respawnMaxGeneration, cooldownStepMs: params.cooldownStepMs, cooldownCapMs: params.cooldownCapMs, startLockFile: params.startLockFile, awaitPidEnv: DOOR_AWAIT_PID_ENV, respawnGenerationEnv: DOOR_RESPAWN_GENERATION_ENV, delayEnv: DOOR_RESPAWN_DELAY_ENV, nativeSample: params.nativeSample, machineLoad: params.machineLoad }
137051
137084
  });
137052
137085
  worker.unref();
137086
+ const beat = setInterval(() => {
137087
+ Atomics.add(counter, 0, 1);
137088
+ }, params.checkEveryMs);
137089
+ beat.unref();
137053
137090
  worker.on("message", (message) => {
137054
137091
  if (typeof message === "object" && message !== null && "message" in message && typeof message.message === "string") {
137055
137092
  params.onReport?.(message.message);
@@ -137057,20 +137094,21 @@ function startThreadWatchdog(params) {
137057
137094
  });
137058
137095
  let stopped = false;
137059
137096
  return {
137060
- beat: () => {
137061
- Atomics.add(counter, 0, 1);
137062
- },
137063
137097
  stop: () => {
137064
137098
  if (stopped) {
137065
137099
  return;
137066
137100
  }
137067
137101
  stopped = true;
137102
+ clearInterval(beat);
137068
137103
  worker.postMessage("stop");
137069
137104
  void worker.terminate();
137070
137105
  }
137071
137106
  };
137072
137107
  }
137073
137108
 
137109
+ // src/frontdoor/commands.ts
137110
+ init_probe();
137111
+
137074
137112
  // src/frontdoor/status.ts
137075
137113
  init_socket();
137076
137114
  init_state2();
@@ -137656,7 +137694,7 @@ function realFrontDoorSupervisorPorts(paths, respawnGeneration, retire, trackTun
137656
137694
  cooldownStepMs: DOOR_STALL_BOUND_MS,
137657
137695
  cooldownCapMs: DOOR_RESPAWN_COOLDOWN_CAP_MS,
137658
137696
  startLockFile: paths.frontdoorLockFile,
137659
- ...process.platform === "darwin" ? { nativeSample: { command: MACOS_SAMPLER, args: ["{pid}", "{seconds}", "-file", "{file}"], directory: import_node_path52.default.join(import_node_path52.default.dirname(paths.frontdoorLogPath), "stall-samples"), seconds: Math.max(1, Math.ceil(DOOR_WATCHDOG_CHECK_MS / MS_PER_SECOND16)), keep: DOOR_RESPAWN_MAX_GENERATIONS + 1 } } : {}
137697
+ ...process.platform === "darwin" ? { nativeSample: { command: MACOS_SAMPLER, args: ["{pid}", "{seconds}", "-file", "{file}"], directory: import_node_path52.default.join(import_node_path52.default.dirname(paths.frontdoorLogPath), "stall-samples"), seconds: Math.max(1, Math.ceil(DOOR_WATCHDOG_CHECK_MS / MS_PER_SECOND16)), overheadMs: PROBE_TIMEOUT_MS, keep: DOOR_RESPAWN_MAX_GENERATIONS + 1 } } : {}
137660
137698
  }),
137661
137699
  // The launch lifecycle publisher over the same backbone the typed API serves: the tick's registry facts go in here, and the door's own register, prune and end moments come out as source-tagged events.
137662
137700
  observeLaunchRegistry: createLaunchEventPublisher(doorEvents, () => Date.now()).observe,
package/dist/index.cjs CHANGED
@@ -4135,7 +4135,6 @@ async function serveFrontDoor(idleShutdownMinutes, ports, options) {
4135
4135
  return 0;
4136
4136
  };
4137
4137
  for (; ; ) {
4138
- watchdog?.beat();
4139
4138
  if (ports.retire?.aborted === true) {
4140
4139
  return await retireByDrain();
4141
4140
  }
@@ -4652,7 +4651,7 @@ var init_package = __esm({
4652
4651
  "package.json"() {
4653
4652
  package_default = {
4654
4653
  name: "agent-shim",
4655
- version: "11.62.2",
4654
+ version: "11.62.3",
4656
4655
  description: "A profile manager and launcher for Claude Code that lets one person run multiple logins from one machine while controlling what gets shared between them.",
4657
4656
  license: "MIT",
4658
4657
  author: "Joseph Mearman <joseph@mearman.co.uk>",
package/dist/index.mjs CHANGED
@@ -4112,7 +4112,6 @@ async function serveFrontDoor(idleShutdownMinutes, ports, options) {
4112
4112
  return 0;
4113
4113
  };
4114
4114
  for (; ; ) {
4115
- watchdog?.beat();
4116
4115
  if (ports.retire?.aborted === true) {
4117
4116
  return await retireByDrain();
4118
4117
  }
@@ -4628,7 +4627,7 @@ var init_package = __esm({
4628
4627
  "package.json"() {
4629
4628
  package_default = {
4630
4629
  name: "agent-shim",
4631
- version: "11.62.2",
4630
+ version: "11.62.3",
4632
4631
  description: "A profile manager and launcher for Claude Code that lets one person run multiple logins from one machine while controlling what gets shared between them.",
4633
4632
  license: "MIT",
4634
4633
  author: "Joseph Mearman <joseph@mearman.co.uk>",
@@ -23869,10 +23869,8 @@ declare function ensureFrontDoor(params: {
23869
23869
  readonly token: string;
23870
23870
  };
23871
23871
 
23872
- /** What the supervisor holds: a beat from each tick of its main loop, and a stop when it begins to retire. */
23872
+ /** What the supervisor holds: a stop for when it begins to retire. The beat that tells the watchdog the main event loop ran comes from the watchdog's own timer on that loop (see `startThreadWatchdog`), not from the supervisor's tick, so nothing a tick awaits (a `ps` run a loaded machine is slow to finish) is taken for a loop that did not run. */
23873
23873
  interface DoorWatchdog {
23874
- /** Records that the main event loop ran. Called from the supervisor's tick, which only runs while the loop serves. */
23875
- readonly beat: () => void;
23876
23874
  /** Ends the watchdog before the door begins an orderly retirement, so a slow close is never taken for a stall. Idempotent. */
23877
23875
  readonly stop: () => void;
23878
23876
  }
@@ -23929,7 +23927,7 @@ interface FrontDoorSupervisorPorts {
23929
23927
  */
23930
23928
  readonly closeRcStreamHub: () => void;
23931
23929
  /**
23932
- * Starts the watchdog that ends this process when its main event loop stops running: it lives on its own thread, so a loop that is blocked cannot stall it. The supervisor beats it every tick and stops it before it begins to retire, so it only ever judges a door that is meant to be serving. Optional because ending the process is only right when the process is the door: the CLI's `__frontdoor-supervisor` supplies it, and a library host that runs the door inside its own process does not get its application killed by default.
23930
+ * Starts the watchdog that ends this process when its main event loop stops running: it lives on its own thread, so a loop that is blocked cannot stall it. It beats from its own timer on the main loop rather than from the tick, so a tick waiting on a slow `ps` is never taken for a loop that stopped, and the supervisor stops it before it begins to retire, so it only ever judges a door that is meant to be serving. Optional because ending the process is only right when the process is the door: the CLI's `__frontdoor-supervisor` supplies it, and a library host that runs the door inside its own process does not get its application killed by default.
23933
23931
  */
23934
23932
  readonly startWatchdog?: () => DoorWatchdog;
23935
23933
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-shim",
3
- "version": "11.62.2",
3
+ "version": "11.62.3",
4
4
  "description": "A profile manager and launcher for Claude Code that lets one person run multiple logins from one machine while controlling what gets shared between them.",
5
5
  "license": "MIT",
6
6
  "author": "Joseph Mearman <joseph@mearman.co.uk>",