codeam-cli 2.61.16 → 2.61.18

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 +14 -0
  2. package/dist/index.js +259 -45
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,20 @@ 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.61.17] — 2026-07-18
8
+
9
+ ### Fixed
10
+
11
+ - **cli:** Reuse bounded adapter-resolve retry in baton + codespace-gate paths; honest fail-then-succeed retry test
12
+ - **cli:** Make buildNpmInstallInvocation platform-injectable → deterministic cross-OS tests (fixes red Windows CI)
13
+ - **cli:** Never wipe a streamed transcript on a turn error (false 'couldn't finish')
14
+
15
+ ## [2.61.16] — 2026-07-17
16
+
17
+ ### Fixed
18
+
19
+ - **cli:** ACP dispatch is registry-static — never silently downgrade an ACP agent to PTY
20
+
7
21
  ## [2.61.15] — 2026-07-17
8
22
 
9
23
  ### Added
package/dist/index.js CHANGED
@@ -841,8 +841,8 @@ var HEADROOM_MODELS = [
841
841
  }
842
842
  ];
843
843
  function headroomSnapshotDownloadLine(model, opts = {}) {
844
- const sep7 = opts.spaceAfterComma ? ", " : ",";
845
- const patterns = model.allowPatterns.map((p2) => `"${p2}"`).join(sep7);
844
+ const sep6 = opts.spaceAfterComma ? ", " : ",";
845
+ const patterns = model.allowPatterns.map((p2) => `"${p2}"`).join(sep6);
846
846
  return `snapshot_download("${model.repo}", allow_patterns=[${patterns}])`;
847
847
  }
848
848
  function headroomModelPredownloadScript(opts = {}) {
@@ -6024,7 +6024,7 @@ function readAnonId() {
6024
6024
  }
6025
6025
  function superProperties() {
6026
6026
  return {
6027
- cliVersion: true ? "2.61.16" : "0.0.0-dev",
6027
+ cliVersion: true ? "2.61.18" : "0.0.0-dev",
6028
6028
  nodeVersion: process.version,
6029
6029
  platform: process.platform,
6030
6030
  arch: process.arch,
@@ -6205,7 +6205,7 @@ var os4 = __toESM(require("os"));
6205
6205
  // package.json
6206
6206
  var package_default = {
6207
6207
  name: "codeam-cli",
6208
- version: "2.61.16",
6208
+ version: "2.61.18",
6209
6209
  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.",
6210
6210
  type: "commonjs",
6211
6211
  main: "dist/index.js",
@@ -7348,7 +7348,7 @@ var CommandRelayService = class _CommandRelayService {
7348
7348
  // fresh + clear the "CLI update available" banner after a self-update
7349
7349
  // (a codespace that reinstalls @latest reconnects via heartbeat, not
7350
7350
  // pair/reconnect). Older backends ignore the extra field.
7351
- ..."2.61.16" ? { ideVersion: "2.61.16" } : {}
7351
+ ..."2.61.18" ? { ideVersion: "2.61.18" } : {}
7352
7352
  }).then(() => log.trace("relay", `heartbeat ok online=${online}`)).catch((err) => log.trace("relay", `heartbeat failed online=${online}`, err));
7353
7353
  }
7354
7354
  /**
@@ -12211,12 +12211,12 @@ function isAvailable() {
12211
12211
  }
12212
12212
  function augmentPath() {
12213
12213
  const dirs = probeInstallDirs();
12214
- const sep7 = path16.delimiter;
12214
+ const sep6 = path16.delimiter;
12215
12215
  const current = process.env.PATH ?? "";
12216
- const existing = new Set(current.split(sep7).filter(Boolean));
12216
+ const existing = new Set(current.split(sep6).filter(Boolean));
12217
12217
  const additions = dirs.filter((d3) => !existing.has(d3));
12218
12218
  if (additions.length === 0) return;
12219
- process.env.PATH = additions.join(sep7) + sep7 + current;
12219
+ process.env.PATH = additions.join(sep6) + sep6 + current;
12220
12220
  }
12221
12221
  function runInstaller() {
12222
12222
  const isWindows = process.platform === "win32";
@@ -17949,7 +17949,7 @@ async function autoUpgradeBeforeCriticalCommand() {
17949
17949
  if (process.env.NODE_ENV === "test") return;
17950
17950
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
17951
17951
  if (process.env.CI) return;
17952
- const current = true ? "2.61.16" : null;
17952
+ const current = true ? "2.61.18" : null;
17953
17953
  if (!current) return;
17954
17954
  const cache = readCache();
17955
17955
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
@@ -17966,7 +17966,7 @@ function checkForUpdates() {
17966
17966
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
17967
17967
  if (process.env.CI) return;
17968
17968
  if (!process.stdout.isTTY) return;
17969
- const current = true ? "2.61.16" : null;
17969
+ const current = true ? "2.61.18" : null;
17970
17970
  if (!current) return;
17971
17971
  const cache = readCache();
17972
17972
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
@@ -17986,7 +17986,7 @@ var SELF_UPDATE_INTERVAL_MS = 60 * 60 * 1e3;
17986
17986
  var SELF_UPDATE_VIEW_TIMEOUT_MS = 3e4;
17987
17987
  var SELF_UPDATE_INSTALL_TIMEOUT_MS = 18e4;
17988
17988
  function currentCliVersion() {
17989
- return true ? "2.61.16" : null;
17989
+ return true ? "2.61.18" : null;
17990
17990
  }
17991
17991
  function runCmd(cmd, args2, timeoutMs) {
17992
17992
  return new Promise((resolve8) => {
@@ -22655,13 +22655,15 @@ function buildNpmInstallInvocation(opts) {
22655
22655
  const entryScript = opts?.entryScript ?? process.argv[1] ?? "";
22656
22656
  const execPath = opts?.execPath ?? process.execPath;
22657
22657
  const exists2 = opts?.existsSync ?? fs55.existsSync;
22658
- const normalized = entryScript.split(path59.sep).join("/");
22658
+ const platform3 = opts?.platform ?? process.platform;
22659
+ const p2 = platform3 === "win32" ? path59.win32 : path59.posix;
22660
+ const normalized = entryScript.split(/[\\/]/).join("/");
22659
22661
  const marker = "/lib/node_modules/codeam-cli/";
22660
22662
  const markerIdx = normalized.indexOf(marker);
22661
22663
  const prefix = markerIdx > 0 ? entryScript.slice(0, markerIdx) : null;
22662
- const siblingNpm = path59.join(
22663
- path59.dirname(execPath),
22664
- process.platform === "win32" ? "npm.cmd" : "npm"
22664
+ const siblingNpm = p2.join(
22665
+ p2.dirname(execPath),
22666
+ platform3 === "win32" ? "npm.cmd" : "npm"
22665
22667
  );
22666
22668
  const npmCommand = exists2(siblingNpm) ? siblingNpm : "npm";
22667
22669
  const npmArgs = prefix ? ["install", "-g", "--prefix", prefix, "codeam-cli@latest"] : ["install", "-g", "codeam-cli@latest"];
@@ -24509,13 +24511,21 @@ async function resolveAcpAdapterWithRetry(agent, opts = {}) {
24509
24511
  const timeoutMs = opts.timeoutMs ?? DEFAULT_ADAPTER_RETRY_TIMEOUT_MS;
24510
24512
  const pollMs = opts.pollMs ?? DEFAULT_ADAPTER_RETRY_POLL_MS;
24511
24513
  const now = opts.now ?? Date.now;
24512
- const sleep5 = opts.sleep ?? ((ms) => new Promise((resolve8) => setTimeout(resolve8, ms)));
24513
- let spec = getAcpAdapter(agent);
24514
+ const sleep5 = opts.sleep ?? ((ms) => new Promise((resolve9) => setTimeout(resolve9, ms)));
24515
+ const resolve8 = opts.resolve ?? getAcpAdapter;
24516
+ const attempt = () => {
24517
+ try {
24518
+ return resolve8(agent);
24519
+ } catch {
24520
+ return null;
24521
+ }
24522
+ };
24523
+ let spec = attempt();
24514
24524
  if (spec) return spec;
24515
24525
  const deadline = now() + timeoutMs;
24516
24526
  while (now() < deadline) {
24517
24527
  await sleep5(pollMs);
24518
- spec = getAcpAdapter(agent);
24528
+ spec = attempt();
24519
24529
  if (spec) return spec;
24520
24530
  }
24521
24531
  return null;
@@ -29182,6 +29192,34 @@ var AcpClient = class {
29182
29192
  connection = null;
29183
29193
  stopping = false;
29184
29194
  sessionId = null;
29195
+ /** The id of the NATIVE ACP `category:'model'` `SessionConfigSelect` option
29196
+ * captured from `newSession`/`loadSession` `configOptions`. It's the SINGLE
29197
+ * source of truth for model listing/switching: `session/set_config_option`
29198
+ * targets this id, and `undefined` here means the agent exposes no native
29199
+ * model selector → {@link getAvailableModels} is empty and {@link setModel}
29200
+ * throws (mobile then hides the picker). */
29201
+ modelConfigId = void 0;
29202
+ /** The models the native model config option advertises — mapped from its
29203
+ * `options` (id = option `value`, label = option `name`). Empty when the
29204
+ * agent has no model config option. NO hardcoded fallback. */
29205
+ availableModels = [];
29206
+ /** The native model select's current value — seeded from the config option's
29207
+ * `currentValue` and updated on a successful {@link setModel}. Exposed via
29208
+ * {@link getCurrentModelId} so `list_models` can report the in-use model to
29209
+ * mobile (the model line under the composer). `undefined` when no model
29210
+ * config option is advertised. */
29211
+ currentModelId = void 0;
29212
+ /** The operating/permission MODES the native ACP `SessionModeState` advertises
29213
+ * (a DIFFERENT axis than models) — mapped from `newSession`/`loadSession`
29214
+ * `modes.availableModes` (id = mode id, label = mode name, description passthrough).
29215
+ * Empty when the agent advertises no modes → {@link getAvailableModes} is empty
29216
+ * and {@link setMode} throws (mobile then hides the mode picker). */
29217
+ availableModes = [];
29218
+ /** The native `SessionModeState.currentModeId` — seeded from `modes.currentModeId`
29219
+ * and updated on a successful {@link setMode}. Exposed via {@link getCurrentModeId}
29220
+ * so `list_modes` can report the in-use mode to mobile. `undefined` when the agent
29221
+ * advertises no modes. */
29222
+ currentModeId = void 0;
29185
29223
  /** Whether the agent advertised `loadSession` on `initialize`. Drives the
29186
29224
  * post-turn session-closed recovery: an agent that supports resume (kimi,
29187
29225
  * claude, codex, gemini) is re-established with `session/load` — preserving
@@ -29402,16 +29440,18 @@ var AcpClient = class {
29402
29440
  }
29403
29441
  this.startAbortReject = null;
29404
29442
  this.sessionId = newSession.sessionId;
29405
- const newSessionMeta = newSession;
29443
+ this.captureModelConfig(newSession.configOptions ?? []);
29444
+ this.captureModes(newSession.modes ?? null);
29445
+ const tier = newSession.currentServiceTier;
29406
29446
  log.info(
29407
29447
  "acpClient",
29408
- `newSession \u2190 ok sessionId=${newSession.sessionId.slice(0, 8)} model=${newSessionMeta.currentModelId ?? "?"} tier=${newSessionMeta.currentServiceTier ?? "?"}`
29448
+ `newSession \u2190 ok sessionId=${newSession.sessionId.slice(0, 8)} model=${this.currentModelId ?? "?"} models=${this.availableModels.length} tier=${tier ?? "?"}`
29409
29449
  );
29410
29450
  return {
29411
29451
  sessionId: newSession.sessionId,
29412
29452
  initialize,
29413
- model: newSessionMeta.currentModelId,
29414
- tier: newSessionMeta.currentServiceTier
29453
+ model: this.currentModelId,
29454
+ tier
29415
29455
  };
29416
29456
  } catch (err) {
29417
29457
  this.cleanupAfterFailedStart(child);
@@ -29618,8 +29658,9 @@ var AcpClient = class {
29618
29658
  }
29619
29659
  log.info("acpClient", `loadSession \u2192 sessionId=${sessionId.slice(0, 8)}`);
29620
29660
  this.opts.beginLoadReplay?.();
29661
+ let loaded;
29621
29662
  try {
29622
- await this.connection.loadSession({
29663
+ loaded = await this.connection.loadSession({
29623
29664
  sessionId,
29624
29665
  cwd: this.opts.cwd,
29625
29666
  mcpServers: this.opts.mcpServers ?? []
@@ -29628,6 +29669,8 @@ var AcpClient = class {
29628
29669
  this.opts.endLoadReplay?.();
29629
29670
  }
29630
29671
  this.sessionId = sessionId;
29672
+ if (loaded?.configOptions) this.captureModelConfig(loaded.configOptions);
29673
+ if (loaded?.modes !== void 0) this.captureModes(loaded.modes ?? null);
29631
29674
  log.info("acpClient", `loadSession \u2190 ok sessionId=${sessionId.slice(0, 8)}`);
29632
29675
  }
29633
29676
  /**
@@ -29656,24 +29699,132 @@ var AcpClient = class {
29656
29699
  }
29657
29700
  }
29658
29701
  /**
29659
- * Switch the active session's model via the non-standard
29660
- * `session/set_model` RPC. The standard ACP SDK doesn't expose
29661
- * this claude-agent-acp and codex-acp implement it as an
29662
- * extension. Adapters without it reject and `change_model`
29663
- * surfaces a clean "not supported" affordance on mobile.
29702
+ * Switch the active session's model via the NATIVE ACP
29703
+ * `session/set_config_option` RPC, targeting the `category:'model'`
29704
+ * `SessionConfigSelect` option captured on newSession/loadSession. This is
29705
+ * the single source of truth — there is no PTY `/model` fallback and no
29706
+ * non-standard `session/set_model` extension.
29707
+ *
29708
+ * Throws a clear error when the agent exposes no model config option, so
29709
+ * `change_model` acks `failed` and mobile surfaces "model picker not
29710
+ * supported on this agent".
29664
29711
  */
29665
29712
  async setModel(modelId) {
29666
29713
  if (!this.connection || !this.sessionId) {
29667
29714
  throw new Error("AcpClient.setModel called before start()");
29668
29715
  }
29669
- log.info("acpClient", `setModel \u2192 ${modelId}`);
29670
- const rawConn = this.connection;
29671
- await rawConn.connection.sendRequest("session/set_model", {
29716
+ if (!this.modelConfigId) {
29717
+ throw new Error(
29718
+ "model selection not available: this agent exposes no ACP model config option"
29719
+ );
29720
+ }
29721
+ log.info("acpClient", `setModel \u2192 ${modelId} (configId=${this.modelConfigId})`);
29722
+ await this.connection.setSessionConfigOption({
29672
29723
  sessionId: this.sessionId,
29673
- modelId
29724
+ configId: this.modelConfigId,
29725
+ value: modelId
29674
29726
  });
29727
+ this.currentModelId = modelId;
29675
29728
  log.info("acpClient", `setModel \u2190 ok modelId=${modelId}`);
29676
29729
  }
29730
+ /**
29731
+ * The models the agent natively advertises via its `category:'model'` config
29732
+ * option (mapped: id = option value, label = option name, contextWindow
29733
+ * derived from the id). Empty array when the agent has no model config option
29734
+ * — mobile then hides the model selector. NO hardcoded fallback.
29735
+ */
29736
+ getAvailableModels() {
29737
+ return this.availableModels;
29738
+ }
29739
+ /** The native model select's current value (or undefined if none advertised). */
29740
+ getCurrentModelId() {
29741
+ return this.currentModelId;
29742
+ }
29743
+ /**
29744
+ * Parse the native model config option out of a `newSession`/`loadSession`
29745
+ * `configOptions` array and populate {@link modelConfigId},
29746
+ * {@link availableModels}, and {@link currentModelId}. The list itself is
29747
+ * native (the option's `options`); `contextWindow` is a display attribute
29748
+ * derived per-id via {@link getContextWindow}. When no `category:'model'`
29749
+ * select option is present, everything resets to empty/undefined — no
29750
+ * hardcoded list, no guessed model.
29751
+ */
29752
+ captureModelConfig(configOptions) {
29753
+ const modelOption = configOptions.find(
29754
+ (o) => o.category === "model" && o.type === "select"
29755
+ );
29756
+ if (!modelOption || modelOption.type !== "select") {
29757
+ this.modelConfigId = void 0;
29758
+ this.availableModels = [];
29759
+ this.currentModelId = void 0;
29760
+ return;
29761
+ }
29762
+ this.modelConfigId = modelOption.id;
29763
+ this.currentModelId = modelOption.currentValue;
29764
+ this.availableModels = flattenSelectOptions(modelOption.options).map((opt) => ({
29765
+ id: opt.value,
29766
+ label: opt.name,
29767
+ contextWindow: getContextWindow(opt.value)
29768
+ }));
29769
+ }
29770
+ /**
29771
+ * Switch the active session's operating/permission MODE via the NATIVE ACP
29772
+ * `session/set_mode` RPC (the standard {@link SessionModeState} axis — DISTINCT
29773
+ * from the model config option). This is the single source of truth — there is
29774
+ * no PTY fallback.
29775
+ *
29776
+ * Throws a clear error when the agent advertises no modes, so `set_mode` acks
29777
+ * `failed` and mobile surfaces "mode switching not supported on this agent".
29778
+ */
29779
+ async setMode(modeId) {
29780
+ if (!this.connection || !this.sessionId) {
29781
+ throw new Error("AcpClient.setMode called before start()");
29782
+ }
29783
+ if (this.availableModes.length === 0) {
29784
+ throw new Error(
29785
+ "mode selection not available: this agent advertises no ACP session modes"
29786
+ );
29787
+ }
29788
+ log.info("acpClient", `setMode \u2192 ${modeId}`);
29789
+ await this.connection.setSessionMode({
29790
+ sessionId: this.sessionId,
29791
+ modeId
29792
+ });
29793
+ this.currentModeId = modeId;
29794
+ log.info("acpClient", `setMode \u2190 ok modeId=${modeId}`);
29795
+ }
29796
+ /**
29797
+ * The operating/permission modes the agent natively advertises via its ACP
29798
+ * `SessionModeState` (mapped: id = mode id, label = mode name, description
29799
+ * passthrough). Empty array when the agent advertises no modes — mobile then
29800
+ * hides the mode selector. NO hardcoded fallback.
29801
+ */
29802
+ getAvailableModes() {
29803
+ return this.availableModes;
29804
+ }
29805
+ /** The native current mode id (or undefined if the agent advertises no modes). */
29806
+ getCurrentModeId() {
29807
+ return this.currentModeId;
29808
+ }
29809
+ /**
29810
+ * Parse the native ACP {@link SessionModeState} out of a `newSession`/`loadSession`
29811
+ * response `modes` field and populate {@link availableModes} + {@link currentModeId}.
29812
+ * When absent (`null`/undefined — the agent advertises no modes) everything resets
29813
+ * to empty/undefined — no hardcoded list, no guessed mode.
29814
+ */
29815
+ captureModes(modes) {
29816
+ if (!modes) {
29817
+ this.availableModes = [];
29818
+ this.currentModeId = void 0;
29819
+ return;
29820
+ }
29821
+ this.currentModeId = modes.currentModeId;
29822
+ this.availableModes = (modes.availableModes ?? []).map((mode) => ({
29823
+ id: mode.id,
29824
+ label: mode.name,
29825
+ description: mode.description ?? void 0
29826
+ }));
29827
+ }
29677
29828
  /**
29678
29829
  * Cancel the in-flight prompt turn. Notification — no response.
29679
29830
  * Safe to call when nothing is in flight (the adapter no-ops).
@@ -29826,6 +29977,17 @@ var AcpClient = class {
29826
29977
  };
29827
29978
  }
29828
29979
  };
29980
+ function flattenSelectOptions(options) {
29981
+ const out2 = [];
29982
+ for (const entry of options) {
29983
+ if ("group" in entry) {
29984
+ out2.push(...entry.options);
29985
+ } else {
29986
+ out2.push(entry);
29987
+ }
29988
+ }
29989
+ return out2;
29990
+ }
29829
29991
  function applyLineRange(content, line, limit) {
29830
29992
  if (line === null && limit === null) return { content };
29831
29993
  const lines = content.split("\n");
@@ -31671,7 +31833,7 @@ async function startTaskH(ctx) {
31671
31833
  await relay.sendResult(cmd.id, "completed", { stopReason: reply.stopReason });
31672
31834
  }
31673
31835
  } catch (err) {
31674
- const hadText = streaming.getCurrentText().trim().length > 0;
31836
+ const hadText = streaming.hasVisibleProgress();
31675
31837
  const detail = describeError(err);
31676
31838
  log.warn("acpRunner", `prompt failed: ${detail}`);
31677
31839
  await cancelStuckTurn(client3);
@@ -31782,8 +31944,38 @@ async function getConversationH(ctx) {
31782
31944
  return;
31783
31945
  }
31784
31946
  async function listModelsH(ctx) {
31785
- const { cmd, relay, models } = ctx;
31786
- await relay.sendResult(cmd.id, "completed", { models });
31947
+ const { cmd, relay, client: client3 } = ctx;
31948
+ await relay.sendResult(cmd.id, "completed", {
31949
+ models: client3.getAvailableModels(),
31950
+ currentModelId: client3.getCurrentModelId()
31951
+ });
31952
+ return;
31953
+ }
31954
+ async function listModesH(ctx) {
31955
+ const { cmd, relay, client: client3 } = ctx;
31956
+ await relay.sendResult(cmd.id, "completed", {
31957
+ modes: client3.getAvailableModes(),
31958
+ currentModeId: client3.getCurrentModeId()
31959
+ });
31960
+ return;
31961
+ }
31962
+ async function setModeH(ctx) {
31963
+ const { cmd, client: client3, relay, opts } = ctx;
31964
+ const payload = cmd.payload;
31965
+ const modeId = payload?.modeId?.trim();
31966
+ if (!modeId) {
31967
+ await relay.sendResult(cmd.id, "failed", { error: "modeId required" });
31968
+ return;
31969
+ }
31970
+ try {
31971
+ await client3.setMode(modeId);
31972
+ await relay.sendResult(cmd.id, "completed", { modeId });
31973
+ } catch (err) {
31974
+ log.warn("acpRunner", `set_mode failed: ${describeError(err)}`);
31975
+ await relay.sendResult(cmd.id, "failed", {
31976
+ error: `Mode switching not supported on ${opts.agent} via ACP: ${describeError(err)}`
31977
+ });
31978
+ }
31787
31979
  return;
31788
31980
  }
31789
31981
  async function ackEmptyH(ctx) {
@@ -32039,6 +32231,8 @@ var ACP_COMMAND_HANDLERS = {
32039
32231
  escape_key: stopTaskH,
32040
32232
  get_conversation: getConversationH,
32041
32233
  list_models: listModelsH,
32234
+ list_modes: listModesH,
32235
+ set_mode: setModeH,
32042
32236
  set_keep_alive: ackEmptyH,
32043
32237
  get_context: ackEmptyH,
32044
32238
  select_option: selectOptionH,
@@ -32250,6 +32444,31 @@ var StreamingState = class {
32250
32444
  getCurrentText() {
32251
32445
  return this.text;
32252
32446
  }
32447
+ /**
32448
+ * True when the in-progress turn has ALREADY streamed VISIBLE content —
32449
+ * assistant text OR any thinking / tool_use / tool_result activity (the
32450
+ * SessionDetail rich feed). Deliberately BROADER than {@link getCurrentText}:
32451
+ * a long agentic turn can stream lots of tool/thinking progress and yet NOT
32452
+ * have emitted a final assistant `text` reply at the instant `client.prompt`
32453
+ * throws — the idle watchdog tripping just as the agent finishes its last
32454
+ * tool, or a trailing adapter error AFTER the work completed. Keying the
32455
+ * failure-bubble decision off `text` alone then mis-reads that visible work
32456
+ * as "nothing streamed" → fires the generic TURN_FAILURE_MESSAGE, and
32457
+ * {@link closeWithBubble} REPLACES the whole turn with it (overwriting the
32458
+ * durable snapshot) — an error wiping a fully-streamed transcript AND a false
32459
+ * "couldn't finish" (2026-07-17 incident). The catch path keys "don't clobber
32460
+ * — a partial reply already serves as the terminal frame" off THIS instead,
32461
+ * routing to the non-destructive {@link closeAll}. streamingChunks still holds
32462
+ * the turn's content at catch time (none of the close/flush methods have run
32463
+ * yet on the throw path), so this reflects the real streamed progress.
32464
+ */
32465
+ hasVisibleProgress() {
32466
+ if (this.text.trim().length > 0) return true;
32467
+ for (const { content } of this.streamingChunks.values()) {
32468
+ if (content.trim().length > 0) return true;
32469
+ }
32470
+ return false;
32471
+ }
32253
32472
  async beginTurn(opts) {
32254
32473
  this.text = "";
32255
32474
  this.streamingChunks.clear();
@@ -32760,7 +32979,6 @@ async function runAcpSession(opts) {
32760
32979
  done: true
32761
32980
  });
32762
32981
  const runtime = createInteractiveAgentStrategy(opts.agent, createOsStrategy());
32763
- const models = await runtime.listModels();
32764
32982
  const history = new AcpHistory(publisher, {
32765
32983
  agent: opts.agent,
32766
32984
  acpSessionId,
@@ -32836,7 +33054,6 @@ async function runAcpSession(opts) {
32836
33054
  client3,
32837
33055
  relay,
32838
33056
  acpSessionId,
32839
- models,
32840
33057
  streaming,
32841
33058
  opts,
32842
33059
  history,
@@ -32892,12 +33109,11 @@ async function runAcpSession(opts) {
32892
33109
  await new Promise(() => {
32893
33110
  });
32894
33111
  }
32895
- async function handleCommand(cmd, client3, relay, acpSessionId, models, streaming, opts, history, jsonlHistory, agentCaps, turnFiles, getBeads, publisher, recentStderr, budgetRecovery, budgetReachedFlag, onActiveSessionChanged) {
33112
+ async function handleCommand(cmd, client3, relay, acpSessionId, streaming, opts, history, jsonlHistory, agentCaps, turnFiles, getBeads, publisher, recentStderr, budgetRecovery, budgetReachedFlag, onActiveSessionChanged) {
32896
33113
  const session = {
32897
33114
  client: client3,
32898
33115
  relay,
32899
33116
  acpSessionId,
32900
- models,
32901
33117
  streaming,
32902
33118
  opts,
32903
33119
  history,
@@ -34233,7 +34449,6 @@ var AcpDriver = class {
34233
34449
  async ensureSession(relay, acpSessionId, agentCaps) {
34234
34450
  if (this.session) return this.session;
34235
34451
  const { opts, publisher, streaming, runtime, recentStderr } = this.deps;
34236
- const models = await runtime.listModels();
34237
34452
  const jsonlHistory = new HistoryService(runtime, opts.pluginId, opts.cwd, {
34238
34453
  pluginAuthToken: opts.pluginAuthToken
34239
34454
  });
@@ -34273,7 +34488,6 @@ var AcpDriver = class {
34273
34488
  client: this.deps.client,
34274
34489
  relay,
34275
34490
  acpSessionId,
34276
- models,
34277
34491
  streaming,
34278
34492
  opts,
34279
34493
  history,
@@ -34782,7 +34996,7 @@ async function start(requestedAgent) {
34782
34996
  const depsReady = process.env.CODESPACES === "true" ? provisionProjectDependencies(cwd).catch(() => void 0) : Promise.resolve();
34783
34997
  if (process.env.CODESPACES === "true") {
34784
34998
  const GATE_TIMEOUT_MS = 24e4;
34785
- const acpAdapterForGate = getAcpAdapter(session.agent);
34999
+ const acpAdapterForGate = requiresAcp(session.agent) ? await resolveAcpAdapterWithRetry(session.agent) : null;
34786
35000
  const agentBinaryReady = acpAdapterForGate ? Promise.all([
34787
35001
  acpAdapterForGate.waitForBinary({ timeoutMs: GATE_TIMEOUT_MS }).catch(() => false),
34788
35002
  waitForAdapterModuleGraph(acpAdapterForGate.command, acpAdapterForGate.args, {
@@ -34803,7 +35017,7 @@ async function start(requestedAgent) {
34803
35017
  );
34804
35018
  }
34805
35019
  if (isLocalSession() && requiresAcp(session.agent)) {
34806
- const adapter = getAcpAdapter(session.agent);
35020
+ const adapter = await resolveAcpAdapterWithRetry(session.agent);
34807
35021
  const batonCapable = runtimeSupportsBaton(createRuntimeStrategy(session.agent));
34808
35022
  if (batonCapable && adapter && session.pluginAuthToken) {
34809
35023
  await runBatonSession({
@@ -37598,7 +37812,7 @@ function checkChokidar() {
37598
37812
  }
37599
37813
  async function doctor(args2 = []) {
37600
37814
  const json = args2.includes("--json");
37601
- const cliVersion = true ? "2.61.16" : "0.0.0-dev";
37815
+ const cliVersion = true ? "2.61.18" : "0.0.0-dev";
37602
37816
  const apiBase2 = resolveApiBaseUrl();
37603
37817
  const diagnosticId = (0, import_node_crypto12.randomUUID)();
37604
37818
  log.info("doctor", `run id=${diagnosticId} cli=${cliVersion}`);
@@ -38109,7 +38323,7 @@ async function mcpRun(args2) {
38109
38323
  // src/commands/version.ts
38110
38324
  var import_picocolors15 = __toESM(require("picocolors"));
38111
38325
  function version2() {
38112
- const v = true ? "2.61.16" : "unknown";
38326
+ const v = true ? "2.61.18" : "unknown";
38113
38327
  console.log(`${import_picocolors15.default.bold("codeam-cli")} ${import_picocolors15.default.cyan(v)}`);
38114
38328
  }
38115
38329
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeam-cli",
3
- "version": "2.61.16",
3
+ "version": "2.61.18",
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",