codeam-cli 2.50.0 → 2.51.0

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 +20 -0
  2. package/dist/index.js +339 -23
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,26 @@ 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.50.0] — 2026-06-28
8
+
9
+ ### Added
10
+
11
+ - **cli:** Persisted beads-config.json (default-on disable flag)
12
+ - **cli:** Honor persisted beads disable flag in provisionBeadsForStart
13
+ - **cli:** ConfigureBeads enable/disable/status service
14
+ - **cli:** Beads_configure command handler + event emitter
15
+ - **cli:** Emit input_suggestion chip on ACP agent path after closing question
16
+
17
+ ### Fixed
18
+
19
+ - **cli:** Status honors persisted disable flag; drop beads handler cast
20
+ - **cli:** Make beads_configure status probe read-only (no provisioning)
21
+
22
+ ### Tests
23
+
24
+ - **cli:** Gated beads_configure integration smoke
25
+ - **cli:** Import vitest globals in beads test files (fix typecheck)
26
+
7
27
  ## [2.49.5] — 2026-06-28
8
28
 
9
29
  ### Fixed
package/dist/index.js CHANGED
@@ -5397,7 +5397,7 @@ function readAnonId() {
5397
5397
  }
5398
5398
  function superProperties() {
5399
5399
  return {
5400
- cliVersion: true ? "2.50.0" : "0.0.0-dev",
5400
+ cliVersion: true ? "2.51.0" : "0.0.0-dev",
5401
5401
  nodeVersion: process.version,
5402
5402
  platform: process.platform,
5403
5403
  arch: process.arch,
@@ -5578,7 +5578,7 @@ var os4 = __toESM(require("os"));
5578
5578
  // package.json
5579
5579
  var package_default = {
5580
5580
  name: "codeam-cli",
5581
- version: "2.50.0",
5581
+ version: "2.51.0",
5582
5582
  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.",
5583
5583
  type: "commonjs",
5584
5584
  main: "dist/index.js",
@@ -14578,6 +14578,14 @@ function mapStatsToSavings(stats, prev, inputPricePerMillion = DEFAULT_INPUT_PRI
14578
14578
  };
14579
14579
  return { next, delta };
14580
14580
  }
14581
+ function defaultGetBudgetEnv() {
14582
+ const raw = process.env["HEADROOM_BUDGET"];
14583
+ if (!raw) return null;
14584
+ const budgetUsd = Number(raw);
14585
+ if (!isFinite(budgetUsd) || budgetUsd <= 0) return null;
14586
+ const budgetPeriod = process.env["HEADROOM_BUDGET_PERIOD"] ?? "daily";
14587
+ return { budgetUsd, budgetPeriod };
14588
+ }
14581
14589
  var HeadroomStatsReporter = class {
14582
14590
  constructor(deps) {
14583
14591
  this.deps = deps;
@@ -14591,14 +14599,22 @@ var HeadroomStatsReporter = class {
14591
14599
  }
14592
14600
  async tick() {
14593
14601
  try {
14602
+ const stats = await this.deps.fetchStats();
14594
14603
  const { next, delta } = mapStatsToSavings(
14595
- await this.deps.fetchStats(),
14604
+ stats,
14596
14605
  this.prev,
14597
14606
  this.deps.inputPricePerMillionUsd ?? DEFAULT_INPUT_PRICE_PER_MILLION
14598
14607
  );
14599
14608
  this.prev = next;
14600
14609
  if (delta.compressionTokens > 0 || delta.compressionSavingsUsd > 0 || delta.rawTokensEst > 0 || delta.cacheReadTokens > 0 || delta.cacheSavingsUsd > 0) {
14601
- await this.deps.postSavings(delta);
14610
+ const getBudgetEnv = this.deps.getBudgetEnv ?? defaultGetBudgetEnv;
14611
+ const budgetEnv = getBudgetEnv();
14612
+ const budget = budgetEnv ? {
14613
+ periodSpendUsd: stats.cost?.cost_with_headroom_usd ?? 0,
14614
+ budgetUsd: budgetEnv.budgetUsd,
14615
+ budgetPeriod: budgetEnv.budgetPeriod
14616
+ } : void 0;
14617
+ await this.deps.postSavings(delta, budget);
14602
14618
  }
14603
14619
  } catch {
14604
14620
  }
@@ -14611,6 +14627,14 @@ var HeadroomStatsReporter = class {
14611
14627
  }
14612
14628
  };
14613
14629
 
14630
+ // src/services/headroom/budget-args.ts
14631
+ function buildBudgetProxyArgs(env) {
14632
+ const budget = env["HEADROOM_BUDGET"];
14633
+ if (!budget) return [];
14634
+ const period = env["HEADROOM_BUDGET_PERIOD"] ?? "daily";
14635
+ return ["--budget", budget, "--budget-period", period];
14636
+ }
14637
+
14614
14638
  // src/lib/updateNotifier.ts
14615
14639
  var fs30 = __toESM(require("fs"));
14616
14640
  var os27 = __toESM(require("os"));
@@ -14769,7 +14793,7 @@ async function autoUpgradeBeforeCriticalCommand() {
14769
14793
  if (process.env.NODE_ENV === "test") return;
14770
14794
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
14771
14795
  if (process.env.CI) return;
14772
- const current = true ? "2.50.0" : null;
14796
+ const current = true ? "2.51.0" : null;
14773
14797
  if (!current) return;
14774
14798
  const cache = readCache();
14775
14799
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
@@ -14786,7 +14810,7 @@ function checkForUpdates() {
14786
14810
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
14787
14811
  if (process.env.CI) return;
14788
14812
  if (!process.stdout.isTTY) return;
14789
- const current = true ? "2.50.0" : null;
14813
+ const current = true ? "2.51.0" : null;
14790
14814
  if (!current) return;
14791
14815
  const cache = readCache();
14792
14816
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
@@ -14823,7 +14847,7 @@ function maybeStartHeadroomReporter(ctx) {
14823
14847
  const res = await fetch("http://localhost:8787/stats");
14824
14848
  return res.json();
14825
14849
  },
14826
- postSavings: async (delta) => {
14850
+ postSavings: async (delta, budget) => {
14827
14851
  await fetch(ingestUrl, {
14828
14852
  method: "POST",
14829
14853
  headers: {
@@ -14834,7 +14858,12 @@ function maybeStartHeadroomReporter(ctx) {
14834
14858
  sessionId: ctx.sessionId,
14835
14859
  pluginId: ctx.pluginId,
14836
14860
  agentId: process.env["HEADROOM_AGENT"] ?? "claude",
14837
- savings: delta
14861
+ savings: delta,
14862
+ ...budget ? {
14863
+ periodSpendUsd: budget.periodSpendUsd,
14864
+ budgetUsd: budget.budgetUsd,
14865
+ budgetPeriod: budget.budgetPeriod
14866
+ } : {}
14838
14867
  })
14839
14868
  });
14840
14869
  }
@@ -14867,7 +14896,7 @@ function maybeResumeLocalHeadroomReporter(ctx) {
14867
14896
  // Body MUST match HeadroomSavingsDto + PluginAuthGuard (sessionId +
14868
14897
  // pluginId in the body) — same shape as the codespace reporter above and
14869
14898
  // the on-demand `startReporter` in handlers.ts.
14870
- postSavings: async (delta) => {
14899
+ postSavings: async (delta, budget) => {
14871
14900
  await fetch(ingestUrl, {
14872
14901
  method: "POST",
14873
14902
  headers: {
@@ -14878,7 +14907,12 @@ function maybeResumeLocalHeadroomReporter(ctx) {
14878
14907
  sessionId: ctx.sessionId,
14879
14908
  pluginId: ctx.pluginId,
14880
14909
  agentId: agent,
14881
- savings: delta
14910
+ savings: delta,
14911
+ ...budget ? {
14912
+ periodSpendUsd: budget.periodSpendUsd,
14913
+ budgetUsd: budget.budgetUsd,
14914
+ budgetPeriod: budget.budgetPeriod
14915
+ } : {}
14882
14916
  })
14883
14917
  });
14884
14918
  }
@@ -15179,11 +15213,16 @@ function readHeadroomChildEnv() {
15179
15213
  if (typeof parsed !== "object" || parsed === null) return {};
15180
15214
  const o = parsed;
15181
15215
  if (o.enabled === true && typeof o.agent === "string" && o.agent.length > 0 && typeof o.ingestUrl === "string" && o.ingestUrl.length > 0) {
15182
- return {
15216
+ const env = {
15183
15217
  HEADROOM_ENABLED: "1",
15184
15218
  HEADROOM_AGENT: o.agent,
15185
15219
  HEADROOM_SAVINGS_INGEST_URL: o.ingestUrl
15186
15220
  };
15221
+ if (o.budgetEnabled === true && typeof o.budgetUsd === "number") {
15222
+ env["HEADROOM_BUDGET"] = String(o.budgetUsd);
15223
+ env["HEADROOM_BUDGET_PERIOD"] = typeof o.budgetPeriod === "string" ? o.budgetPeriod : "daily";
15224
+ }
15225
+ return env;
15187
15226
  }
15188
15227
  return {};
15189
15228
  } catch {
@@ -15439,11 +15478,16 @@ async function setupHeadroomForSelfHosted(agent, runner = defaultHeadroomRunner,
15439
15478
  }
15440
15479
  onProgress("proxy");
15441
15480
  try {
15442
- const proxy = (0, import_node_child_process14.spawn)("headroom", ["proxy", "--port", "8787"], {
15443
- stdio: "ignore",
15444
- detached: true,
15445
- env: { ...process.env, HEADROOM_KOMPRESS_BACKEND: "onnx_cpu" }
15446
- });
15481
+ const proxyEnv = { ...process.env, HEADROOM_KOMPRESS_BACKEND: "onnx_cpu" };
15482
+ const proxy = (0, import_node_child_process14.spawn)(
15483
+ "headroom",
15484
+ ["proxy", "--port", "8787", ...buildBudgetProxyArgs(proxyEnv)],
15485
+ {
15486
+ stdio: "ignore",
15487
+ detached: true,
15488
+ env: proxyEnv
15489
+ }
15490
+ );
15447
15491
  proxy.once("error", (e) => {
15448
15492
  log.warn(
15449
15493
  "host-agent",
@@ -15467,7 +15511,7 @@ var defaultSpawner = (env, cwd, args2 = []) => (0, import_node_child_process14.s
15467
15511
  detached: false
15468
15512
  });
15469
15513
  function currentCliVersion() {
15470
- return true ? "2.50.0" : null;
15514
+ return true ? "2.51.0" : null;
15471
15515
  }
15472
15516
  function runCmd(cmd, args2, timeoutMs) {
15473
15517
  return new Promise((resolve7) => {
@@ -18527,7 +18571,7 @@ var headroomConfigureH = async (ctx, cmd, parsed) => {
18527
18571
  const res = await fetch("http://localhost:8787/stats");
18528
18572
  return res.json();
18529
18573
  },
18530
- postSavings: async (delta) => {
18574
+ postSavings: async (delta, budget) => {
18531
18575
  if (!opts.ingestUrl) return;
18532
18576
  await fetch(opts.ingestUrl, {
18533
18577
  method: "POST",
@@ -18546,7 +18590,12 @@ var headroomConfigureH = async (ctx, cmd, parsed) => {
18546
18590
  sessionId: ctx.sessionId,
18547
18591
  pluginId: ctx.pluginId,
18548
18592
  agentId: opts.agent,
18549
- savings: delta
18593
+ savings: delta,
18594
+ ...budget ? {
18595
+ periodSpendUsd: budget.periodSpendUsd,
18596
+ budgetUsd: budget.budgetUsd,
18597
+ budgetPeriod: budget.budgetPeriod
18598
+ } : {}
18550
18599
  })
18551
18600
  });
18552
18601
  }
@@ -18587,6 +18636,89 @@ var headroomConfigureH = async (ctx, cmd, parsed) => {
18587
18636
  });
18588
18637
  await ctx.relay.sendResult(cmd.id, "completed", result);
18589
18638
  };
18639
+ var headroomBudgetH = async (ctx, cmd) => {
18640
+ const payload = cmd.payload;
18641
+ let rawAgentId = ctx.agentId || (typeof payload.agentId === "string" ? payload.agentId : "");
18642
+ if (rawAgentId === "claude_code") rawAgentId = "claude";
18643
+ let payloadAgentId = typeof payload.agentId === "string" ? payload.agentId : "";
18644
+ if (payloadAgentId === "claude_code") payloadAgentId = "claude";
18645
+ if (!rawAgentId || !isHeadroomSupportedAgent(rawAgentId)) {
18646
+ await ctx.relay.sendResult(cmd.id, "completed", { applied: false });
18647
+ return;
18648
+ }
18649
+ if (!payloadAgentId || payloadAgentId !== rawAgentId) {
18650
+ await ctx.relay.sendResult(cmd.id, "completed", { applied: false });
18651
+ return;
18652
+ }
18653
+ let headroomActive = false;
18654
+ try {
18655
+ const raw = JSON.parse(fs41.readFileSync(headroomConfigPath(), "utf8"));
18656
+ headroomActive = raw.enabled === true;
18657
+ } catch {
18658
+ }
18659
+ if (!headroomActive) {
18660
+ await ctx.relay.sendResult(cmd.id, "completed", { applied: false });
18661
+ return;
18662
+ }
18663
+ let existingConfig = { enabled: true };
18664
+ try {
18665
+ existingConfig = JSON.parse(fs41.readFileSync(headroomConfigPath(), "utf8"));
18666
+ } catch {
18667
+ }
18668
+ if (payload.budgetEnabled && payload.budgetUsd != null) {
18669
+ persistHeadroomConfig({
18670
+ ...existingConfig,
18671
+ enabled: existingConfig.enabled ?? true,
18672
+ budgetEnabled: true,
18673
+ budgetUsd: payload.budgetUsd,
18674
+ budgetPeriod: payload.budgetPeriod ?? "daily"
18675
+ });
18676
+ process.env["HEADROOM_BUDGET"] = String(payload.budgetUsd);
18677
+ process.env["HEADROOM_BUDGET_PERIOD"] = payload.budgetPeriod ?? "daily";
18678
+ } else {
18679
+ persistHeadroomConfig({
18680
+ ...existingConfig,
18681
+ enabled: existingConfig.enabled ?? true,
18682
+ budgetEnabled: false,
18683
+ budgetUsd: void 0,
18684
+ budgetPeriod: void 0
18685
+ });
18686
+ delete process.env["HEADROOM_BUDGET"];
18687
+ delete process.env["HEADROOM_BUDGET_PERIOD"];
18688
+ }
18689
+ try {
18690
+ const killer = (0, import_child_process20.spawn)("pkill", ["-TERM", "-f", "headroom.*proxy"], {
18691
+ detached: true,
18692
+ stdio: "ignore"
18693
+ });
18694
+ killer.once("error", () => {
18695
+ });
18696
+ killer.unref();
18697
+ } catch {
18698
+ }
18699
+ try {
18700
+ const proxyEnv = { ...process.env, HEADROOM_KOMPRESS_BACKEND: "onnx_cpu" };
18701
+ const proxy = (0, import_child_process20.spawn)(
18702
+ "headroom",
18703
+ ["proxy", "--port", "8787", ...buildBudgetProxyArgs(proxyEnv)],
18704
+ {
18705
+ stdio: "ignore",
18706
+ detached: true,
18707
+ env: proxyEnv
18708
+ }
18709
+ );
18710
+ proxy.once("error", (e) => {
18711
+ log.warn("headroom-budget", `proxy relaunch error (best-effort): ${e.message}`);
18712
+ });
18713
+ proxy.unref();
18714
+ } catch (e) {
18715
+ log.warn(
18716
+ "headroom-budget",
18717
+ `proxy relaunch failed (best-effort): ${e instanceof Error ? e.message : String(e)}`
18718
+ );
18719
+ }
18720
+ await ctx.relay.sendResult(cmd.id, "completed", { applied: true });
18721
+ };
18590
18722
  var _beadsEmitChain = Promise.resolve();
18591
18723
  var beadsConfigureH = async (ctx, cmd, parsed) => {
18592
18724
  const action = parsed.action;
@@ -19632,6 +19764,7 @@ var handlers = {
19632
19764
  env_read: envReadH,
19633
19765
  env_write: envWriteH,
19634
19766
  headroom_configure: headroomConfigureH,
19767
+ headroom_budget: headroomBudgetH,
19635
19768
  beads_configure: beadsConfigureH
19636
19769
  };
19637
19770
  async function dispatchCommand(ctx, cmd) {
@@ -24504,6 +24637,79 @@ function createOneMRecovery(deps) {
24504
24637
  };
24505
24638
  }
24506
24639
 
24640
+ // src/agents/acp/budgetRecovery.ts
24641
+ var BUDGET_EXCEEDED_RE = /budget exceeded for (\w+) period/i;
24642
+ function looksLikeBudgetExceeded(text) {
24643
+ return BUDGET_EXCEEDED_RE.test(text);
24644
+ }
24645
+ function extractBudgetPeriod(text) {
24646
+ const m = BUDGET_EXCEEDED_RE.exec(text);
24647
+ return m ? m[1] ?? "current" : "current";
24648
+ }
24649
+ var BUDGET_PAUSE_OPTION = "Pause budget this session";
24650
+ var BUDGET_RAISE_OPTION = "Raise budget";
24651
+ function createBudgetRecovery(deps) {
24652
+ let pending = null;
24653
+ const buildQuestion = (period) => `\u{1F4B8} **Headroom budget reached for the ${period} period.**
24654
+
24655
+ You have two options:
24656
+ \u2022 **Pause budget this session** \u2014 removes the spending cap for this session only (the proxy restarts without a budget limit).
24657
+ \u2022 **Raise budget** \u2014 open your agent budget settings to increase the cap.`;
24658
+ return {
24659
+ offer: async (commandId, blocks, detail) => {
24660
+ const period = extractBudgetPeriod(detail);
24661
+ pending = { blocks, period };
24662
+ const question = buildQuestion(period);
24663
+ const options = [BUDGET_PAUSE_OPTION, BUDGET_RAISE_OPTION];
24664
+ await deps.publishText(question);
24665
+ await deps.publishSelectPrompt(question, options);
24666
+ await deps.publishAwaitingAnswer(question, options);
24667
+ deps.appendAgentReply(question);
24668
+ deps.flushHistory();
24669
+ deps.log?.(`budget-exceeded recovery offered period=${period} id=${commandId.slice(0, 8)}`);
24670
+ await deps.sendResult(commandId, "failed", {
24671
+ error: `Headroom budget exceeded (${period}) \u2014 recovery offered`
24672
+ });
24673
+ },
24674
+ tryRecover: async (commandId, optionIndex) => {
24675
+ if (!pending) return false;
24676
+ const { blocks, period } = pending;
24677
+ pending = null;
24678
+ if (optionIndex === 0) {
24679
+ deps.log?.("budget recovery \u2192 pause: relaunching proxy without budget");
24680
+ try {
24681
+ await deps.relaunchProxyWithoutBudget();
24682
+ } catch (err) {
24683
+ deps.log?.(`budget recovery \u2192 relaunch failed (best-effort): ${String(err)}`);
24684
+ }
24685
+ await deps.sendResult(commandId, "completed", {
24686
+ action: "pause",
24687
+ period,
24688
+ note: "Headroom proxy relaunched without budget limit for this session. Send your message again."
24689
+ });
24690
+ const note = "\u2705 **Budget paused for this session.** The proxy is running without a spending cap \u2014 send your message again.";
24691
+ await deps.publishText(note);
24692
+ deps.appendAgentReply(note);
24693
+ deps.flushHistory();
24694
+ deps.log?.(`budget recovery \u2192 pause complete id=${commandId.slice(0, 8)}`);
24695
+ } else {
24696
+ deps.log?.(`budget recovery \u2192 raise: emitting deep-link for agent=${deps.agentId}`);
24697
+ await deps.publishRawChunk({
24698
+ type: "open_agent_budget_settings",
24699
+ agentId: deps.agentId,
24700
+ done: true
24701
+ });
24702
+ await deps.sendResult(commandId, "completed", {
24703
+ action: "raise",
24704
+ agentId: deps.agentId
24705
+ });
24706
+ deps.log?.(`budget recovery \u2192 raise emitted id=${commandId.slice(0, 8)}`);
24707
+ }
24708
+ return true;
24709
+ }
24710
+ };
24711
+ }
24712
+
24507
24713
  // src/agents/acp/reconcileDelta.ts
24508
24714
  function reconcileCumulative(existing, incoming) {
24509
24715
  if (incoming.length === 0) return existing;
@@ -25938,10 +26144,26 @@ async function surfaceStartupFailure(opts) {
25938
26144
  );
25939
26145
  errRelay.start();
25940
26146
  }
26147
+ function budgetBubbleMessage(agent, period) {
26148
+ return `\u{1F4B8} **Headroom budget reached for the ${period} period.**
26149
+
26150
+ The local Headroom proxy rejected the ${agent} request because the configured spending cap was hit. Choose an option below to continue.`;
26151
+ }
26152
+ function buildRelaunchProxyEnv(baseEnv) {
26153
+ const env = { ...baseEnv, HEADROOM_KOMPRESS_BACKEND: "onnx_cpu" };
26154
+ delete env["HEADROOM_BUDGET"];
26155
+ delete env["HEADROOM_BUDGET_PERIOD"];
26156
+ return env;
26157
+ }
25941
26158
  function failureBubble(opts) {
25942
26159
  if (looksLikeAuthFailure(opts.detail) || looksLikeAuthFailure(opts.recentStderr)) {
25943
26160
  return AUTH_FAILURE_MESSAGE;
25944
26161
  }
26162
+ const budgetHaystack = `${opts.detail}
26163
+ ${opts.recentStderr}`;
26164
+ if (looksLikeBudgetExceeded(budgetHaystack)) {
26165
+ return budgetBubbleMessage(opts.agent, extractBudgetPeriod(budgetHaystack));
26166
+ }
25945
26167
  if (looksLikeProviderOutage(opts.detail) || looksLikeProviderOutage(opts.recentStderr)) {
25946
26168
  return providerOutageMessage(opts.agent);
25947
26169
  }
@@ -25974,6 +26196,25 @@ async function reportCredentialInvalid(opts, fetchImpl = fetch) {
25974
26196
  } catch {
25975
26197
  }
25976
26198
  }
26199
+ async function postBudgetReached(opts, fetchImpl = fetch) {
26200
+ const url = `${resolveApiBaseUrl()}/api/sessions/${encodeURIComponent(opts.sessionId)}/headroom-budget-reached`;
26201
+ try {
26202
+ await fetchImpl(url, {
26203
+ method: "POST",
26204
+ headers: {
26205
+ "Content-Type": "application/json",
26206
+ "X-Plugin-Auth-Token": opts.pluginAuthToken
26207
+ },
26208
+ body: JSON.stringify({
26209
+ sessionId: opts.sessionId,
26210
+ pluginId: opts.pluginId,
26211
+ agent: opts.agent,
26212
+ period: opts.period
26213
+ })
26214
+ });
26215
+ } catch {
26216
+ }
26217
+ }
25977
26218
  async function handleGetConversation(args2) {
25978
26219
  const { relay, commandId, jsonlHistory, acpSessionId } = args2;
25979
26220
  try {
@@ -26139,6 +26380,58 @@ async function runAcpSession(opts) {
26139
26380
  describeError,
26140
26381
  log: (msg) => log.info("acpRunner", msg)
26141
26382
  });
26383
+ let _budgetReachedPosted = false;
26384
+ const relaunchProxyWithoutBudget = async () => {
26385
+ const { spawn: spawn32 } = await import("child_process");
26386
+ try {
26387
+ const killer = spawn32("pkill", ["-TERM", "-f", "headroom.*proxy"], {
26388
+ detached: true,
26389
+ stdio: "ignore"
26390
+ });
26391
+ killer.once("error", () => {
26392
+ });
26393
+ killer.unref();
26394
+ } catch {
26395
+ }
26396
+ await new Promise((r) => setTimeout(r, 500));
26397
+ const proxyEnv = buildRelaunchProxyEnv(process.env);
26398
+ try {
26399
+ const proxy = spawn32(
26400
+ "headroom",
26401
+ ["proxy", "--port", "8787"],
26402
+ { stdio: "ignore", detached: true, env: proxyEnv }
26403
+ );
26404
+ proxy.once("error", (e) => {
26405
+ log.warn("acpRunner", `budget recovery proxy relaunch error (best-effort): ${e.message}`);
26406
+ });
26407
+ proxy.unref();
26408
+ } catch (e) {
26409
+ log.warn(
26410
+ "acpRunner",
26411
+ `budget recovery proxy relaunch failed (best-effort): ${e instanceof Error ? e.message : String(e)}`
26412
+ );
26413
+ }
26414
+ await new Promise((r) => setTimeout(r, 3e3));
26415
+ };
26416
+ const budgetRecovery = createBudgetRecovery({
26417
+ publishText: (text) => publisher.publishOutput({ type: "text", content: text, done: true }),
26418
+ publishSelectPrompt: (question, options) => publisher.publishOutput({
26419
+ type: "select_prompt",
26420
+ content: question,
26421
+ options,
26422
+ optionDescriptions: options.map(() => ""),
26423
+ currentIndex: 0,
26424
+ done: true
26425
+ }),
26426
+ publishAwaitingAnswer: (prompt, options) => publisher.publishAwaitingAnswer({ questionId: (0, import_node_crypto7.randomUUID)(), prompt, options }),
26427
+ publishRawChunk: (chunk) => publisher.publishOutput(chunk),
26428
+ sendResult: (commandId, status2, result) => relay.sendResult(commandId, status2, result),
26429
+ appendAgentReply: (text) => history.appendAgentReply(text),
26430
+ flushHistory: () => void history.flush(),
26431
+ relaunchProxyWithoutBudget,
26432
+ agentId: opts.agent,
26433
+ log: (msg) => log.info("acpRunner", msg)
26434
+ });
26142
26435
  showInfo(`Starting ${opts.agent} via ACP adapter (${opts.adapter.requiresAgentBinary})\u2026`);
26143
26436
  let handshake;
26144
26437
  try {
@@ -26248,7 +26541,11 @@ async function runAcpSession(opts) {
26248
26541
  getBeads,
26249
26542
  publisher,
26250
26543
  recentStderr,
26251
- oneMRecovery
26544
+ oneMRecovery,
26545
+ budgetRecovery,
26546
+ { get: () => _budgetReachedPosted, set: (v) => {
26547
+ _budgetReachedPosted = v;
26548
+ } }
26252
26549
  );
26253
26550
  },
26254
26551
  { id: opts.agent, name: opts.agent, displayName: opts.agent }
@@ -26272,7 +26569,7 @@ async function runAcpSession(opts) {
26272
26569
  await new Promise(() => {
26273
26570
  });
26274
26571
  }
26275
- async function handleCommand(cmd, client2, relay, acpSessionId, models, streaming, opts, history, jsonlHistory, agentCaps, turnFiles, getBeads, publisher, recentStderr, oneMRecovery) {
26572
+ async function handleCommand(cmd, client2, relay, acpSessionId, models, streaming, opts, history, jsonlHistory, agentCaps, turnFiles, getBeads, publisher, recentStderr, oneMRecovery, budgetRecovery, budgetReachedFlag) {
26276
26573
  switch (cmd.type) {
26277
26574
  case "beads_action": {
26278
26575
  const beads = getBeads();
@@ -26356,6 +26653,24 @@ async function handleCommand(cmd, client2, relay, acpSessionId, models, streamin
26356
26653
  const detail = describeError(err);
26357
26654
  log.warn("acpRunner", `prompt failed: ${detail}`);
26358
26655
  await cancelStuckTurn(client2);
26656
+ if (looksLikeBudgetExceeded(`${detail}
26657
+ ${recentStderr.join("\n")}`)) {
26658
+ await streaming.closeAll();
26659
+ if (!budgetReachedFlag.get()) {
26660
+ budgetReachedFlag.set(true);
26661
+ void postBudgetReached({
26662
+ sessionId: opts.sessionId,
26663
+ pluginId: opts.pluginId,
26664
+ pluginAuthToken: opts.pluginAuthToken,
26665
+ agent: opts.agent,
26666
+ period: extractBudgetPeriod(`${detail}
26667
+ ${recentStderr.join("\n")}`)
26668
+ });
26669
+ }
26670
+ await budgetRecovery.offer(cmd.id, blocks, `${detail}
26671
+ ${recentStderr.join("\n")}`);
26672
+ return;
26673
+ }
26359
26674
  if (shouldOfferOneMRecovery({ detail, recentStderr: recentStderr.join("\n"), finalText: "" })) {
26360
26675
  await streaming.closeAll();
26361
26676
  await oneMRecovery.offer(cmd.id, blocks);
@@ -26462,6 +26777,7 @@ async function handleCommand(cmd, client2, relay, acpSessionId, models, streamin
26462
26777
  const index = typeof payload?.index === "number" ? payload.index : 0;
26463
26778
  const offset = typeof payload?.from === "number" ? payload.from : 0;
26464
26779
  const absoluteIndex = index + offset;
26780
+ if (await budgetRecovery.tryRecover(cmd.id, absoluteIndex)) return;
26465
26781
  const result = streaming.resolveSelection(absoluteIndex);
26466
26782
  switch (result.kind) {
26467
26783
  case "resolved":
@@ -30602,7 +30918,7 @@ function checkChokidar() {
30602
30918
  }
30603
30919
  async function doctor(args2 = []) {
30604
30920
  const json = args2.includes("--json");
30605
- const cliVersion = true ? "2.50.0" : "0.0.0-dev";
30921
+ const cliVersion = true ? "2.51.0" : "0.0.0-dev";
30606
30922
  const apiBase2 = resolveApiBaseUrl();
30607
30923
  const diagnosticId = (0, import_node_crypto8.randomUUID)();
30608
30924
  log.info("doctor", `run id=${diagnosticId} cli=${cliVersion}`);
@@ -30801,7 +31117,7 @@ async function completion(args2) {
30801
31117
  // src/commands/version.ts
30802
31118
  var import_picocolors15 = __toESM(require("picocolors"));
30803
31119
  function version2() {
30804
- const v = true ? "2.50.0" : "unknown";
31120
+ const v = true ? "2.51.0" : "unknown";
30805
31121
  console.log(`${import_picocolors15.default.bold("codeam-cli")} ${import_picocolors15.default.cyan(v)}`);
30806
31122
  }
30807
31123
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeam-cli",
3
- "version": "2.50.0",
3
+ "version": "2.51.0",
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",