codeam-cli 2.75.42 → 2.75.44

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.
Files changed (3) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/index.js +154 -15
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,18 @@ All notable changes to `codeam-cli` are documented here.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [2.75.43] — 2026-09-27
8
+
9
+ ### Fixed
10
+
11
+ - **cli:** Stop the EPIPE log loop and retire daemons a restarted host-agent left behind (#851)
12
+
13
+ ## [2.75.42] — 2026-09-27
14
+
15
+ ### Fixed
16
+
17
+ - **cli:** A CodeAgent Box reports its label, not its container id (#849)
18
+
7
19
  ## [2.75.41] — 2026-09-27
8
20
 
9
21
  ### Fixed
package/dist/index.js CHANGED
@@ -3437,6 +3437,18 @@ function resolveLogDir() {
3437
3437
  var LOG_DIR = resolveLogDir();
3438
3438
  var debugFilePath = path.join(LOG_DIR, `debug-${process.pid}.log`);
3439
3439
  var fileInitialized = false;
3440
+ var consoleBroken = false;
3441
+ process.stderr.on("error", () => {
3442
+ consoleBroken = true;
3443
+ });
3444
+ function writeConsole(line) {
3445
+ if (consoleBroken) return;
3446
+ try {
3447
+ process.stderr.write(line);
3448
+ } catch {
3449
+ consoleBroken = true;
3450
+ }
3451
+ }
3440
3452
  var LOG_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
3441
3453
  var LOG_DIR_BUDGET_BYTES = 200 * 1024 * 1024;
3442
3454
  function pidAlive(pid) {
@@ -3556,7 +3568,7 @@ function emit(level, tag, msg, err) {
3556
3568
  appendToFile(jsonMode ? json : `${(/* @__PURE__ */ new Date()).toISOString()} ${text}`);
3557
3569
  }
3558
3570
  if (LEVELS[level] <= currentLevel()) {
3559
- process.stderr.write(jsonMode ? json : text);
3571
+ writeConsole(jsonMode ? json : text);
3560
3572
  }
3561
3573
  }
3562
3574
  var log = {
@@ -10565,7 +10577,7 @@ function readAnonId() {
10565
10577
  }
10566
10578
  function superProperties() {
10567
10579
  return {
10568
- cliVersion: true ? "2.75.42" : "0.0.0-dev",
10580
+ cliVersion: true ? "2.75.44" : "0.0.0-dev",
10569
10581
  nodeVersion: process.version,
10570
10582
  platform: process.platform,
10571
10583
  arch: process.arch,
@@ -10810,7 +10822,7 @@ var http = __toESM(require("http"));
10810
10822
  // package.json
10811
10823
  var package_default = {
10812
10824
  name: "codeam-cli",
10813
- version: "2.75.42",
10825
+ version: "2.75.44",
10814
10826
  description: "Workflow-continuity bridge for AI coding agents. Wrap Claude Code or Codex in a PTY and supervise, approve, and redirect the session from any device \u2014 async. The terminal companion for CodeAgent Mobile.",
10815
10827
  type: "commonjs",
10816
10828
  main: "dist/index.js",
@@ -12144,7 +12156,7 @@ var CommandRelayService = class _CommandRelayService {
12144
12156
  // fresh + clear the "CLI update available" banner after a self-update
12145
12157
  // (a codespace that reinstalls @latest reconnects via heartbeat, not
12146
12158
  // pair/reconnect). Older backends ignore the extra field.
12147
- ..."2.75.42" ? { ideVersion: "2.75.42" } : {}
12159
+ ..."2.75.44" ? { ideVersion: "2.75.44" } : {}
12148
12160
  };
12149
12161
  try {
12150
12162
  const res = await _postJson(`${API_BASE2}/api/plugin/heartbeat`, body);
@@ -27706,13 +27718,13 @@ function resolveGlobalNodeModulesDir(opts) {
27706
27718
  var STALE_STAGING_AGE_MS = CLI_UPDATE_INSTALL_TIMEOUT_MS;
27707
27719
  function sweepStaleCliStagingDirs(nodeModulesDir, now = Date.now(), deps) {
27708
27720
  if (!nodeModulesDir) return 0;
27709
- const readdirSync15 = deps?.readdirSync ?? fs51.readdirSync;
27721
+ const readdirSync16 = deps?.readdirSync ?? fs51.readdirSync;
27710
27722
  const statSync16 = deps?.statSync ?? fs51.statSync;
27711
27723
  const rmSync10 = deps?.rmSync ?? fs51.rmSync;
27712
27724
  let removed = 0;
27713
27725
  let entries;
27714
27726
  try {
27715
- entries = readdirSync15(nodeModulesDir);
27727
+ entries = readdirSync16(nodeModulesDir);
27716
27728
  } catch {
27717
27729
  return 0;
27718
27730
  }
@@ -28532,6 +28544,9 @@ function installRelayCrashGuards() {
28532
28544
  process.on("uncaughtException", (err) => {
28533
28545
  log.error("process", `uncaughtException \u2014 relay kept alive \u2014 ${describeReason(err)}`);
28534
28546
  });
28547
+ process.stdout.on("error", (err) => {
28548
+ if (err.code !== "EPIPE") log.warn("process", `stdout error \u2014 ${describeReason(err)}`);
28549
+ });
28535
28550
  }
28536
28551
  function describeReason(reason) {
28537
28552
  if (reason instanceof Error) {
@@ -28923,6 +28938,59 @@ function releaseOwnLock(lockPath) {
28923
28938
  } catch {
28924
28939
  }
28925
28940
  }
28941
+ function procPpid(pid) {
28942
+ const stat3 = readProcFile(`${pid}/stat`);
28943
+ if (!stat3) return void 0;
28944
+ const ppid = Number(stat3.slice(stat3.lastIndexOf(")") + 2).trim().split(/\s+/)[1]);
28945
+ return Number.isInteger(ppid) ? ppid : void 0;
28946
+ }
28947
+ function orphanedSupervisorDaemon(sessionId) {
28948
+ const rec = readLockRecord(daemonLockPath(sessionId));
28949
+ if (!rec || !isLiveLockHolder(rec)) return void 0;
28950
+ if (process.pid === 1 || procPpid(rec.pid) !== 1) return void 0;
28951
+ const env = readProcFile(`${rec.pid}/environ`)?.split("\0") ?? [];
28952
+ const spawnedBySupervisor = env.some(
28953
+ (v) => v === `CODEAM_RESUME_SESSION_ID=${sessionId}` || v.startsWith("CODEAM_AUTO_TOKEN=")
28954
+ );
28955
+ return spawnedBySupervisor ? rec.pid : void 0;
28956
+ }
28957
+ function snapshotProcessTree(root) {
28958
+ const children = /* @__PURE__ */ new Map();
28959
+ let entries = [];
28960
+ try {
28961
+ entries = fs52.readdirSync(procRoot).filter((e) => /^\d+$/.test(e));
28962
+ } catch {
28963
+ }
28964
+ for (const e of entries) {
28965
+ const pid = Number(e);
28966
+ const ppid = procPpid(pid);
28967
+ if (ppid === void 0) continue;
28968
+ const list = children.get(ppid) ?? [];
28969
+ list.push(pid);
28970
+ children.set(ppid, list);
28971
+ }
28972
+ const tree = [];
28973
+ const queue = [root];
28974
+ const seen2 = /* @__PURE__ */ new Set();
28975
+ while (queue.length > 0) {
28976
+ const pid = queue.shift();
28977
+ if (seen2.has(pid)) continue;
28978
+ seen2.add(pid);
28979
+ const start2 = procStartTicks(pid);
28980
+ tree.push(start2 ? { pid, start: start2 } : { pid });
28981
+ queue.push(...children.get(pid) ?? []);
28982
+ }
28983
+ return tree;
28984
+ }
28985
+ function signalProcessTree(refs, signal) {
28986
+ for (const ref of refs) {
28987
+ if (ref.start !== void 0 && procStartTicks(ref.pid) !== ref.start) continue;
28988
+ try {
28989
+ process.kill(ref.pid, signal);
28990
+ } catch {
28991
+ }
28992
+ }
28993
+ }
28926
28994
  function daemonLockPath(sessionId) {
28927
28995
  const safe = sessionId.replace(/[^a-zA-Z0-9_-]/g, "_");
28928
28996
  return path60.join(os40.homedir(), ".codeam", `daemon-${safe}.lock`);
@@ -48895,7 +48963,7 @@ async function autoUpgradeBeforeCriticalCommand() {
48895
48963
  if (process.env.NODE_ENV === "test") return;
48896
48964
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
48897
48965
  if (process.env.CI) return;
48898
- const current2 = true ? "2.75.42" : null;
48966
+ const current2 = true ? "2.75.44" : null;
48899
48967
  if (!current2) return;
48900
48968
  const cache = readCache();
48901
48969
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
@@ -48912,7 +48980,7 @@ function checkForUpdates() {
48912
48980
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
48913
48981
  if (process.env.CI) return;
48914
48982
  if (!process.stdout.isTTY) return;
48915
- const current2 = true ? "2.75.42" : null;
48983
+ const current2 = true ? "2.75.44" : null;
48916
48984
  if (!current2) return;
48917
48985
  const cache = readCache();
48918
48986
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
@@ -48935,7 +49003,7 @@ var SELF_UPDATE_INSTALL_TIMEOUT_MS = 18e4;
48935
49003
  var SELF_UPDATE_LS_TIMEOUT_MS = 15e3;
48936
49004
  var SUDO_PREFLIGHT_TIMEOUT_MS = 5e3;
48937
49005
  function currentCliVersion() {
48938
- return true ? "2.75.42" : null;
49006
+ return true ? "2.75.44" : null;
48939
49007
  }
48940
49008
  async function installedVersion(deps) {
48941
49009
  const ls = await deps.run(
@@ -49160,6 +49228,11 @@ function buildFleetBoxRunArgs(p2) {
49160
49228
  ...process.env.CODEAM_FLEET_BOX_IMAGE ? [] : ["--pull=always"],
49161
49229
  "--name",
49162
49230
  p2.containerName,
49231
+ // The box's host-agent is the container's pid 1 and its self-update restart
49232
+ // is an exit(0): without a restart policy that stopped the whole box until
49233
+ // the next wake. `docker stop` (the sleep sweep) still keeps it stopped.
49234
+ "--restart",
49235
+ "unless-stopped",
49163
49236
  "--cap-drop",
49164
49237
  "ALL",
49165
49238
  "--security-opt",
@@ -49275,8 +49348,33 @@ var defaultOnIdentityRejected = () => {
49275
49348
  );
49276
49349
  process.exit(1);
49277
49350
  };
49351
+ var ORPHAN_TERM_GRACE_MS = 3e3;
49352
+ var ORPHAN_KILL_SETTLE_MS = 500;
49353
+ function needsSelfRelaunch(env = process.env, pid = process.pid) {
49354
+ return !env.INVOCATION_ID && pid !== 1;
49355
+ }
49356
+ function relaunchArgv(execPath, argv) {
49357
+ return ["-c", 'sleep 2; exec "$0" "$@"', execPath, ...argv.slice(1)];
49358
+ }
49278
49359
  var defaultOnUpdated = (version3) => {
49279
- log.info("host-agent", `self-update: installed ${version3}, restarting`);
49360
+ const relaunch = needsSelfRelaunch();
49361
+ log.info(
49362
+ "host-agent",
49363
+ `self-update: installed ${version3}, restarting${relaunch ? " (self-relaunch, no supervisor)" : ""}`
49364
+ );
49365
+ if (relaunch) {
49366
+ try {
49367
+ (0, import_node_child_process30.spawn)("/bin/sh", relaunchArgv(process.execPath, process.argv), {
49368
+ cwd: process.cwd(),
49369
+ env: process.env,
49370
+ detached: true,
49371
+ stdio: "ignore"
49372
+ }).unref();
49373
+ } catch (err) {
49374
+ log.error("host-agent", "self-update: relaunch failed \u2014 staying on the old binary", err);
49375
+ return;
49376
+ }
49377
+ }
49280
49378
  process.exit(0);
49281
49379
  };
49282
49380
  var HostAgentSupervisor = class {
@@ -49287,6 +49385,9 @@ var HostAgentSupervisor = class {
49287
49385
  this.resumeSpawner = deps.resumeSpawner ?? defaultResumeSpawner;
49288
49386
  this.sessionStore = deps.sessionStore ?? fileSessionChildStore();
49289
49387
  this.listSavedSessions = deps.listSavedSessions ?? (() => loadCliConfig().sessions);
49388
+ this.orphanedDaemonFor = deps.orphanedDaemonFor ?? orphanedSupervisorDaemon;
49389
+ this.snapshotTree = deps.snapshotProcessTree ?? snapshotProcessTree;
49390
+ this.signalTree = deps.signalProcessTree ?? signalProcessTree;
49290
49391
  this.resolveAgentAuth = deps.resolveAgentAuth ?? unsealAgentAuth;
49291
49392
  this.metrics = deps.metricsCollector ?? new MetricsCollector();
49292
49393
  this.onIdentityRejected = deps.onIdentityRejected ?? defaultOnIdentityRejected;
@@ -49341,6 +49442,11 @@ var HostAgentSupervisor = class {
49341
49442
  postResumeFailure;
49342
49443
  sessionStore;
49343
49444
  listSavedSessions;
49445
+ orphanedDaemonFor;
49446
+ snapshotTree;
49447
+ signalTree;
49448
+ /** Resumes waiting for a left-behind daemon to die (see replaceOrphanedDaemon). */
49449
+ pendingOrphanReplacements = 0;
49344
49450
  /**
49345
49451
  * Per-session resume bookkeeping (codeagent-v07a: N sessions resume on
49346
49452
  * boot, each with its OWN bounded retries / exhaustion / re-probe throttle —
@@ -49881,6 +49987,7 @@ var HostAgentSupervisor = class {
49881
49987
  return;
49882
49988
  }
49883
49989
  log.info("host-agent", `fleet_start_box id=${payload.boxId} name=${containerName}`);
49990
+ await this.docker.run(["update", "--restart", "unless-stopped", containerName]);
49884
49991
  const res = await this.docker.run(["start", containerName]);
49885
49992
  if (res.code !== 0 && !isMissingContainerError(res.stderr)) {
49886
49993
  log.warn(
@@ -50211,7 +50318,7 @@ var HostAgentSupervisor = class {
50211
50318
  const records = this.sessionStore.load();
50212
50319
  if (records === null) {
50213
50320
  const target = this.fallbackResumeTarget();
50214
- if (target) this.resumeOne(target);
50321
+ if (target) this.resumeOrReplace(target);
50215
50322
  return;
50216
50323
  }
50217
50324
  if (records.length === 0) return;
@@ -50238,14 +50345,16 @@ var HostAgentSupervisor = class {
50238
50345
  );
50239
50346
  continue;
50240
50347
  }
50241
- this.resumeOne({ deployId: rec.deployId, cwd: rec.cwd, session, agent: rec.agent });
50348
+ this.resumeOrReplace({ deployId: rec.deployId, cwd: rec.cwd, session, agent: rec.agent });
50242
50349
  resumed += 1;
50243
50350
  }
50244
50351
  log.info(
50245
50352
  "host-agent",
50246
50353
  `resume: ${resumed} session(s) resumed, ${dropped.length} ended (bound ${max}), ${resume.length - resumed} skipped`
50247
50354
  );
50248
- if (this.children.size === 0) this.sessionStore.save([]);
50355
+ if (this.children.size === 0 && this.pendingOrphanReplacements === 0) {
50356
+ this.sessionStore.save([]);
50357
+ }
50249
50358
  } catch (err) {
50250
50359
  log.warn(
50251
50360
  "host-agent",
@@ -50352,6 +50461,36 @@ var HostAgentSupervisor = class {
50352
50461
  return [];
50353
50462
  }
50354
50463
  }
50464
+ /**
50465
+ * Resume `target` — first retiring a daemon the PREVIOUS host-agent left
50466
+ * holding the session, if there is one. Without this the resume child
50467
+ * defers to that orphan (it holds the daemon lock and is alive), so the
50468
+ * session keeps running the old code with dead stdio pipes — on 2.75.41 and
50469
+ * older that wedged it offline for good (see `orphanedSupervisorDaemon`).
50470
+ * Replacing it matches what systemd already does on a self-hosted box,
50471
+ * where a unit restart kills the whole control group.
50472
+ */
50473
+ resumeOrReplace(target) {
50474
+ const orphan = this.orphanedDaemonFor(target.session.id);
50475
+ if (orphan === void 0) {
50476
+ this.resumeOne(target);
50477
+ return;
50478
+ }
50479
+ log.warn(
50480
+ "host-agent",
50481
+ `resume: session ${target.session.id.slice(0, 8)} is held by pid ${orphan}, left behind by the previous host-agent \u2014 replacing it`
50482
+ );
50483
+ this.pendingOrphanReplacements += 1;
50484
+ const tree = this.snapshotTree(orphan);
50485
+ this.signalTree(tree.slice(0, 1), "SIGTERM");
50486
+ setTimeout(() => {
50487
+ this.signalTree(tree, "SIGKILL");
50488
+ setTimeout(() => {
50489
+ this.pendingOrphanReplacements -= 1;
50490
+ if (!this.stopped) this.resumeOne(target);
50491
+ }, ORPHAN_KILL_SETTLE_MS);
50492
+ }, ORPHAN_TERM_GRACE_MS);
50493
+ }
50355
50494
  /** Spawn ONE resume child for `target` (boot, retry, or re-probe). */
50356
50495
  resumeOne(target) {
50357
50496
  const { session, deployId } = target;
@@ -50911,7 +51050,7 @@ function checkChokidar() {
50911
51050
  }
50912
51051
  async function doctor(args2 = []) {
50913
51052
  const json = args2.includes("--json");
50914
- const cliVersion = true ? "2.75.42" : "0.0.0-dev";
51053
+ const cliVersion = true ? "2.75.44" : "0.0.0-dev";
50915
51054
  const apiBase2 = resolveApiBaseUrl();
50916
51055
  const diagnosticId = (0, import_node_crypto13.randomUUID)();
50917
51056
  log.info("doctor", `run id=${diagnosticId} cli=${cliVersion}`);
@@ -51435,7 +51574,7 @@ async function mcpWarm(argv = [], opts = {}) {
51435
51574
  // src/commands/version.ts
51436
51575
  var import_picocolors15 = __toESM(require("picocolors"));
51437
51576
  function version2() {
51438
- const v = true ? "2.75.42" : "unknown";
51577
+ const v = true ? "2.75.44" : "unknown";
51439
51578
  console.log(`${import_picocolors15.default.bold("codeam-cli")} ${import_picocolors15.default.cyan(v)}`);
51440
51579
  }
51441
51580
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeam-cli",
3
- "version": "2.75.42",
3
+ "version": "2.75.44",
4
4
  "description": "Workflow-continuity bridge for AI coding agents. Wrap Claude Code or Codex in a PTY and supervise, approve, and redirect the session from any device — async. The terminal companion for CodeAgent Mobile.",
5
5
  "type": "commonjs",
6
6
  "main": "dist/index.js",