codeam-cli 2.23.2 → 2.23.4

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 +114 -18
  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.23.3] — 2026-05-25
8
+
9
+ ### Fixed
10
+
11
+ - **cli:** Reap in-flight spawnAndCapture children on sigint/exit (#191)
12
+
13
+ ## [2.23.2] — 2026-05-25
14
+
15
+ ### Fixed
16
+
17
+ - **cli:** Fire-and-forget background handlers so they don't block start_task (#190)
18
+
7
19
  ## [2.23.1] — 2026-05-25
8
20
 
9
21
  ### Fixed
package/dist/index.js CHANGED
@@ -441,7 +441,7 @@ var import_qrcode_terminal = __toESM(require("qrcode-terminal"));
441
441
  // package.json
442
442
  var package_default = {
443
443
  name: "codeam-cli",
444
- version: "2.23.2",
444
+ version: "2.23.4",
445
445
  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.",
446
446
  type: "commonjs",
447
447
  main: "dist/index.js",
@@ -5768,7 +5768,7 @@ function readAnonId() {
5768
5768
  }
5769
5769
  function superProperties() {
5770
5770
  return {
5771
- cliVersion: true ? "2.23.2" : "0.0.0-dev",
5771
+ cliVersion: true ? "2.23.4" : "0.0.0-dev",
5772
5772
  nodeVersion: process.version,
5773
5773
  platform: process.platform,
5774
5774
  arch: process.arch,
@@ -5781,7 +5781,7 @@ function initTelemetry() {
5781
5781
  log.trace("telemetry", "opted out (CODEAM_TELEMETRY / CI / NODE_ENV=test)");
5782
5782
  return false;
5783
5783
  }
5784
- const apiKey = true ? "" : "";
5784
+ const apiKey = true ? "phc_Otx6LUf0KeabkbTnPuLPLg6d6r8319k8bKDYqoXyeb4" : "";
5785
5785
  if (!apiKey) {
5786
5786
  log.trace("telemetry", "no PostHog API key baked into build \u2014 disabled");
5787
5787
  return false;
@@ -5860,6 +5860,8 @@ function maybePrintFirstRunBanner() {
5860
5860
 
5861
5861
  // src/services/command-relay.service.ts
5862
5862
  var API_BASE2 = resolveApiBaseUrl();
5863
+ var SSE_LIVENESS_TIMEOUT_MS = 45e3;
5864
+ var SSE_WATCHDOG_INTERVAL_MS = 1e4;
5863
5865
  var CommandRelayService = class {
5864
5866
  constructor(pluginId, onCommand, agentMeta) {
5865
5867
  this.pluginId = pluginId;
@@ -5889,10 +5891,25 @@ var CommandRelayService = class {
5889
5891
  /** Reconnect backoff state for the SSE stream. */
5890
5892
  sseFailures = 0;
5891
5893
  sseReconnectTimer = null;
5894
+ /**
5895
+ * Liveness watchdog for the SSE socket. The server pings every 30 s
5896
+ * (`pending-stream.controller.ts` interval(30_000)). If we go more
5897
+ * than `SSE_LIVENESS_TIMEOUT_MS` without ANY bytes, we assume the
5898
+ * TCP peer died half-open (Cloud Run scaled / redeployed mid-stream,
5899
+ * NAT dropped the conntrack entry, laptop slept) and force a
5900
+ * reconnect. Without this the kernel can keep the socket in a
5901
+ * zombie ESTABLISHED state for HOURS before the default TCP
5902
+ * keepalive triggers — observed live in #190 where a 17:18 redeploy
5903
+ * left CLIs subscribed for >30 minutes without ever receiving a
5904
+ * pushed command.
5905
+ */
5906
+ sseWatchdog = null;
5907
+ sseLastByteAt = 0;
5892
5908
  start() {
5893
5909
  this.cleanup();
5894
5910
  this._running = true;
5895
5911
  this.agentsRegistered = false;
5912
+ log.info("relay", `start pluginId=${this.pluginId.slice(0, 8)} agent=${this.agentMeta.id}`);
5896
5913
  this.sendHeartbeat(true);
5897
5914
  this.heartbeatTimer = setInterval(() => this.sendHeartbeat(true), 2e4);
5898
5915
  this.agentsTimer = setInterval(() => {
@@ -5900,6 +5917,7 @@ var CommandRelayService = class {
5900
5917
  }, 5e3);
5901
5918
  this.reportAgents();
5902
5919
  if (process.env.NODE_ENV === "test" || process.env.CODEAM_DISABLE_SSE_PULL === "1") {
5920
+ log.info("relay", "SSE disabled \u2014 using polling fallback");
5903
5921
  this.startPollingFallback();
5904
5922
  } else {
5905
5923
  this.connectSSE();
@@ -5921,7 +5939,7 @@ var CommandRelayService = class {
5921
5939
  const url = new URL(`${API_BASE2}/api/commands/pending/stream`);
5922
5940
  url.searchParams.set("pluginId", this.pluginId);
5923
5941
  const transport = url.protocol === "https:" ? https2 : http2;
5924
- log.trace("relay", `sse connect ${url.pathname}`);
5942
+ log.info("relay", `sse connect pluginId=${this.pluginId.slice(0, 8)}`);
5925
5943
  const req = transport.request(
5926
5944
  {
5927
5945
  hostname: url.hostname,
@@ -5933,11 +5951,11 @@ var CommandRelayService = class {
5933
5951
  },
5934
5952
  (res) => {
5935
5953
  if (res.statusCode !== 200) {
5936
- log.trace("relay", `sse status=${res.statusCode}`);
5954
+ log.info("relay", `sse status=${res.statusCode} \u2014 backing off`);
5937
5955
  res.resume();
5938
5956
  this.sseFailures += 1;
5939
5957
  if (this.sseFailures >= 2) {
5940
- log.trace("relay", "sse unavailable, falling back to polling");
5958
+ log.info("relay", "sse unavailable \u2014 falling back to polling");
5941
5959
  capture("sse_fallback_to_poll", {
5942
5960
  pluginId: this.pluginId,
5943
5961
  agentId: this.agentMeta.id,
@@ -5950,10 +5968,13 @@ var CommandRelayService = class {
5950
5968
  this.scheduleSseReconnect();
5951
5969
  return;
5952
5970
  }
5971
+ log.info("relay", "sse connected");
5953
5972
  this.sseFailures = 0;
5973
+ this.armSseWatchdog();
5954
5974
  let buffer = "";
5955
5975
  res.setEncoding("utf8");
5956
5976
  res.on("data", (chunk) => {
5977
+ this.sseLastByteAt = Date.now();
5957
5978
  buffer += chunk;
5958
5979
  let frameEnd;
5959
5980
  while ((frameEnd = buffer.indexOf("\n\n")) !== -1) {
@@ -5963,15 +5984,26 @@ var CommandRelayService = class {
5963
5984
  }
5964
5985
  });
5965
5986
  res.on("end", () => {
5987
+ log.info("relay", "sse end \u2014 reconnecting");
5988
+ this.disarmSseWatchdog();
5966
5989
  if (this._running) this.scheduleSseReconnect();
5967
5990
  });
5968
- res.on("error", () => {
5991
+ res.on("error", (err) => {
5992
+ log.info("relay", `sse res error \u2014 reconnecting (${err.message})`);
5993
+ this.disarmSseWatchdog();
5969
5994
  if (this._running) this.scheduleSseReconnect();
5970
5995
  });
5971
5996
  }
5972
5997
  );
5998
+ req.on("socket", (socket) => {
5999
+ try {
6000
+ socket.setKeepAlive(true, 3e4);
6001
+ } catch {
6002
+ }
6003
+ });
5973
6004
  req.on("error", (err) => {
5974
- log.trace("relay", "sse req error", err);
6005
+ log.info("relay", `sse req error \u2014 ${err.message}`);
6006
+ this.disarmSseWatchdog();
5975
6007
  this.sseFailures += 1;
5976
6008
  if (this.sseFailures >= 2) {
5977
6009
  capture("sse_fallback_to_poll", {
@@ -5986,11 +6018,43 @@ var CommandRelayService = class {
5986
6018
  this.scheduleSseReconnect();
5987
6019
  });
5988
6020
  req.on("timeout", () => {
6021
+ log.info("relay", "sse req timeout \u2014 destroying + reconnecting");
5989
6022
  req.destroy();
5990
6023
  });
5991
6024
  req.end();
5992
6025
  this.sseRequest = req;
5993
6026
  }
6027
+ // ─── SSE liveness watchdog ───────────────────────────────────────
6028
+ //
6029
+ // The server pings every 30 s; if more than SSE_LIVENESS_TIMEOUT_MS
6030
+ // pass without a byte, we assume the TCP peer is dead-zombie and
6031
+ // force the request to destroy → triggers `error`/`end` → standard
6032
+ // reconnect path. Without this, observed in the field: half-open
6033
+ // socket sits ESTABLISHED for 30 + minutes (until macOS default
6034
+ // 2 h TCP keepalive kicks in) and the CLI silently misses every
6035
+ // pushed command.
6036
+ armSseWatchdog() {
6037
+ this.disarmSseWatchdog();
6038
+ this.sseLastByteAt = Date.now();
6039
+ this.sseWatchdog = setInterval(() => {
6040
+ const idle = Date.now() - this.sseLastByteAt;
6041
+ if (idle > SSE_LIVENESS_TIMEOUT_MS) {
6042
+ log.info("relay", `sse watchdog \u2014 ${idle}ms since last byte, forcing reconnect`);
6043
+ try {
6044
+ this.sseRequest?.destroy();
6045
+ } catch {
6046
+ }
6047
+ this.disarmSseWatchdog();
6048
+ }
6049
+ }, SSE_WATCHDOG_INTERVAL_MS);
6050
+ this.sseWatchdog.unref?.();
6051
+ }
6052
+ disarmSseWatchdog() {
6053
+ if (this.sseWatchdog) {
6054
+ clearInterval(this.sseWatchdog);
6055
+ this.sseWatchdog = null;
6056
+ }
6057
+ }
5994
6058
  handleSseFrame(frame) {
5995
6059
  let event = "message";
5996
6060
  let data = "";
@@ -6003,10 +6067,13 @@ var CommandRelayService = class {
6003
6067
  const parsed = JSON.parse(data);
6004
6068
  const commands = parsed.commands ?? [];
6005
6069
  if (commands.length === 0) return;
6006
- log.trace("relay", `sse received ${commands.length} command(s)`);
6070
+ log.info(
6071
+ "relay",
6072
+ `sse received ${commands.length} command(s) types=[${commands.map((c2) => c2.type).join(",")}]`
6073
+ );
6007
6074
  void this.dispatchCommands(commands);
6008
6075
  } catch (err) {
6009
- log.trace("relay", "sse parse error", err);
6076
+ log.info("relay", "sse parse error", err);
6010
6077
  }
6011
6078
  }
6012
6079
  scheduleSseReconnect() {
@@ -6101,6 +6168,7 @@ var CommandRelayService = class {
6101
6168
  clearTimeout(this.sseReconnectTimer);
6102
6169
  this.sseReconnectTimer = null;
6103
6170
  }
6171
+ this.disarmSseWatchdog();
6104
6172
  if (this.sseRequest) {
6105
6173
  try {
6106
6174
  this.sseRequest.destroy();
@@ -9129,6 +9197,23 @@ async function fetchClaudeQuota() {
9129
9197
 
9130
9198
  // src/services/spawn-and-capture.ts
9131
9199
  var import_child_process6 = require("child_process");
9200
+ var activeChildren = /* @__PURE__ */ new Set();
9201
+ function killActiveSpawnAndCaptureChildren() {
9202
+ for (const child of activeChildren) {
9203
+ try {
9204
+ child.kill("SIGTERM");
9205
+ } catch {
9206
+ }
9207
+ }
9208
+ setTimeout(() => {
9209
+ for (const child of activeChildren) {
9210
+ try {
9211
+ child.kill("SIGKILL");
9212
+ } catch {
9213
+ }
9214
+ }
9215
+ }, 250).unref?.();
9216
+ }
9132
9217
  async function spawnAndCapture(cmd, args2, opts = {}) {
9133
9218
  const timeoutMs = opts.timeoutMs ?? 6e4;
9134
9219
  return new Promise((resolve5) => {
@@ -9149,6 +9234,7 @@ async function spawnAndCapture(cmd, args2, opts = {}) {
9149
9234
  settle(null);
9150
9235
  return;
9151
9236
  }
9237
+ activeChildren.add(child);
9152
9238
  let stdout = "";
9153
9239
  child.stdout?.on("data", (chunk) => {
9154
9240
  stdout += chunk.toString("utf8");
@@ -9166,10 +9252,12 @@ async function spawnAndCapture(cmd, args2, opts = {}) {
9166
9252
  timer.unref();
9167
9253
  child.on("error", () => {
9168
9254
  clearTimeout(timer);
9255
+ activeChildren.delete(child);
9169
9256
  settle(null);
9170
9257
  });
9171
9258
  child.on("exit", (code) => {
9172
9259
  clearTimeout(timer);
9260
+ activeChildren.delete(child);
9173
9261
  if (code !== 0) {
9174
9262
  settle(null);
9175
9263
  return;
@@ -12581,11 +12669,17 @@ var FileWatcherService = class {
12581
12669
  } : {},
12582
12670
  awaitWriteFinish: {
12583
12671
  // Coalesces rapid sequential writes (npm install spam, build
12584
- // tools emitting bursts). Lower than chokidar's default so
12585
- // the user sees their Files screen update within 0.5 s of
12586
- // saving.
12587
- stabilityThreshold: 150,
12588
- pollInterval: 50
12672
+ // tools emitting bursts). Tuned for monorepo workloads after
12673
+ // observing a CLI process spike to 126% CPU on the codeagent
12674
+ // monorepo: pollInterval=50ms caused thousands of fs.stat
12675
+ // calls per second when many files were being edited
12676
+ // concurrently (agent edits + AI summary subprocess writes +
12677
+ // git status outputs all touching files in the watched tree).
12678
+ // 200ms is still well below the typical user perception
12679
+ // threshold for "the file appeared in the Files screen" while
12680
+ // keeping the polling cost bounded.
12681
+ stabilityThreshold: 300,
12682
+ pollInterval: 200
12589
12683
  }
12590
12684
  });
12591
12685
  watcher.on("add", (filePath) => this.schedule(filePath, "add"));
@@ -15715,6 +15809,7 @@ async function start(requestedAgent) {
15715
15809
  void streamingEmitter?.stop();
15716
15810
  closeAllTerminals();
15717
15811
  cleanupAttachmentTempFiles();
15812
+ killActiveSpawnAndCaptureChildren();
15718
15813
  process.exit(code);
15719
15814
  }
15720
15815
  }
@@ -15760,6 +15855,7 @@ async function start(requestedAgent) {
15760
15855
  void streamingEmitter?.stop();
15761
15856
  closeAllTerminals();
15762
15857
  cleanupAttachmentTempFiles();
15858
+ killActiveSpawnAndCaptureChildren();
15763
15859
  void shutdownTelemetry();
15764
15860
  process.exit(0);
15765
15861
  }
@@ -18304,7 +18400,7 @@ function checkChokidar() {
18304
18400
  }
18305
18401
  async function doctor(args2 = []) {
18306
18402
  const json = args2.includes("--json");
18307
- const cliVersion = true ? "2.23.2" : "0.0.0-dev";
18403
+ const cliVersion = true ? "2.23.4" : "0.0.0-dev";
18308
18404
  const apiBase = resolveApiBaseUrl();
18309
18405
  const diagnosticId = (0, import_node_crypto5.randomUUID)();
18310
18406
  log.info("doctor", `run id=${diagnosticId} cli=${cliVersion}`);
@@ -18503,7 +18599,7 @@ async function completion(args2) {
18503
18599
  // src/commands/version.ts
18504
18600
  var import_picocolors13 = __toESM(require("picocolors"));
18505
18601
  function version2() {
18506
- const v = true ? "2.23.2" : "unknown";
18602
+ const v = true ? "2.23.4" : "unknown";
18507
18603
  console.log(`${import_picocolors13.default.bold("codeam-cli")} ${import_picocolors13.default.cyan(v)}`);
18508
18604
  }
18509
18605
 
@@ -18731,7 +18827,7 @@ function checkForUpdates() {
18731
18827
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
18732
18828
  if (process.env.CI) return;
18733
18829
  if (!process.stdout.isTTY) return;
18734
- const current = true ? "2.23.2" : null;
18830
+ const current = true ? "2.23.4" : null;
18735
18831
  if (!current) return;
18736
18832
  const cache = readCache();
18737
18833
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeam-cli",
3
- "version": "2.23.2",
3
+ "version": "2.23.4",
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",