usebeeline 0.0.123 → 0.0.124

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/README.md +1 -1
  2. package/dist/usebeeline.mjs +1843 -1412
  3. package/package.json +1 -1
@@ -11315,10 +11315,10 @@ __export(self_update_exports, {
11315
11315
  });
11316
11316
  import { createHash as createHash8 } from "node:crypto";
11317
11317
  import { constants as fsConstants2 } from "node:fs";
11318
- import { access as access2, chmod as chmod6, lstat as lstat4, mkdir as mkdir18, open, readFile as readFile11, rename as rename6, rm as rm6, symlink as symlink3, writeFile as writeFile11 } from "node:fs/promises";
11319
- import { spawn as spawn8 } from "node:child_process";
11318
+ import { access as access2, chmod as chmod6, lstat as lstat4, mkdir as mkdir18, open, readFile as readFile11, rename as rename6, rm as rm7, symlink as symlink3, writeFile as writeFile11 } from "node:fs/promises";
11319
+ import { spawn as spawn9 } from "node:child_process";
11320
11320
  import { homedir as homedir12 } from "node:os";
11321
- import { dirname as dirname16, join as join13, resolve as resolve27 } from "node:path";
11321
+ import { dirname as dirname16, join as join14, resolve as resolve27 } from "node:path";
11322
11322
  function anchorLayout(rawLibDir) {
11323
11323
  const libDir = resolve27(rawLibDir);
11324
11324
  const segments = libDir.split(/[/\\]/);
@@ -11391,7 +11391,7 @@ function hostPlatformKey() {
11391
11391
  return `${os}-${arch}`;
11392
11392
  }
11393
11393
  function bundleJsonCandidates(bundleDir) {
11394
- return [join13(bundleDir, "lib", "beeline", "bundle.json"), join13(bundleDir, "bundle.json")];
11394
+ return [join14(bundleDir, "lib", "beeline", "bundle.json"), join14(bundleDir, "bundle.json")];
11395
11395
  }
11396
11396
  async function readBundleJson(bundleDir) {
11397
11397
  let raw;
@@ -11415,7 +11415,7 @@ async function readBundleJson(bundleDir) {
11415
11415
  }
11416
11416
  }
11417
11417
  function updateStatePath(layout) {
11418
- return join13(layout.releasesRoot, ".state", "update-state.json");
11418
+ return join14(layout.releasesRoot, ".state", "update-state.json");
11419
11419
  }
11420
11420
  async function readUpdateState(layout) {
11421
11421
  try {
@@ -11425,7 +11425,7 @@ async function readUpdateState(layout) {
11425
11425
  }
11426
11426
  }
11427
11427
  async function writeUpdateState(layout, state) {
11428
- await mkdir18(join13(layout.releasesRoot, ".state"), { recursive: true });
11428
+ await mkdir18(join14(layout.releasesRoot, ".state"), { recursive: true });
11429
11429
  await writeFile11(updateStatePath(layout), `${JSON.stringify(state, null, 2)}
11430
11430
  `, "utf8");
11431
11431
  }
@@ -11489,7 +11489,7 @@ async function fetchText(url, fetchImpl) {
11489
11489
  }
11490
11490
  function run(command, args, timeoutMs) {
11491
11491
  return new Promise((resolveRun) => {
11492
- const child = spawn8(command, args, { stdio: ["ignore", "ignore", "pipe"] });
11492
+ const child = spawn9(command, args, { stdio: ["ignore", "ignore", "pipe"] });
11493
11493
  let stderr = "";
11494
11494
  child.stderr?.setEncoding("utf8");
11495
11495
  child.stderr?.on("data", (chunk) => {
@@ -11507,7 +11507,7 @@ function run(command, args, timeoutMs) {
11507
11507
  });
11508
11508
  }
11509
11509
  function entrypointCandidates(bundleDir) {
11510
- return [join13(bundleDir, BUNDLE_ENTRYPOINT), join13(bundleDir, "beeline-cli.mjs")];
11510
+ return [join14(bundleDir, BUNDLE_ENTRYPOINT), join14(bundleDir, "beeline-cli.mjs")];
11511
11511
  }
11512
11512
  async function resolveBundleEntrypoint(bundleDir) {
11513
11513
  for (const candidate of entrypointCandidates(bundleDir)) {
@@ -11526,8 +11526,8 @@ async function stageRelease(layout, manifestUrl, published, opts = {}) {
11526
11526
  const fetchImpl = opts.fetchImpl ?? fetch;
11527
11527
  const log2 = opts.logger ?? ((line) => console.log(`[body] self-update: ${line}`));
11528
11528
  const releaseId = sanitizeReleaseId(published.commit ?? published.version ?? `release-${Date.now()}`);
11529
- const releaseDir = join13(layout.releasesRoot, releaseId);
11530
- const okMarker = join13(releaseDir, ".stage-ok");
11529
+ const releaseDir = join14(layout.releasesRoot, releaseId);
11530
+ const okMarker = join14(releaseDir, ".stage-ok");
11531
11531
  let previouslyVerified = false;
11532
11532
  try {
11533
11533
  const recorded = await readFile11(okMarker, "utf8");
@@ -11537,7 +11537,7 @@ async function stageRelease(layout, manifestUrl, published, opts = {}) {
11537
11537
  } catch {
11538
11538
  }
11539
11539
  await mkdir18(releaseDir, { recursive: true });
11540
- const tempArchive = join13(layout.releasesRoot, `.download-${releaseId}-${process.pid}.tar.gz`);
11540
+ const tempArchive = join14(layout.releasesRoot, `.download-${releaseId}-${process.pid}.tar.gz`);
11541
11541
  try {
11542
11542
  log2(`downloading ${published.file}`);
11543
11543
  const response = await fetchImpl(archiveUrlFor(manifestUrl, published.file), {
@@ -11559,7 +11559,7 @@ async function stageRelease(layout, manifestUrl, published, opts = {}) {
11559
11559
  }
11560
11560
  await writeFile11(tempArchive, Buffer.concat(chunks), { mode: 384 });
11561
11561
  const entries = (await new Promise((resolveList, rejectList) => {
11562
- const child = spawn8("tar", ["-tzf", tempArchive], { stdio: ["ignore", "pipe", "inherit"] });
11562
+ const child = spawn9("tar", ["-tzf", tempArchive], { stdio: ["ignore", "pipe", "inherit"] });
11563
11563
  let out = "";
11564
11564
  child.stdout?.on("data", (chunk) => {
11565
11565
  out += chunk.toString("utf8");
@@ -11577,13 +11577,13 @@ async function stageRelease(layout, manifestUrl, published, opts = {}) {
11577
11577
  throw new Error(`extracting bundle failed: ${extract2.stderr}`);
11578
11578
  for (const relative3 of requiredBundlePaths()) {
11579
11579
  try {
11580
- await access2(join13(releaseDir, relative3), fsConstants2.F_OK);
11580
+ await access2(join14(releaseDir, relative3), fsConstants2.F_OK);
11581
11581
  } catch {
11582
11582
  throw new Error(`staged bundle is missing ${relative3}`);
11583
11583
  }
11584
11584
  }
11585
11585
  if (opts.smokeTestCli !== false) {
11586
- const probe = await run(process.execPath, [join13(releaseDir, BUNDLE_ENTRYPOINT), "--version"], 6e4);
11586
+ const probe = await run(process.execPath, [join14(releaseDir, BUNDLE_ENTRYPOINT), "--version"], 6e4);
11587
11587
  if (probe.status !== 0) {
11588
11588
  throw new Error(`staged bundle failed its startup smoke test (--version exited ${probe.status})${probe.stderr ? `: ${probe.stderr.trim()}` : ""}`);
11589
11589
  }
@@ -11594,10 +11594,10 @@ async function stageRelease(layout, manifestUrl, published, opts = {}) {
11594
11594
  return releaseId;
11595
11595
  } catch (error) {
11596
11596
  if (!previouslyVerified)
11597
- await rm6(releaseDir, { recursive: true, force: true });
11597
+ await rm7(releaseDir, { recursive: true, force: true });
11598
11598
  throw error;
11599
11599
  } finally {
11600
- await rm6(tempArchive, { force: true });
11600
+ await rm7(tempArchive, { force: true });
11601
11601
  }
11602
11602
  }
11603
11603
  function forwarderScript(tool) {
@@ -11621,8 +11621,8 @@ async function replaceFile(path, contents, mode) {
11621
11621
  await rename6(temp, path);
11622
11622
  }
11623
11623
  async function activateRelease(layout, releaseId) {
11624
- const releaseDir = join13(layout.releasesRoot, releaseId);
11625
- await access2(join13(releaseDir, BUNDLE_ENTRYPOINT), fsConstants2.F_OK);
11624
+ const releaseDir = join14(layout.releasesRoot, releaseId);
11625
+ await access2(join14(releaseDir, BUNDLE_ENTRYPOINT), fsConstants2.F_OK);
11626
11626
  await mkdir18(layout.releasesRoot, { recursive: true });
11627
11627
  await mkdir18(layout.binDir, { recursive: true });
11628
11628
  let previousReleaseId = await activeReleaseId(layout);
@@ -11630,12 +11630,12 @@ async function activateRelease(layout, releaseId) {
11630
11630
  if (kind === "directory") {
11631
11631
  const legacyIdentity = await readBundleJson(layout.libDir);
11632
11632
  const legacyId = sanitizeReleaseId(legacyIdentity?.commit ?? legacyIdentity?.version ?? `legacy-${Date.now()}`);
11633
- const legacyDir = join13(layout.releasesRoot, legacyId);
11633
+ const legacyDir = join14(layout.releasesRoot, legacyId);
11634
11634
  try {
11635
11635
  await access2(legacyDir, fsConstants2.F_OK);
11636
11636
  previousReleaseId = `${legacyId}-${Date.now()}`;
11637
- await rename6(layout.libDir, join13(layout.releasesRoot, previousReleaseId));
11638
- await normalizeLegacyBundleShape(join13(layout.releasesRoot, previousReleaseId));
11637
+ await rename6(layout.libDir, join14(layout.releasesRoot, previousReleaseId));
11638
+ await normalizeLegacyBundleShape(join14(layout.releasesRoot, previousReleaseId));
11639
11639
  } catch {
11640
11640
  await rename6(layout.libDir, legacyDir);
11641
11641
  await normalizeLegacyBundleShape(legacyDir);
@@ -11643,19 +11643,19 @@ async function activateRelease(layout, releaseId) {
11643
11643
  }
11644
11644
  }
11645
11645
  const tempLink = `${layout.libDir}.new-${process.pid}`;
11646
- await rm6(tempLink, { force: true });
11647
- await symlink3(join13("beeline-releases", releaseId), tempLink);
11646
+ await rm7(tempLink, { force: true });
11647
+ await symlink3(join14("beeline-releases", releaseId), tempLink);
11648
11648
  await rename6(tempLink, layout.libDir);
11649
11649
  await fsyncDir(dirname16(layout.libDir));
11650
11650
  await writeBinForwarders(layout, releaseDir);
11651
11651
  return { previousReleaseId };
11652
11652
  }
11653
11653
  async function normalizeLegacyBundleShape(bundleDir) {
11654
- const innerLib = join13(bundleDir, "lib", "beeline");
11654
+ const innerLib = join14(bundleDir, "lib", "beeline");
11655
11655
  let anyFlat = false;
11656
11656
  for (const name of LEGACY_FLAT_BUNDLE_FILES) {
11657
11657
  try {
11658
- await access2(join13(bundleDir, name), fsConstants2.F_OK);
11658
+ await access2(join14(bundleDir, name), fsConstants2.F_OK);
11659
11659
  anyFlat = true;
11660
11660
  break;
11661
11661
  } catch {
@@ -11666,11 +11666,11 @@ async function normalizeLegacyBundleShape(bundleDir) {
11666
11666
  await mkdir18(innerLib, { recursive: true });
11667
11667
  for (const name of LEGACY_FLAT_BUNDLE_FILES) {
11668
11668
  try {
11669
- await access2(join13(innerLib, name), fsConstants2.F_OK);
11669
+ await access2(join14(innerLib, name), fsConstants2.F_OK);
11670
11670
  continue;
11671
11671
  } catch {
11672
11672
  }
11673
- await rename6(join13(bundleDir, name), join13(innerLib, name)).catch(() => void 0);
11673
+ await rename6(join14(bundleDir, name), join14(innerLib, name)).catch(() => void 0);
11674
11674
  }
11675
11675
  }
11676
11676
  async function writeBinForwarders(layout, activeBundleRoot) {
@@ -11679,13 +11679,13 @@ async function writeBinForwarders(layout, activeBundleRoot) {
11679
11679
  ...Object.entries(FORWARDER_ALIASES).map(([alias, tool]) => [alias, tool])
11680
11680
  ];
11681
11681
  for (const [name, tool] of entries) {
11682
- const target = join13(activeBundleRoot, "bin", tool);
11682
+ const target = join14(activeBundleRoot, "bin", tool);
11683
11683
  try {
11684
11684
  await access2(target, fsConstants2.X_OK);
11685
11685
  } catch {
11686
11686
  continue;
11687
11687
  }
11688
- await replaceFile(join13(layout.binDir, name), forwarderScript(tool), 493);
11688
+ await replaceFile(join14(layout.binDir, name), forwarderScript(tool), 493);
11689
11689
  }
11690
11690
  }
11691
11691
  async function repairInstallForwarders(layout, opts = {}) {
@@ -11694,7 +11694,7 @@ async function repairInstallForwarders(layout, opts = {}) {
11694
11694
  const forwarderHealthy = async (name, tool) => {
11695
11695
  let current;
11696
11696
  try {
11697
- current = await readFile11(join13(layout.binDir, name), "utf8");
11697
+ current = await readFile11(join14(layout.binDir, name), "utf8");
11698
11698
  } catch {
11699
11699
  current = void 0;
11700
11700
  }
@@ -11713,19 +11713,19 @@ async function repairInstallForwarders(layout, opts = {}) {
11713
11713
  return true;
11714
11714
  }
11715
11715
  async function rollbackToPreviousRelease(layout, previousReleaseId) {
11716
- const releaseDir = join13(layout.releasesRoot, previousReleaseId);
11716
+ const releaseDir = join14(layout.releasesRoot, previousReleaseId);
11717
11717
  const entrypoint = await resolveBundleEntrypoint(releaseDir);
11718
11718
  if (!entrypoint) {
11719
11719
  throw new Error(`release ${previousReleaseId} has no runnable CLI entrypoint`);
11720
11720
  }
11721
11721
  const tempLink = `${layout.libDir}.rollback-${process.pid}`;
11722
- await rm6(tempLink, { force: true });
11723
- await symlink3(join13("beeline-releases", previousReleaseId), tempLink);
11722
+ await rm7(tempLink, { force: true });
11723
+ await symlink3(join14("beeline-releases", previousReleaseId), tempLink);
11724
11724
  await rename6(tempLink, layout.libDir);
11725
11725
  await fsyncDir(dirname16(layout.libDir));
11726
11726
  }
11727
11727
  function updateAttemptPath(layout) {
11728
- return join13(layout.releasesRoot, ".state", "update-attempt.json");
11728
+ return join14(layout.releasesRoot, ".state", "update-attempt.json");
11729
11729
  }
11730
11730
  async function readUpdateAttempt(layout) {
11731
11731
  try {
@@ -11739,7 +11739,7 @@ async function readUpdateAttempt(layout) {
11739
11739
  }
11740
11740
  }
11741
11741
  async function writeUpdateAttempt(layout, record3) {
11742
- await mkdir18(join13(layout.releasesRoot, ".state"), { recursive: true });
11742
+ await mkdir18(join14(layout.releasesRoot, ".state"), { recursive: true });
11743
11743
  const path = updateAttemptPath(layout);
11744
11744
  const staged = `${path}.${process.pid}.tmp`;
11745
11745
  await writeFile11(staged, `${JSON.stringify(record3, null, 2)}
@@ -13438,6 +13438,7 @@ import { accessSync as accessSync2, constants as constants2, existsSync as exist
13438
13438
  import { basename as basename2, dirname as dirname2, resolve as resolve2 } from "node:path";
13439
13439
 
13440
13440
  // apps/body/dist/agent-command.js
13441
+ import { spawn as spawn3 } from "node:child_process";
13441
13442
  import { accessSync, constants, existsSync as existsSync2, readdirSync } from "node:fs";
13442
13443
  import { homedir } from "node:os";
13443
13444
  import { delimiter, isAbsolute, join, resolve } from "node:path";
@@ -14040,17 +14041,44 @@ var AGENT_EXECUTABLES = {
14040
14041
  var ADAPTER_INSTALL_COMMANDS = {
14041
14042
  codex: {
14042
14043
  command: "npm",
14043
- args: ["install", "-g", "@agentclientprotocol/codex-acp"]
14044
+ args: ["install", "-g", "@agentclientprotocol/codex-acp@latest"]
14044
14045
  },
14045
14046
  claude: {
14046
14047
  command: "npm",
14047
- args: ["install", "-g", "@agentclientprotocol/claude-agent-acp"]
14048
+ args: ["install", "-g", "@agentclientprotocol/claude-agent-acp@latest"]
14048
14049
  },
14049
14050
  pi: {
14050
14051
  command: "npm",
14051
- args: ["install", "-g", "pi-acp"]
14052
+ args: ["install", "-g", "pi-acp@latest"]
14052
14053
  }
14053
14054
  };
14055
+ function latestAdapterInstallCommand(kind) {
14056
+ return kind === "codex" || kind === "claude" || kind === "pi" ? ADAPTER_INSTALL_COMMANDS[kind] : void 0;
14057
+ }
14058
+ async function runAdapterInstall(install, opts = {}) {
14059
+ await new Promise((resolveInstall, rejectInstall) => {
14060
+ const child = spawn3(install.command, install.args, {
14061
+ ...opts.cwd ? { cwd: opts.cwd } : {},
14062
+ env: opts.env ?? process.env,
14063
+ stdio: opts.stdio ?? "inherit"
14064
+ });
14065
+ child.once("error", rejectInstall);
14066
+ child.once("exit", (code, signal) => {
14067
+ if (code === 0)
14068
+ resolveInstall();
14069
+ else {
14070
+ rejectInstall(new Error(signal ? `${install.command} was terminated by ${signal}` : `${install.command} exited with status ${code ?? "unknown"}`));
14071
+ }
14072
+ });
14073
+ });
14074
+ }
14075
+ async function installLatestAgentAdapter(kind, opts = {}) {
14076
+ const install = latestAdapterInstallCommand(kind);
14077
+ if (!install)
14078
+ return false;
14079
+ await runAdapterInstall(install, opts);
14080
+ return true;
14081
+ }
14054
14082
  function adapterInstallHint(kind) {
14055
14083
  return formatAdapterInstallCommand(ADAPTER_INSTALL_COMMANDS[kind]);
14056
14084
  }
@@ -14208,7 +14236,7 @@ function resolveAgentCommand(opts) {
14208
14236
  throw new Error("--agent-command may only be used with --agent custom");
14209
14237
  }
14210
14238
  if (typedKind === "codex") {
14211
- requireExecutable("codex", env, cwd, "Codex CLI not found. Install it with `npm install -g @openai/codex`, then retry with `--agent codex`.");
14239
+ requireExecutable("codex", env, cwd, "Codex CLI not found. Install it with `npm install -g @openai/codex@latest`, then retry with `--agent codex`.");
14212
14240
  const command2 = requireExecutable("codex-acp", env, cwd, `Codex ACP adapter not found. Install it with \`${adapterInstallHint("codex")}\`, then retry with \`--agent codex\`.`);
14213
14241
  return { kind: typedKind, command: command2, args: [] };
14214
14242
  }
@@ -14225,7 +14253,7 @@ function resolveAgentCommand(opts) {
14225
14253
  return { kind: typedKind, command: command2, args: ["acp"] };
14226
14254
  }
14227
14255
  if (typedKind === "pi") {
14228
- requireExecutable("pi", env, cwd, "Pi coding agent not found. Install it with `npm install -g @mariozechner/pi-coding-agent`, then retry with `--agent pi`.");
14256
+ requireExecutable("pi", env, cwd, "Pi coding agent not found. Install it with `npm install -g @mariozechner/pi-coding-agent@latest`, then retry with `--agent pi`.");
14229
14257
  const command2 = requireExecutable("pi-acp", env, cwd, `Pi needs an ACP adapter. Install it with \`${adapterInstallHint("pi")}\`, then retry with \`--agent pi\`.`);
14230
14258
  return { kind: typedKind, command: command2, args: [] };
14231
14259
  }
@@ -14607,7 +14635,7 @@ function parseSandboxMaskEnv(env) {
14607
14635
  }
14608
14636
 
14609
14637
  // apps/body/dist/squire-host.js
14610
- import { spawn as spawn3 } from "node:child_process";
14638
+ import { spawn as spawn4 } from "node:child_process";
14611
14639
  import { existsSync as existsSync4, lstatSync, mkdirSync } from "node:fs";
14612
14640
  import { createRequire as createRequire2 } from "node:module";
14613
14641
  import { homedir as homedir2 } from "node:os";
@@ -14706,7 +14734,7 @@ WantedBy=default.target
14706
14734
  function spawnSquireServer(env, locateNpx) {
14707
14735
  const launch = locateNpx ? squireServerCommand() : { command: "npx", args: [...SQUIRE_SERVER_ARGS], pathPrefix: "" };
14708
14736
  const path = [launch.pathPrefix, env.PATH || "/usr/bin:/bin"].filter(Boolean).join(":");
14709
- const child = spawn3(launch.command, launch.args, {
14737
+ const child = spawn4(launch.command, launch.args, {
14710
14738
  env: { ...env, PATH: path },
14711
14739
  stdio: "inherit"
14712
14740
  });
@@ -14745,1317 +14773,1317 @@ var DEFAULT_ACCESS_POLICY = DEFAULT_AGENT_ACCESS_POLICY;
14745
14773
  var LEGACY_ACCESS_POLICY = "everyone";
14746
14774
  var ACCESS_REFUSAL_WINDOW_MS = 60 * 60 * 1e3;
14747
14775
 
14748
- // apps/body/dist/model-catalog.js
14749
- import { mkdtemp, rm } from "node:fs/promises";
14750
- import { tmpdir } from "node:os";
14751
- import { resolve as resolve5 } from "node:path";
14752
-
14753
- // apps/body/dist/acp.js
14754
- import { spawn as spawn4 } from "node:child_process";
14755
- import { EventEmitter } from "node:events";
14756
-
14757
- // apps/body/dist/harness-capabilities.js
14758
- function harnessHonorsSessionSystemPrompt(agentCommand) {
14759
- return Boolean(agentCommand && /(^|[/\\])claude-(agent|code)-acp(\.[a-z]+)?$/i.test(agentCommand));
14776
+ // apps/body/dist/model-types.js
14777
+ var ALLOWED = /* @__PURE__ */ new Set(["model", "thought_level", "effort", "reasoning_effort"]);
14778
+ function isAllowedAgentModelConfigCategory(category) {
14779
+ return ALLOWED.has(category);
14760
14780
  }
14761
- var GROK_WARM_SESSION_IDLE_MS = 30 * 6e4;
14762
- function cornerAutonomyModeCandidates(agentCommand) {
14763
- if (agentCommand && /(^|[/\\])codex-acp(\.[a-z]+)?$/i.test(agentCommand)) {
14764
- return ["agent-full-access"];
14765
- }
14766
- if (agentCommand && /(^|[/\\])claude-(agent|code)-acp(\.[a-z]+)?$/i.test(agentCommand)) {
14767
- return ["bypassPermissions"];
14768
- }
14769
- if (agentCommand && /(^|[/\\])grok(\.[a-z]+)?$/i.test(agentCommand))
14770
- return [];
14771
- if (agentCommand && /(^|[/\\])pi-acp(\.[a-z]+)?$/i.test(agentCommand))
14772
- return [];
14773
- return ["agent", "edit", "code"];
14781
+
14782
+ // apps/body/dist/model-config.js
14783
+ import { lstatSync as lstatSync2, readFileSync as readFileSync2 } from "node:fs";
14784
+ import { basename as basename3, resolve as resolve4 } from "node:path";
14785
+ var GROK_SESSION_MODEL_AXIS_ID = "beeline:grok-session-model";
14786
+ var GROK_LAUNCH_EFFORT_AXIS_ID = "beeline:grok-launch-reasoning-effort";
14787
+ function isGrokAgentCommand(agent) {
14788
+ if (agent.kind === "grok")
14789
+ return true;
14790
+ if (agent.kind && agent.kind !== "custom")
14791
+ return false;
14792
+ return /^grok(?:\.[a-z0-9]+)?$/i.test(basename3(agent.command));
14774
14793
  }
14775
- function roomModeCandidates(agentCommand, options = {}) {
14776
- if (options.osSandbox && agentCommand && /(^|[/\\])codex-acp(\.[a-z]+)?$/i.test(agentCommand)) {
14777
- return ["agent-full-access"];
14794
+ function agentArgsWithModelSelection(agent, selection) {
14795
+ const original = [...agent.args];
14796
+ if (!selection || !isGrokAgentCommand(agent) || !original.includes("agent") || !original.includes("stdio")) {
14797
+ return original;
14778
14798
  }
14779
- return ["read-only", "readonly"];
14780
- }
14781
-
14782
- // apps/body/dist/harness-tool-scope.js
14783
- var CLAUDE_TOOL_SCOPE_SETTINGS = { disableClaudeAiConnectors: true };
14784
- var CLAUDE_PROFILE = {
14785
- // `settingSources: ['user']` admits the sanitized user MCP copy
14786
- // while rejecting repository/local/plugin MCP. `strictMcpConfig` cannot be
14787
- // used because it would reject the user copy too. Account-bound claude.ai
14788
- // cloud connectors remain off independently.
14789
- enforcement: "config-isolated",
14790
- note: "claude-agent-acp forwards _meta.claudeCode.options to the Claude Agent SDK; user-only settingSources loads MCP from the isolated CLAUDE_CONFIG_DIR without repository/local settings, while disableClaudeAiConnectors keeps account-bound connectors off",
14791
- sessionMeta: {
14792
- claudeCode: {
14793
- options: {
14794
- // Only the sanitized user config in the isolated CLAUDE_CONFIG_DIR;
14795
- // never a repository's .mcp.json, local settings, or plugins.
14796
- settingSources: ["user"],
14797
- // Account-bound claude.ai cloud connectors are not fetched at all.
14798
- settings: CLAUDE_TOOL_SCOPE_SETTINGS
14799
- }
14800
- }
14801
- },
14802
- metaSystemPrompt: true
14803
- };
14804
- var PROFILES = [
14805
- { match: /(^|[/\\])claude-(agent|code)-acp(\.[a-z]+)?$/i, profile: CLAUDE_PROFILE },
14806
- {
14807
- match: /(^|[/\\])codex-acp(\.[a-z]+)?$/i,
14808
- profile: {
14809
- enforcement: "config-isolated",
14810
- note: "codex-acp merges the session's MCP servers into $CODEX_HOME/config.toml's own [mcp_servers] instead of replacing them, so only an isolated CODEX_HOME scopes the session; the ChatGPT account's codex_apps connectors survive either way"
14811
- }
14812
- },
14813
- {
14814
- match: /(^|[/\\])pi-acp(\.[a-z]+)?$/i,
14815
- profile: {
14816
- enforcement: "config-isolated",
14817
- note: "pi-acp 0.0.33 still stores session/new mcpServers and never reads them; pi 0.85.1 itself has no MCP client, while optional pi-mcp-adapter reads $PI_CODING_AGENT_DIR/mcp.json and isolated homes exclude the settings.json that would load it"
14818
- }
14819
- },
14820
- {
14821
- match: /(^|[/\\])grok(\.[a-z]+)?$/i,
14822
- profile: {
14823
- enforcement: "config-isolated",
14824
- note: "grok loads the operator's own ~/.grok MCP servers even when session/new passes an empty mcpServers list (measured live), so only an isolated GROK_HOME scopes the session; no account-bound connector surface was found"
14825
- }
14826
- },
14827
- {
14828
- match: /(^|[/\\])buzz-agent(\.[a-z]+)?$/i,
14829
- profile: {
14830
- enforcement: "allowlisted",
14831
- note: "buzz-agent mounts only the MCP servers the daemon passes; it has no operator-global tool config"
14799
+ const stripped = [];
14800
+ for (let index = 0; index < original.length; index += 1) {
14801
+ const arg = original[index];
14802
+ if (arg === "-m" || arg === "--model" || arg === "--reasoning-effort") {
14803
+ index += 1;
14804
+ continue;
14832
14805
  }
14833
- },
14834
- {
14835
- match: CURSOR_HARNESS_COMMAND,
14836
- profile: {
14837
- enforcement: "config-isolated",
14838
- note: "cursor-acp-bridge writes session/new mcpServers into the isolated CURSOR_HOME mcp.json and the isolated $HOME/.cursor/mcp.json cursor-agent loads; it never writes the operator ~/.cursor"
14806
+ if (arg.startsWith("--model=") || arg.startsWith("--reasoning-effort=") || /^-m=/.test(arg)) {
14807
+ continue;
14839
14808
  }
14809
+ stripped.push(arg);
14840
14810
  }
14841
- ];
14842
- function sessionToolScopeMeta(agentCommand) {
14843
- const meta = matchProfile(agentCommand)?.sessionMeta;
14844
- return meta ? JSON.parse(JSON.stringify(meta)) : void 0;
14845
- }
14846
- function harnessReadsMetaSystemPrompt(agentCommand) {
14847
- return matchProfile(agentCommand)?.metaSystemPrompt === true;
14811
+ const stdioIndex = stripped.lastIndexOf("stdio");
14812
+ const configured = [
14813
+ ...selection.model ? ["--model", selection.model] : [],
14814
+ ...selection.effort ? ["--reasoning-effort", selection.effort] : []
14815
+ ];
14816
+ stripped.splice(stdioIndex, 0, ...configured);
14817
+ return stripped;
14848
14818
  }
14849
- function matchProfile(agentCommand) {
14850
- if (!agentCommand)
14851
- return void 0;
14852
- for (const { match, profile } of PROFILES) {
14853
- if (match.test(agentCommand))
14854
- return profile;
14855
- }
14819
+ function advertisedChoiceId(choice) {
14820
+ if (typeof choice.id === "string" && choice.id.length > 0)
14821
+ return choice.id;
14822
+ if (typeof choice.value === "string" && choice.value.length > 0)
14823
+ return choice.value;
14856
14824
  return void 0;
14857
14825
  }
14858
-
14859
- // apps/body/dist/acp.js
14860
- var STDERR_TAIL_MAX_CHARS2 = 2e3;
14861
- function braceDelta(line) {
14862
- let delta = 0;
14863
- for (const char of line) {
14864
- if (char === "{")
14865
- delta += 1;
14866
- else if (char === "}")
14867
- delta -= 1;
14868
- }
14869
- return delta;
14826
+ function sortModelChoicesNewestFirst(choices) {
14827
+ return choices.map((choice, index) => ({ choice, index })).sort((left, right) => {
14828
+ const leftParts = left.choice.id.match(/\d+/g)?.map(Number) ?? [];
14829
+ const rightParts = right.choice.id.match(/\d+/g)?.map(Number) ?? [];
14830
+ for (let index = 0; index < Math.max(leftParts.length, rightParts.length); index += 1) {
14831
+ const delta = (rightParts[index] ?? -1) - (leftParts[index] ?? -1);
14832
+ if (delta !== 0)
14833
+ return delta;
14834
+ }
14835
+ return left.index - right.index;
14836
+ }).map(({ choice }) => choice);
14870
14837
  }
14871
- function meaningfulHarnessStderr(tail) {
14872
- const lines = tail.split(/\r?\n/);
14873
- const kept = [];
14874
- for (let index = 0; index < lines.length; index += 1) {
14875
- const line = lines[index];
14876
- if (!/error handling notification/i.test(line)) {
14877
- kept.push(line);
14838
+ function parseAdvertisedConfigOptions(raw, preferredModelId, includeLaunchEffort = false) {
14839
+ const configOptions = raw?.configOptions;
14840
+ const result = [];
14841
+ for (const entry of Array.isArray(configOptions) ? configOptions : []) {
14842
+ if (!entry || typeof entry !== "object")
14878
14843
  continue;
14844
+ const record3 = entry;
14845
+ const id = record3.id;
14846
+ const category = record3.category;
14847
+ if (typeof id !== "string" || typeof category !== "string")
14848
+ continue;
14849
+ const rawChoices = Array.isArray(record3.options) ? record3.options : [];
14850
+ const choices = [];
14851
+ for (const rawChoice of rawChoices) {
14852
+ if (!rawChoice || typeof rawChoice !== "object")
14853
+ continue;
14854
+ const choice = rawChoice;
14855
+ const choiceId = advertisedChoiceId(choice);
14856
+ if (!choiceId)
14857
+ continue;
14858
+ choices.push({
14859
+ id: choiceId,
14860
+ ...typeof choice.name === "string" ? { name: choice.name } : {}
14861
+ });
14879
14862
  }
14880
- const block2 = [line];
14881
- let depth = braceDelta(line);
14882
- if (depth === 0 && lines[index + 1]?.trimStart().startsWith("{")) {
14883
- index += 1;
14884
- block2.push(lines[index]);
14885
- depth = braceDelta(lines[index]);
14886
- }
14887
- while (depth > 0 && index + 1 < lines.length) {
14888
- index += 1;
14889
- block2.push(lines[index]);
14890
- depth += braceDelta(lines[index]);
14891
- }
14892
- if (!/notifications\/initialized/.test(block2.join("\n")))
14893
- kept.push(...block2);
14894
- }
14895
- return kept.join("\n").trim();
14896
- }
14897
- function flattenHarnessCause(tail) {
14898
- return meaningfulHarnessStderr(tail).replace(/\s+/g, " ").trim();
14899
- }
14900
- var AcpRequestTimeoutError = class extends Error {
14901
- method;
14902
- timeoutMs;
14903
- inactivity;
14904
- code = "ACP_REQUEST_TIMEOUT";
14905
- constructor(method, timeoutMs, stderrTail = "", inactivity = false, detail = "") {
14906
- const meaningful = flattenHarnessCause(stderrTail);
14907
- const suffix = meaningful ? `; harness stderr: ${meaningful}` : "";
14908
- super(`ACP ${method} timed out after ${timeoutMs}ms${inactivity ? " of inactivity" : ""}${detail ? ` (${detail})` : ""}${suffix}`);
14909
- this.method = method;
14910
- this.timeoutMs = timeoutMs;
14911
- this.inactivity = inactivity;
14912
- this.name = "AcpRequestTimeoutError";
14863
+ result.push({
14864
+ id,
14865
+ category,
14866
+ ...typeof record3.currentValue === "string" ? { currentValue: record3.currentValue } : {},
14867
+ options: category === "model" ? sortModelChoicesNewestFirst(choices) : choices
14868
+ });
14913
14869
  }
14914
- };
14915
- function killChildProcessGroup(child, signal) {
14916
- if (!child.pid)
14917
- return;
14918
- try {
14919
- if (process.platform === "win32")
14920
- child.kill(signal);
14921
- else
14922
- process.kill(-child.pid, signal);
14923
- } catch (error) {
14924
- if (error.code !== "ESRCH")
14925
- throw error;
14870
+ const models = raw?.models;
14871
+ if (!models || typeof models !== "object")
14872
+ return result;
14873
+ const modelState = models;
14874
+ const availableModels = Array.isArray(modelState.availableModels) ? modelState.availableModels : [];
14875
+ const modelChoices = [];
14876
+ for (const entry of availableModels) {
14877
+ if (!entry || typeof entry !== "object")
14878
+ continue;
14879
+ const model = entry;
14880
+ if (typeof model.modelId !== "string" || !model.modelId)
14881
+ continue;
14882
+ modelChoices.push({
14883
+ id: model.modelId,
14884
+ ...typeof model.name === "string" ? { name: model.name } : {}
14885
+ });
14926
14886
  }
14927
- }
14928
- function promptPayloadNote(prompt) {
14929
- if (typeof prompt === "string")
14930
- return "";
14931
- const images = prompt.filter((block2) => block2.type === "image");
14932
- if (!images.length)
14933
- return "";
14934
- const bytes = images.reduce((total, block2) => total + Math.floor(block2.data.length * 3 / 4), 0);
14935
- return `prompt carried ${images.length} inline image${images.length === 1 ? "" : "s"}, ${Math.round(bytes / 1024)} KB`;
14936
- }
14937
- function parseAvailableCommands(value) {
14938
- if (!Array.isArray(value))
14939
- return [];
14940
- const seen = /* @__PURE__ */ new Set();
14941
- const result = [];
14942
- for (const entry of value) {
14887
+ const currentModelId = typeof modelState.currentModelId === "string" ? modelState.currentModelId : void 0;
14888
+ if (modelChoices.length > 0 && !result.some((axis) => axis.category === "model")) {
14889
+ result.push({
14890
+ id: GROK_SESSION_MODEL_AXIS_ID,
14891
+ category: "model",
14892
+ ...currentModelId ? { currentValue: currentModelId } : {},
14893
+ options: sortModelChoicesNewestFirst(modelChoices)
14894
+ });
14895
+ }
14896
+ const effortModelId = preferredModelId ?? currentModelId;
14897
+ const currentModel = availableModels.find((entry) => entry && typeof entry === "object" && entry.modelId === effortModelId);
14898
+ const meta = currentModel?._meta;
14899
+ const modelMeta = meta && typeof meta === "object" ? meta : void 0;
14900
+ const efforts = Array.isArray(modelMeta?.reasoningEfforts) ? modelMeta.reasoningEfforts : [];
14901
+ const effortChoices = [];
14902
+ for (const entry of efforts) {
14943
14903
  if (!entry || typeof entry !== "object")
14944
14904
  continue;
14945
- const record3 = entry;
14946
- const rawName = typeof record3.name === "string" ? record3.name.trim().replace(/^\/+/, "") : "";
14947
- if (!rawName || rawName.length > 80 || seen.has(rawName))
14905
+ const effort = entry;
14906
+ const id = advertisedChoiceId(effort);
14907
+ if (!id)
14948
14908
  continue;
14949
- seen.add(rawName);
14950
- const description = typeof record3.description === "string" && record3.description.trim() ? record3.description.trim().slice(0, 300) : void 0;
14951
- const input = record3.input;
14952
- const hintRaw = input && typeof input.hint === "string" ? input.hint : void 0;
14953
- const inputHint = hintRaw && hintRaw.trim() ? hintRaw.trim().slice(0, 120) : void 0;
14909
+ effortChoices.push({
14910
+ id,
14911
+ ...typeof effort.label === "string" ? { name: effort.label } : typeof effort.name === "string" ? { name: effort.name } : {}
14912
+ });
14913
+ }
14914
+ const hasEffortAxis = result.some((axis) => ["thought_level", "effort", "reasoning_effort"].includes(axis.category));
14915
+ if (includeLaunchEffort && effortChoices.length > 0 && !hasEffortAxis) {
14916
+ const currentEffort = typeof modelMeta?.reasoningEffort === "string" ? modelMeta.reasoningEffort : efforts.find((entry) => entry && typeof entry === "object" && entry.default)?.value;
14954
14917
  result.push({
14955
- name: rawName,
14956
- ...description ? { description } : {},
14957
- ...inputHint ? { inputHint } : {}
14918
+ id: GROK_LAUNCH_EFFORT_AXIS_ID,
14919
+ category: "reasoning_effort",
14920
+ ...typeof currentEffort === "string" ? { currentValue: currentEffort } : {},
14921
+ options: effortChoices
14958
14922
  });
14959
- if (result.length >= 200)
14960
- break;
14961
14923
  }
14962
14924
  return result;
14963
14925
  }
14964
- function agentMessageChunkText(update) {
14965
- if (update.sessionUpdate !== "agent_message_chunk")
14966
- return "";
14967
- const content = update.content;
14968
- if (content?.type === "text")
14969
- return content.text ?? "";
14970
- if (typeof update.content === "string")
14971
- return update.content;
14972
- return "";
14973
- }
14974
- var CHUNK_CONTINUES_PREVIOUS_WORD = /^[\s'\u2018\u2019\u02bc.,!?;:%)\]}-]/;
14975
- function continuesPreviousWord(current, delta) {
14976
- if (/\s$/.test(current))
14977
- return false;
14978
- return CHUNK_CONTINUES_PREVIOUS_WORD.test(delta);
14979
- }
14980
- var PI_ACP_HARNESS = /(^|[/\\])pi-acp(?:\.[a-z]+)?$/i;
14981
- function isPiAcpHarness(agentLabel) {
14982
- return Boolean(agentLabel && PI_ACP_HARNESS.test(agentLabel));
14926
+ function filterAllowedModelConfigOptions(options) {
14927
+ return options.filter((option) => isAllowedAgentModelConfigCategory(option.category));
14983
14928
  }
14984
- function normalizeStreamDelta(text2, _agentLabel) {
14985
- return text2;
14929
+ var PROVIDER_CREDENTIAL_ENV_HINTS = {
14930
+ openai: ["OPENAI_API_KEY", "OPENAI_COMPAT_API_KEY"],
14931
+ openrouter: ["OPENROUTER_API_KEY"],
14932
+ anthropic: ["ANTHROPIC_API_KEY"],
14933
+ google: ["GOOGLE_API_KEY", "GEMINI_API_KEY"],
14934
+ groq: ["GROQ_API_KEY"],
14935
+ mistral: ["MISTRAL_API_KEY"],
14936
+ deepseek: ["DEEPSEEK_API_KEY"],
14937
+ xai: ["XAI_API_KEY"],
14938
+ cohere: ["COHERE_API_KEY"],
14939
+ together: ["TOGETHER_API_KEY"],
14940
+ fireworks: ["FIREWORKS_API_KEY"]
14941
+ };
14942
+ function credentialedProviders(env) {
14943
+ const held = /* @__PURE__ */ new Set();
14944
+ for (const [provider, names] of Object.entries(PROVIDER_CREDENTIAL_ENV_HINTS)) {
14945
+ if (names.some((name) => Boolean(env[name]?.trim())))
14946
+ held.add(provider);
14947
+ }
14948
+ for (const provider of credentialedPiCustomProviders(env))
14949
+ held.add(provider);
14950
+ return held;
14986
14951
  }
14987
- function agentMessageRuns(updates, agentLabel) {
14988
- const runs = [];
14989
- let current = "";
14990
- let lastWasText = false;
14991
- for (const u4 of updates) {
14992
- const delta = normalizeStreamDelta(agentMessageChunkText(u4.update), agentLabel);
14993
- if (!delta) {
14994
- lastWasText = false;
14995
- continue;
14952
+ function credentialedPiCustomProviders(env) {
14953
+ const agentDir = env.PI_CODING_AGENT_DIR?.trim() ? resolve4(env.PI_CODING_AGENT_DIR) : env.HOME?.trim() ? resolve4(env.HOME, ".pi/agent") : void 0;
14954
+ const held = /* @__PURE__ */ new Set();
14955
+ if (!agentDir)
14956
+ return held;
14957
+ const path = resolve4(agentDir, "models.json");
14958
+ try {
14959
+ const stats = lstatSync2(path);
14960
+ if (!stats.isFile() || stats.isSymbolicLink() || stats.nlink !== 1 || stats.size > 1048576) {
14961
+ return held;
14996
14962
  }
14997
- if (!lastWasText && current && !continuesPreviousWord(current, delta)) {
14998
- runs.push(current);
14999
- current = "";
14963
+ const parsed = JSON.parse(readFileSync2(path, "utf8"));
14964
+ const providers = parsed.providers;
14965
+ const remember = (name, config) => {
14966
+ if (typeof name !== "string" || !name.trim() || !config || typeof config !== "object")
14967
+ return;
14968
+ const apiKey = config.apiKey;
14969
+ if (typeof apiKey === "string" && apiKey.trim())
14970
+ held.add(name.trim());
14971
+ };
14972
+ if (Array.isArray(providers)) {
14973
+ for (const provider of providers) {
14974
+ const record3 = provider;
14975
+ remember(record3?.name ?? record3?.id, record3);
14976
+ }
14977
+ } else if (providers && typeof providers === "object") {
14978
+ for (const [name, config] of Object.entries(providers)) {
14979
+ remember(name, config);
14980
+ }
15000
14981
  }
15001
- current += delta;
15002
- lastWasText = true;
14982
+ } catch {
15003
14983
  }
15004
- if (current)
15005
- runs.push(current);
15006
- const seen = /* @__PURE__ */ new Set();
15007
- return runs.filter((run2) => {
15008
- if (seen.has(run2))
15009
- return false;
15010
- seen.add(run2);
15011
- return true;
14984
+ return held;
14985
+ }
14986
+ function filterModelOptionsByCredentials(options, env) {
14987
+ const held = credentialedProviders(env);
14988
+ return options.map((option) => {
14989
+ if (option.category !== "model")
14990
+ return option;
14991
+ return {
14992
+ ...option,
14993
+ options: option.options.filter((choice) => {
14994
+ const slash = choice.id.indexOf("/");
14995
+ if (slash < 0)
14996
+ return true;
14997
+ return held.has(choice.id.slice(0, slash));
14998
+ })
14999
+ };
15012
15000
  });
15013
15001
  }
15014
- var RETRY_NARRATION_FRAGMENTS = [
15015
- // `Retrying (attempt 2/3, waiting 4s)` — the pi/ox-alpha shape.
15016
- /retrying\s*\((?:attempt|try)\s*\d+\s*\/\s*\d+(?:,\s*(?:waiting|backoff)\s*\d+(?:\.\d+)?s)?\)/gi,
15017
- // A bare `(attempt 2/3...)` or `[attempt 2/3]` qualifier on its own.
15018
- /[([](?:attempt|try)\s*\d+\s*\/\s*\d+(?:,\s*(?:waiting|backoff)\s*\d+(?:\.\d+)?s)?[))]\s*/gi,
15019
- // Standalone narration words with their optional trailing ellipsis.
15020
- /retrying\s*\.{0,3}/gi,
15021
- /retried\s*\.{0,3}/gi,
15022
- /retry\s+finished,?\s*/gi,
15023
- /retry\s+failed,?\s*/gi,
15024
- /resuming\.?/gi,
15025
- /waiting\s+\d+(?:\.\d+)?s/gi
15026
- ];
15027
- function isPureRetryNarration(text2) {
15028
- const trimmed = text2.trim();
15029
- if (!trimmed)
15030
- return false;
15031
- let rest = trimmed;
15032
- for (const fragment of RETRY_NARRATION_FRAGMENTS) {
15033
- rest = rest.replace(fragment, "");
15002
+ var DisallowedModelConfigOptionError = class extends Error {
15003
+ constructor(configId) {
15004
+ super(`model config option "${configId}" is not in the allow-listed catalog`);
15005
+ this.name = "DisallowedModelConfigOptionError";
15034
15006
  }
15035
- return !/[\w]/.test(rest);
15036
- }
15037
- function finalAgentMessageText(updates, agentLabel) {
15038
- const last = agentMessageRuns(updates, agentLabel).at(-1);
15039
- if (!last || isPureRetryNarration(last))
15040
- return "";
15041
- return last;
15042
- }
15043
- function describeEmptyTurn(result, agentLabel) {
15044
- const counts = /* @__PURE__ */ new Map();
15045
- for (const { update } of result.updates) {
15046
- const kind = typeof update.sessionUpdate === "string" ? update.sessionUpdate : "unknown";
15047
- if (kind === "session_info_update" || kind === "available_commands_update")
15048
- continue;
15049
- counts.set(kind, (counts.get(kind) ?? 0) + 1);
15007
+ };
15008
+ var ModelSelectionUnavailableError = class extends Error {
15009
+ label;
15010
+ value;
15011
+ reason;
15012
+ guidance;
15013
+ constructor(input) {
15014
+ const recovery = input.guidance ? ` ${input.guidance}` : " Choose one of the values in the live harness catalog.";
15015
+ super(`${input.label} "${input.value}" is unavailable.${recovery}`);
15016
+ this.name = "ModelSelectionUnavailableError";
15017
+ this.label = input.label;
15018
+ this.value = input.value;
15019
+ this.reason = input.reason;
15020
+ this.guidance = input.guidance;
15050
15021
  }
15051
- const stream = counts.size ? `the stream carried only ${[...counts].map(([kind, count]) => `${kind}\xD7${count}`).join(", ")}` : "the stream carried no content updates";
15052
- const lastRun = agentMessageRuns(result.updates, agentLabel).at(-1);
15053
- const narration = lastRun && isPureRetryNarration(lastRun) ? `; the last message was retry narration "${lastRun.trim().slice(0, 80)}"` : "";
15054
- return `harness ended the turn (${result.stopReason}) with no answer text; ${stream}${narration}`;
15022
+ };
15023
+ function withEffectiveCurrentValues(options, selection) {
15024
+ if (!selection || !selection.model && !selection.effort)
15025
+ return options;
15026
+ return options.map((option) => {
15027
+ const target = modelSelectionTargets(selection).find((entry) => entry.categories.includes(option.category));
15028
+ if (!target?.value || option.currentValue === target.value)
15029
+ return option;
15030
+ return { ...option, currentValue: target.value };
15031
+ });
15055
15032
  }
15056
- function updateText(update) {
15057
- const content = update.content;
15058
- if (typeof content === "string")
15059
- return content;
15060
- return content?.type === "text" ? content.text ?? "" : "";
15033
+ function modelSelectionTargets(selection) {
15034
+ return [
15035
+ { categories: ["model"], label: "model", value: selection.model },
15036
+ {
15037
+ categories: ["thought_level", "effort", "reasoning_effort"],
15038
+ label: "effort",
15039
+ value: selection.effort
15040
+ }
15041
+ ];
15061
15042
  }
15062
- var THOUGHT_UPDATE_TYPES = /* @__PURE__ */ new Set([
15063
- "agent_thought_chunk",
15064
- "agent_thought",
15065
- "reasoning",
15066
- "reasoning_chunk",
15067
- "thinking",
15068
- "thinking_chunk",
15069
- "analysis",
15070
- "analysis_chunk",
15071
- "progress",
15072
- "progress_update"
15073
- ]);
15074
- function agentStreamSnapshot(updates, agentLabel) {
15075
- const completedMessages = [];
15076
- let messageText2 = "";
15077
- let lastWasMessage = false;
15078
- let thoughtText2 = "";
15079
- let thoughtRunOpen = false;
15080
- for (const { update } of updates) {
15081
- const kind = typeof update.sessionUpdate === "string" ? update.sessionUpdate : "";
15082
- if (kind === "agent_message_chunk") {
15083
- const delta = normalizeStreamDelta(agentMessageChunkText(update), agentLabel);
15084
- if (!delta)
15085
- continue;
15086
- if (!lastWasMessage && messageText2 && !/\s$/.test(messageText2) && !CHUNK_CONTINUES_PREVIOUS_WORD.test(delta)) {
15087
- if (!completedMessages.includes(messageText2))
15088
- completedMessages.push(messageText2);
15089
- messageText2 = "";
15090
- }
15091
- messageText2 += delta;
15092
- lastWasMessage = true;
15093
- thoughtRunOpen = false;
15043
+ function assertModelConfigAxisAllowed(configId, advertisedOptions) {
15044
+ const axis = advertisedOptions.find((option) => option.id === configId);
15045
+ if (!axis || !isAllowedAgentModelConfigCategory(axis.category)) {
15046
+ throw new DisallowedModelConfigOptionError(configId);
15047
+ }
15048
+ }
15049
+ async function applyAgentModelSelectionWithUpdatedCatalog(client, sessionId, advertisedOptions, selection) {
15050
+ let currentOptions = advertisedOptions;
15051
+ for (const target of modelSelectionTargets(selection)) {
15052
+ if (!target.value)
15094
15053
  continue;
15054
+ const axis = currentOptions.find((option) => target.categories.includes(option.category));
15055
+ if (!axis)
15056
+ continue;
15057
+ assertModelConfigAxisAllowed(axis.id, currentOptions);
15058
+ if (!axis.options.some((choice) => choice.id === target.value)) {
15059
+ throw new ModelSelectionUnavailableError({
15060
+ label: target.label,
15061
+ value: target.value,
15062
+ reason: "not-advertised"
15063
+ });
15095
15064
  }
15096
- if (lastWasMessage && messageText2) {
15097
- if (!completedMessages.includes(messageText2))
15098
- completedMessages.push(messageText2);
15099
- messageText2 = "";
15100
- }
15101
- lastWasMessage = false;
15102
- if (THOUGHT_UPDATE_TYPES.has(kind)) {
15103
- const delta = normalizeStreamDelta(updateText(update), agentLabel);
15104
- if (!delta)
15105
- continue;
15106
- if (!thoughtRunOpen)
15107
- thoughtText2 = "";
15108
- thoughtText2 += delta;
15109
- thoughtRunOpen = true;
15110
- } else {
15111
- thoughtRunOpen = false;
15065
+ try {
15066
+ if (axis.id === GROK_SESSION_MODEL_AXIS_ID) {
15067
+ if (!client.setModel)
15068
+ throw new Error("ACP client does not support session/set_model");
15069
+ await client.setModel(sessionId, target.value);
15070
+ } else if (axis.id === GROK_LAUNCH_EFFORT_AXIS_ID) {
15071
+ if (axis.currentValue !== target.value) {
15072
+ throw new Error(`Grok started with reasoning effort "${axis.currentValue ?? "unknown"}", not "${target.value}"`);
15073
+ }
15074
+ } else {
15075
+ const updated = await client.setConfigOption(sessionId, axis.id, target.value);
15076
+ const refreshed = filterAllowedModelConfigOptions(parseAdvertisedConfigOptions(updated, selection.model));
15077
+ if (refreshed.length > 0)
15078
+ currentOptions = refreshed;
15079
+ }
15080
+ } catch (error) {
15081
+ throw new ModelSelectionUnavailableError({
15082
+ label: target.label,
15083
+ value: target.value,
15084
+ reason: "provider-refused",
15085
+ guidance: error instanceof Error ? error.message : String(error)
15086
+ });
15112
15087
  }
15113
15088
  }
15114
- return {
15115
- messageText: messageText2,
15116
- ...thoughtText2 || completedMessages.length ? { thoughtText: thoughtText2 || completedMessages.at(-1) } : {}
15117
- };
15089
+ return currentOptions;
15118
15090
  }
15119
- function toolCallEntries(updates) {
15120
- const calls = /* @__PURE__ */ new Map();
15121
- let anonymous = 0;
15122
- let activeAnonymous;
15123
- for (const { update } of updates) {
15124
- const sessionUpdate = update.sessionUpdate;
15125
- if (sessionUpdate !== "tool_call" && sessionUpdate !== "tool_call_update" && sessionUpdate !== "tool_result") {
15126
- continue;
15091
+ async function applyAgentModelSelection(client, sessionId, advertisedOptions, selection) {
15092
+ await applyAgentModelSelectionWithUpdatedCatalog(client, sessionId, advertisedOptions, selection);
15093
+ }
15094
+
15095
+ // apps/body/dist/model-catalog.js
15096
+ import { mkdtemp, rm } from "node:fs/promises";
15097
+ import { tmpdir } from "node:os";
15098
+ import { resolve as resolve5 } from "node:path";
15099
+
15100
+ // apps/body/dist/acp.js
15101
+ import { spawn as spawn5 } from "node:child_process";
15102
+ import { EventEmitter } from "node:events";
15103
+
15104
+ // apps/body/dist/harness-capabilities.js
15105
+ function harnessHonorsSessionSystemPrompt(agentCommand) {
15106
+ return Boolean(agentCommand && /(^|[/\\])claude-(agent|code)-acp(\.[a-z]+)?$/i.test(agentCommand));
15107
+ }
15108
+ var GROK_WARM_SESSION_IDLE_MS = 30 * 6e4;
15109
+ function cornerAutonomyModeCandidates(agentCommand) {
15110
+ if (agentCommand && /(^|[/\\])codex-acp(\.[a-z]+)?$/i.test(agentCommand)) {
15111
+ return ["agent-full-access"];
15112
+ }
15113
+ if (agentCommand && /(^|[/\\])claude-(agent|code)-acp(\.[a-z]+)?$/i.test(agentCommand)) {
15114
+ return ["bypassPermissions"];
15115
+ }
15116
+ if (agentCommand && /(^|[/\\])grok(\.[a-z]+)?$/i.test(agentCommand))
15117
+ return [];
15118
+ if (agentCommand && /(^|[/\\])pi-acp(\.[a-z]+)?$/i.test(agentCommand))
15119
+ return [];
15120
+ return ["agent", "edit", "code"];
15121
+ }
15122
+ function roomModeCandidates(agentCommand, options = {}) {
15123
+ if (options.osSandbox && agentCommand && /(^|[/\\])codex-acp(\.[a-z]+)?$/i.test(agentCommand)) {
15124
+ return ["agent-full-access"];
15125
+ }
15126
+ return ["read-only", "readonly"];
15127
+ }
15128
+
15129
+ // apps/body/dist/harness-tool-scope.js
15130
+ var CLAUDE_TOOL_SCOPE_SETTINGS = { disableClaudeAiConnectors: true };
15131
+ var CLAUDE_PROFILE = {
15132
+ // `settingSources: ['user']` admits the sanitized user MCP copy
15133
+ // while rejecting repository/local/plugin MCP. `strictMcpConfig` cannot be
15134
+ // used because it would reject the user copy too. Account-bound claude.ai
15135
+ // cloud connectors remain off independently.
15136
+ enforcement: "config-isolated",
15137
+ note: "claude-agent-acp forwards _meta.claudeCode.options to the Claude Agent SDK; user-only settingSources loads MCP from the isolated CLAUDE_CONFIG_DIR without repository/local settings, while disableClaudeAiConnectors keeps account-bound connectors off",
15138
+ sessionMeta: {
15139
+ claudeCode: {
15140
+ options: {
15141
+ // Only the sanitized user config in the isolated CLAUDE_CONFIG_DIR;
15142
+ // never a repository's .mcp.json, local settings, or plugins.
15143
+ settingSources: ["user"],
15144
+ // Account-bound claude.ai cloud connectors are not fetched at all.
15145
+ settings: CLAUDE_TOOL_SCOPE_SETTINGS
15146
+ }
15127
15147
  }
15128
- const id = typeof update.toolCallId === "string" ? update.toolCallId : void 0;
15129
- if (id)
15130
- activeAnonymous = void 0;
15131
- if (!id && (sessionUpdate === "tool_call" || !activeAnonymous)) {
15132
- activeAnonymous = `anonymous-${anonymous++}`;
15148
+ },
15149
+ metaSystemPrompt: true
15150
+ };
15151
+ var PROFILES = [
15152
+ { match: /(^|[/\\])claude-(agent|code)-acp(\.[a-z]+)?$/i, profile: CLAUDE_PROFILE },
15153
+ {
15154
+ match: /(^|[/\\])codex-acp(\.[a-z]+)?$/i,
15155
+ profile: {
15156
+ enforcement: "config-isolated",
15157
+ note: "codex-acp merges the session's MCP servers into $CODEX_HOME/config.toml's own [mcp_servers] instead of replacing them, so only an isolated CODEX_HOME scopes the session; the ChatGPT account's codex_apps connectors survive either way"
15158
+ }
15159
+ },
15160
+ {
15161
+ match: /(^|[/\\])pi-acp(\.[a-z]+)?$/i,
15162
+ profile: {
15163
+ enforcement: "config-isolated",
15164
+ note: "pi-acp 0.0.33 still stores session/new mcpServers and never reads them; pi 0.85.1 itself has no MCP client, while optional pi-mcp-adapter reads $PI_CODING_AGENT_DIR/mcp.json and isolated homes exclude the settings.json that would load it"
15165
+ }
15166
+ },
15167
+ {
15168
+ match: /(^|[/\\])grok(\.[a-z]+)?$/i,
15169
+ profile: {
15170
+ enforcement: "config-isolated",
15171
+ note: "grok loads the operator's own ~/.grok MCP servers even when session/new passes an empty mcpServers list (measured live), so only an isolated GROK_HOME scopes the session; no account-bound connector surface was found"
15172
+ }
15173
+ },
15174
+ {
15175
+ match: /(^|[/\\])buzz-agent(\.[a-z]+)?$/i,
15176
+ profile: {
15177
+ enforcement: "allowlisted",
15178
+ note: "buzz-agent mounts only the MCP servers the daemon passes; it has no operator-global tool config"
15179
+ }
15180
+ },
15181
+ {
15182
+ match: CURSOR_HARNESS_COMMAND,
15183
+ profile: {
15184
+ enforcement: "config-isolated",
15185
+ note: "cursor-acp-bridge writes session/new mcpServers into the isolated CURSOR_HOME mcp.json and the isolated $HOME/.cursor/mcp.json cursor-agent loads; it never writes the operator ~/.cursor"
15133
15186
  }
15134
- const key = id ?? activeAnonymous;
15135
- const previous = calls.get(key);
15136
- calls.set(key, {
15137
- ...previous,
15138
- id: key,
15139
- ...typeof update.title === "string" ? { title: update.title } : {},
15140
- ...typeof update.kind === "string" ? { kind: update.kind } : {},
15141
- ...typeof update.status === "string" ? { status: update.status } : {},
15142
- ...sessionUpdate === "tool_result" ? { resultReceived: true } : {},
15143
- ..."rawInput" in update ? { rawInput: update.rawInput } : {},
15144
- ..."content" in update ? { content: update.content } : {},
15145
- ..."rawOutput" in update ? { rawOutput: update.rawOutput } : {},
15146
- ..."locations" in update ? { locations: update.locations } : {}
15147
- });
15148
- if (!id && sessionUpdate === "tool_result")
15149
- activeAnonymous = void 0;
15150
15187
  }
15151
- return [...calls.values()];
15188
+ ];
15189
+ function sessionToolScopeMeta(agentCommand) {
15190
+ const meta = matchProfile(agentCommand)?.sessionMeta;
15191
+ return meta ? JSON.parse(JSON.stringify(meta)) : void 0;
15152
15192
  }
15153
- var AcpClient = class extends EventEmitter {
15154
- child = null;
15155
- buf = "";
15156
- nextId = 1;
15157
- pending = /* @__PURE__ */ new Map();
15158
- activeRunIds = /* @__PURE__ */ new Map();
15159
- activePromptSessions = /* @__PURE__ */ new Set();
15160
- /** Tool metadata arrives in session/update just before a sparse permission request. */
15161
- toolCallMetadata = /* @__PURE__ */ new Map();
15162
- /** Latest `available_commands_update` per session, keyed by sessionId. */
15163
- sessionCommands = /* @__PURE__ */ new Map();
15164
- supportsStandardSteering = false;
15165
- supportsSessionLoading = false;
15166
- supportsImagePrompts = false;
15167
- alive = false;
15168
- /** Bounded tail of recent stderr, so a spawn/exit failure's rejection text
15169
- * carries the real reason (e.g. a harness's own "missing API key" notice)
15170
- * instead of just the bare exit code, in the daemon's log. */
15171
- stderrTail = "";
15172
- agentEnv;
15173
- agentCommand;
15174
- agentLabel;
15175
- agentArgs;
15176
- agentCwd;
15177
- inheritProcessEnv;
15178
- autoApprove;
15179
- osSandbox;
15180
- permissionHandler;
15181
- permissionAllowlist;
15182
- commandsHandler;
15183
- constructor(opts) {
15184
- super();
15185
- const command = opts.agentCommand ?? opts.agentBinary;
15186
- if (!command)
15187
- throw new Error("ACP agent command is required");
15188
- this.agentCommand = command;
15189
- this.agentLabel = opts.agentLabel ?? command;
15190
- this.agentArgs = [...opts.agentArgs ?? []];
15191
- this.agentEnv = opts.agentEnv;
15192
- if (opts.agentCwd)
15193
- this.agentCwd = opts.agentCwd;
15194
- this.inheritProcessEnv = opts.inheritProcessEnv ?? process.env.BUZZY_BODY_AGENT_ENV_INHERIT === "1";
15195
- this.autoApprove = opts.autoApprovePermissions ?? true;
15196
- this.osSandbox = opts.osSandbox ?? false;
15197
- this.permissionHandler = opts.permissionHandler;
15198
- this.permissionAllowlist = opts.permissionAllowlist;
15199
- this.commandsHandler = opts.onCommands;
15193
+ function harnessReadsMetaSystemPrompt(agentCommand) {
15194
+ return matchProfile(agentCommand)?.metaSystemPrompt === true;
15195
+ }
15196
+ function matchProfile(agentCommand) {
15197
+ if (!agentCommand)
15198
+ return void 0;
15199
+ for (const { match, profile } of PROFILES) {
15200
+ if (match.test(agentCommand))
15201
+ return profile;
15200
15202
  }
15201
- async start(timeoutMs = 6e4) {
15202
- if (this.alive)
15203
- return;
15204
- this.child = spawn4(this.agentCommand, this.agentArgs, {
15205
- // agentEnv is the child's whole environment: buildAgentEnv's allowlist is
15206
- // a real boundary, not a decorative one layered over a full inherit.
15207
- env: this.inheritProcessEnv ? { ...process.env, ...this.agentEnv } : this.agentEnv,
15208
- ...this.agentCwd ? { cwd: this.agentCwd } : {},
15209
- // The harness and every tool it spawns share a disposable process
15210
- // group. A hard turn deadline can therefore retire the whole tree,
15211
- // never just the ACP parent while a shell/compiler keeps running.
15212
- detached: process.platform !== "win32",
15213
- stdio: ["pipe", "pipe", "pipe"]
15214
- });
15215
- this.alive = true;
15216
- this.stderrTail = "";
15217
- this.child.stderr.setEncoding("utf8");
15218
- this.child.stderr.on("data", (chunk) => {
15219
- this.stderrTail = (this.stderrTail + chunk).slice(-STDERR_TAIL_MAX_CHARS2);
15220
- this.emit("stderr", chunk);
15221
- });
15222
- this.child.stdout.setEncoding("utf8");
15223
- this.child.stdout.on("data", (chunk) => this.onData(chunk));
15224
- this.child.on("exit", (code, signal) => {
15225
- this.alive = false;
15226
- const meaningful = flattenHarnessCause(this.stderrTail);
15227
- const stderrSuffix = meaningful ? `: ${meaningful}` : "";
15228
- for (const [, p] of this.pending) {
15229
- this.clearTimer(p);
15230
- p.reject(new Error(`ACP agent ${this.agentLabel} exited code=${code} signal=${signal}${stderrSuffix}`));
15231
- }
15232
- this.pending.clear();
15233
- this.activeRunIds.clear();
15234
- this.activePromptSessions.clear();
15235
- this.toolCallMetadata.clear();
15236
- this.sessionCommands.clear();
15237
- this.emit("exit", { code, signal });
15238
- });
15239
- this.child.on("error", (err) => {
15240
- this.alive = false;
15241
- for (const [, p] of this.pending) {
15242
- clearTimeout(p.timer);
15243
- p.reject(err);
15244
- }
15245
- this.pending.clear();
15246
- if (this.listenerCount("error") > 0)
15247
- this.emit("error", err);
15248
- });
15249
- const initResult = await this.request("initialize", {
15250
- protocolVersion: 1,
15251
- clientCapabilities: {}
15252
- }, timeoutMs);
15253
- const initMeta = initResult._meta;
15254
- const steering = initMeta?.steering;
15255
- this.supportsStandardSteering = steering?.supported === true;
15256
- const agentCapabilities = initResult.agentCapabilities;
15257
- this.supportsSessionLoading = agentCapabilities?.loadSession === true;
15258
- this.supportsImagePrompts = agentCapabilities?.promptCapabilities?.image === true;
15259
- this.emit("initialized", initResult);
15260
- if (!isPiAcpHarness(this.agentLabel))
15261
- this.notify("notifications/initialized", {});
15203
+ return void 0;
15204
+ }
15205
+
15206
+ // apps/body/dist/acp.js
15207
+ var STDERR_TAIL_MAX_CHARS2 = 2e3;
15208
+ function braceDelta(line) {
15209
+ let delta = 0;
15210
+ for (const char of line) {
15211
+ if (char === "{")
15212
+ delta += 1;
15213
+ else if (char === "}")
15214
+ delta -= 1;
15262
15215
  }
15263
- async stop() {
15264
- if (!this.alive)
15265
- return;
15266
- const child = this.child;
15267
- if (!child)
15268
- return;
15269
- try {
15270
- if (this.child?.stdin.writable) {
15271
- this.notify("shutdown", {});
15272
- }
15273
- } catch {
15216
+ return delta;
15217
+ }
15218
+ function meaningfulHarnessStderr(tail) {
15219
+ const lines = tail.split(/\r?\n/);
15220
+ const kept = [];
15221
+ for (let index = 0; index < lines.length; index += 1) {
15222
+ const line = lines[index];
15223
+ if (!/error handling notification/i.test(line)) {
15224
+ kept.push(line);
15225
+ continue;
15274
15226
  }
15275
- await new Promise((r2) => setTimeout(r2, 300));
15276
- try {
15277
- killChildProcessGroup(child, "SIGTERM");
15278
- } catch {
15227
+ const block2 = [line];
15228
+ let depth = braceDelta(line);
15229
+ if (depth === 0 && lines[index + 1]?.trimStart().startsWith("{")) {
15230
+ index += 1;
15231
+ block2.push(lines[index]);
15232
+ depth = braceDelta(lines[index]);
15279
15233
  }
15280
- await new Promise((r2) => setTimeout(r2, 500));
15281
- try {
15282
- killChildProcessGroup(child, "SIGKILL");
15283
- } catch {
15234
+ while (depth > 0 && index + 1 < lines.length) {
15235
+ index += 1;
15236
+ block2.push(lines[index]);
15237
+ depth += braceDelta(lines[index]);
15284
15238
  }
15285
- this.child = null;
15286
- this.alive = false;
15287
- this.activeRunIds.clear();
15288
- this.activePromptSessions.clear();
15289
- this.toolCallMetadata.clear();
15290
- }
15291
- get isAlive() {
15292
- return this.alive;
15293
- }
15294
- /** Host pid of the harness wrapper (bwrap when enabled). */
15295
- processPid() {
15296
- return this.child?.pid;
15239
+ if (!/notifications\/initialized/.test(block2.join("\n")))
15240
+ kept.push(...block2);
15297
15241
  }
15298
- /** The latest command list this harness advertised for a session, if any. */
15299
- sessionCommandsFor(sessionId) {
15300
- return this.sessionCommands.get(sessionId) ?? [];
15242
+ return kept.join("\n").trim();
15243
+ }
15244
+ function flattenHarnessCause(tail) {
15245
+ return meaningfulHarnessStderr(tail).replace(/\s+/g, " ").trim();
15246
+ }
15247
+ var AcpRequestTimeoutError = class extends Error {
15248
+ method;
15249
+ timeoutMs;
15250
+ inactivity;
15251
+ code = "ACP_REQUEST_TIMEOUT";
15252
+ constructor(method, timeoutMs, stderrTail = "", inactivity = false, detail = "") {
15253
+ const meaningful = flattenHarnessCause(stderrTail);
15254
+ const suffix = meaningful ? `; harness stderr: ${meaningful}` : "";
15255
+ super(`ACP ${method} timed out after ${timeoutMs}ms${inactivity ? " of inactivity" : ""}${detail ? ` (${detail})` : ""}${suffix}`);
15256
+ this.method = method;
15257
+ this.timeoutMs = timeoutMs;
15258
+ this.inactivity = inactivity;
15259
+ this.name = "AcpRequestTimeoutError";
15301
15260
  }
15302
- /** Whether this live ACP process advertised the stable `session/load` method. */
15303
- canLoadSession() {
15304
- return this.supportsSessionLoading;
15261
+ };
15262
+ function killChildProcessGroup(child, signal) {
15263
+ if (!child.pid)
15264
+ return;
15265
+ try {
15266
+ if (process.platform === "win32")
15267
+ child.kill(signal);
15268
+ else
15269
+ process.kill(-child.pid, signal);
15270
+ } catch (error) {
15271
+ if (error.code !== "ESRCH")
15272
+ throw error;
15305
15273
  }
15306
- /** Whether this live ACP process accepts `image` prompt blocks (`promptCapabilities.image`). */
15307
- canPromptWithImages() {
15308
- return this.supportsImagePrompts;
15274
+ }
15275
+ function promptPayloadNote(prompt) {
15276
+ if (typeof prompt === "string")
15277
+ return "";
15278
+ const images = prompt.filter((block2) => block2.type === "image");
15279
+ if (!images.length)
15280
+ return "";
15281
+ const bytes = images.reduce((total, block2) => total + Math.floor(block2.data.length * 3 / 4), 0);
15282
+ return `prompt carried ${images.length} inline image${images.length === 1 ? "" : "s"}, ${Math.round(bytes / 1024)} KB`;
15283
+ }
15284
+ function parseAvailableCommands(value) {
15285
+ if (!Array.isArray(value))
15286
+ return [];
15287
+ const seen = /* @__PURE__ */ new Set();
15288
+ const result = [];
15289
+ for (const entry of value) {
15290
+ if (!entry || typeof entry !== "object")
15291
+ continue;
15292
+ const record3 = entry;
15293
+ const rawName = typeof record3.name === "string" ? record3.name.trim().replace(/^\/+/, "") : "";
15294
+ if (!rawName || rawName.length > 80 || seen.has(rawName))
15295
+ continue;
15296
+ seen.add(rawName);
15297
+ const description = typeof record3.description === "string" && record3.description.trim() ? record3.description.trim().slice(0, 300) : void 0;
15298
+ const input = record3.input;
15299
+ const hintRaw = input && typeof input.hint === "string" ? input.hint : void 0;
15300
+ const inputHint = hintRaw && hintRaw.trim() ? hintRaw.trim().slice(0, 120) : void 0;
15301
+ result.push({
15302
+ name: rawName,
15303
+ ...description ? { description } : {},
15304
+ ...inputHint ? { inputHint } : {}
15305
+ });
15306
+ if (result.length >= 200)
15307
+ break;
15309
15308
  }
15310
- async sessionNew(opts) {
15311
- const params = {
15312
- cwd: opts.cwd,
15313
- mcpServers: (opts.mcpServers ?? []).map((s) => ({
15314
- name: s.name,
15315
- command: s.command,
15316
- args: s.args ?? [],
15317
- env: s.env ?? []
15318
- }))
15319
- };
15320
- if (opts.systemPrompt)
15321
- params.systemPrompt = opts.systemPrompt;
15322
- const meta = {
15323
- ...sessionToolScopeMeta(this.agentLabel) ?? {}
15324
- };
15325
- if (opts.systemPrompt && harnessReadsMetaSystemPrompt(this.agentLabel)) {
15326
- meta.systemPrompt = { append: opts.systemPrompt };
15309
+ return result;
15310
+ }
15311
+ function agentMessageChunkText(update) {
15312
+ if (update.sessionUpdate !== "agent_message_chunk")
15313
+ return "";
15314
+ const content = update.content;
15315
+ if (content?.type === "text")
15316
+ return content.text ?? "";
15317
+ if (typeof update.content === "string")
15318
+ return update.content;
15319
+ return "";
15320
+ }
15321
+ var CHUNK_CONTINUES_PREVIOUS_WORD = /^[\s'\u2018\u2019\u02bc.,!?;:%)\]}-]/;
15322
+ function continuesPreviousWord(current, delta) {
15323
+ if (/\s$/.test(current))
15324
+ return false;
15325
+ return CHUNK_CONTINUES_PREVIOUS_WORD.test(delta);
15326
+ }
15327
+ var PI_ACP_HARNESS = /(^|[/\\])pi-acp(?:\.[a-z]+)?$/i;
15328
+ function isPiAcpHarness(agentLabel) {
15329
+ return Boolean(agentLabel && PI_ACP_HARNESS.test(agentLabel));
15330
+ }
15331
+ function normalizeStreamDelta(text2, _agentLabel) {
15332
+ return text2;
15333
+ }
15334
+ function agentMessageRuns(updates, agentLabel) {
15335
+ const runs = [];
15336
+ let current = "";
15337
+ let lastWasText = false;
15338
+ for (const u4 of updates) {
15339
+ const delta = normalizeStreamDelta(agentMessageChunkText(u4.update), agentLabel);
15340
+ if (!delta) {
15341
+ lastWasText = false;
15342
+ continue;
15327
15343
  }
15328
- if (Object.keys(meta).length > 0)
15329
- params._meta = meta;
15330
- const raw = await this.request("session/new", params, opts.timeoutMs ?? 6e4);
15331
- const sessionId = raw.sessionId;
15332
- if (!sessionId)
15333
- throw new Error("session/new missing sessionId");
15334
- await this.applySessionMode(sessionId, raw, opts.mode);
15335
- return { sessionId, raw };
15336
- }
15337
- /**
15338
- * Reopen a provider-persisted logical conversation in this new ACP process.
15339
- * Unlike `session/new`, load accepts no system prompt: the harness restores
15340
- * its own conversation and instructions instead of receiving transcript
15341
- * text from Body again.
15342
- */
15343
- async sessionLoad(opts) {
15344
- if (!this.supportsSessionLoading) {
15345
- throw new Error("ACP agent does not advertise session/load");
15344
+ if (!lastWasText && current && !continuesPreviousWord(current, delta)) {
15345
+ runs.push(current);
15346
+ current = "";
15346
15347
  }
15347
- const params = {
15348
- sessionId: opts.sessionId,
15349
- cwd: opts.cwd,
15350
- mcpServers: (opts.mcpServers ?? []).map((server) => ({
15351
- name: server.name,
15352
- command: server.command,
15353
- args: server.args ?? [],
15354
- env: server.env ?? []
15355
- }))
15356
- };
15357
- const meta = sessionToolScopeMeta(this.agentLabel);
15358
- if (meta)
15359
- params._meta = meta;
15360
- const result = await this.request("session/load", params);
15361
- const raw = result && typeof result === "object" ? result : {};
15362
- await this.applySessionMode(opts.sessionId, raw, opts.mode);
15363
- return { sessionId: opts.sessionId, raw };
15348
+ current += delta;
15349
+ lastWasText = true;
15364
15350
  }
15365
- async applySessionMode(sessionId, raw, mode) {
15366
- if (!mode)
15367
- return;
15368
- const modes = raw.modes;
15369
- const candidates = mode === "readonly" ? roomModeCandidates(this.agentLabel, { osSandbox: this.osSandbox }) : cornerAutonomyModeCandidates(this.agentLabel);
15370
- const target = modes?.availableModes?.find((candidate) => candidate.id && candidates.includes(candidate.id))?.id;
15371
- if (!target || modes?.currentModeId === target)
15372
- return;
15373
- await this.request("session/set_mode", { sessionId, modeId: target });
15351
+ if (current)
15352
+ runs.push(current);
15353
+ const seen = /* @__PURE__ */ new Set();
15354
+ return runs.filter((run2) => {
15355
+ if (seen.has(run2))
15356
+ return false;
15357
+ seen.add(run2);
15358
+ return true;
15359
+ });
15360
+ }
15361
+ var RETRY_NARRATION_FRAGMENTS = [
15362
+ // `Retrying (attempt 2/3, waiting 4s)` — the pi/ox-alpha shape.
15363
+ /retrying\s*\((?:attempt|try)\s*\d+\s*\/\s*\d+(?:,\s*(?:waiting|backoff)\s*\d+(?:\.\d+)?s)?\)/gi,
15364
+ // A bare `(attempt 2/3...)` or `[attempt 2/3]` qualifier on its own.
15365
+ /[([](?:attempt|try)\s*\d+\s*\/\s*\d+(?:,\s*(?:waiting|backoff)\s*\d+(?:\.\d+)?s)?[))]\s*/gi,
15366
+ // Standalone narration words with their optional trailing ellipsis.
15367
+ /retrying\s*\.{0,3}/gi,
15368
+ /retried\s*\.{0,3}/gi,
15369
+ /retry\s+finished,?\s*/gi,
15370
+ /retry\s+failed,?\s*/gi,
15371
+ /resuming\.?/gi,
15372
+ /waiting\s+\d+(?:\.\d+)?s/gi
15373
+ ];
15374
+ function isPureRetryNarration(text2) {
15375
+ const trimmed = text2.trim();
15376
+ if (!trimmed)
15377
+ return false;
15378
+ let rest = trimmed;
15379
+ for (const fragment of RETRY_NARRATION_FRAGMENTS) {
15380
+ rest = rest.replace(fragment, "");
15374
15381
  }
15375
- /**
15376
- * Run one prompt turn. `timeoutMs` is an idle window, not a hard cap on
15377
- * turn length: every `session/update` for this session (message chunk,
15378
- * tool call, or otherwise) re-arms it, so an actively-working agent can
15379
- * run indefinitely while a genuinely wedged one (zero activity for
15380
- * `timeoutMs`) still gets cancelled. `onChunk` is the ACP-boundary
15381
- * streaming hook: called for every incremental `agent_message_chunk`
15382
- * delta as it arrives, so a caller can project live text without waiting
15383
- * for the turn to finish. A harness that only emits a final message
15384
- * (never chunks) simply never invokes it — `agentText` is unaffected
15385
- * either way. `onActivity`, when given, fires on every `session/update`
15386
- * regardless of kind (the same trigger that re-arms the idle timer above),
15387
- * so a caller can drive its own shorter "still working" notice off the
15388
- * identical genuine-activity signal without duplicating the timeout logic.
15389
- * It receives the current lane snapshot; existing callbacks that ignore
15390
- * arguments remain valid.
15391
- */
15392
- async sessionPrompt(sessionId, text2, timeoutMs = 12e4, onChunk, onActivity, onToolCalls) {
15393
- const updates = [];
15394
- let promptRunId;
15395
- let requestId;
15396
- const onUpdate = (u4) => {
15397
- if (u4.sessionId !== sessionId)
15398
- return;
15399
- updates.push(u4);
15400
- promptRunId ??= this.activeRunIdFromUpdate(u4.update);
15401
- if (requestId !== void 0)
15402
- this.resetPendingIdleTimeout(requestId);
15403
- onActivity?.(agentStreamSnapshot(updates, this.agentLabel));
15404
- onToolCalls?.(toolCallEntries(updates));
15405
- if (onChunk) {
15406
- const delta = agentMessageChunkText(u4.update);
15407
- if (delta) {
15408
- const runs = agentMessageRuns(updates, this.agentLabel);
15409
- onChunk(delta, runs.join("\n\n"), runs.at(-1), runs);
15410
- }
15411
- }
15412
- };
15413
- this.on("session/update", onUpdate);
15414
- this.activePromptSessions.add(sessionId);
15415
- try {
15416
- const result = await this.request("session/prompt", {
15417
- sessionId,
15418
- prompt: typeof text2 === "string" ? [{ type: "text", text: text2 }] : [...text2]
15419
- }, timeoutMs, (id) => {
15420
- requestId = id;
15421
- }, promptPayloadNote(text2));
15422
- const agentText = finalAgentMessageText(updates, this.agentLabel);
15423
- const toolCalls = toolCallEntries(updates);
15424
- return {
15425
- stopReason: result?.stopReason ?? "end_turn",
15426
- updates,
15427
- agentText,
15428
- toolCalls
15429
- };
15430
- } finally {
15431
- this.activePromptSessions.delete(sessionId);
15432
- if (promptRunId && this.activeRunIds.get(sessionId) === promptRunId) {
15433
- this.activeRunIds.delete(sessionId);
15434
- }
15435
- this.off("session/update", onUpdate);
15436
- }
15382
+ return !/[\w]/.test(rest);
15383
+ }
15384
+ function finalAgentMessageText(updates, agentLabel) {
15385
+ const last = agentMessageRuns(updates, agentLabel).at(-1);
15386
+ if (!last || isPureRetryNarration(last))
15387
+ return "";
15388
+ return last;
15389
+ }
15390
+ function describeEmptyTurn(result, agentLabel) {
15391
+ const counts = /* @__PURE__ */ new Map();
15392
+ for (const { update } of result.updates) {
15393
+ const kind = typeof update.sessionUpdate === "string" ? update.sessionUpdate : "unknown";
15394
+ if (kind === "session_info_update" || kind === "available_commands_update")
15395
+ continue;
15396
+ counts.set(kind, (counts.get(kind) ?? 0) + 1);
15437
15397
  }
15438
- /**
15439
- * Inject follow-up input into the prompt currently running for this session.
15440
- * buzz-agent advertises the target run through ACP session metadata; binding
15441
- * the request to that id prevents a late message from steering a newer turn.
15442
- */
15443
- async sessionSteer(sessionId, text2, timeoutMs = 6e4) {
15444
- if (this.supportsStandardSteering) {
15445
- const raw2 = await this.request("_session/steering", {
15446
- sessionId,
15447
- prompt: [{ type: "text", text: text2 }]
15448
- }, timeoutMs);
15449
- if (!raw2.outcome || raw2.outcome === "failed") {
15450
- throw new Error("ACP session steering failed");
15398
+ const stream = counts.size ? `the stream carried only ${[...counts].map(([kind, count]) => `${kind}\xD7${count}`).join(", ")}` : "the stream carried no content updates";
15399
+ const lastRun = agentMessageRuns(result.updates, agentLabel).at(-1);
15400
+ const narration = lastRun && isPureRetryNarration(lastRun) ? `; the last message was retry narration "${lastRun.trim().slice(0, 80)}"` : "";
15401
+ return `harness ended the turn (${result.stopReason}) with no answer text; ${stream}${narration}`;
15402
+ }
15403
+ function updateText(update) {
15404
+ const content = update.content;
15405
+ if (typeof content === "string")
15406
+ return content;
15407
+ return content?.type === "text" ? content.text ?? "" : "";
15408
+ }
15409
+ var THOUGHT_UPDATE_TYPES = /* @__PURE__ */ new Set([
15410
+ "agent_thought_chunk",
15411
+ "agent_thought",
15412
+ "reasoning",
15413
+ "reasoning_chunk",
15414
+ "thinking",
15415
+ "thinking_chunk",
15416
+ "analysis",
15417
+ "analysis_chunk",
15418
+ "progress",
15419
+ "progress_update"
15420
+ ]);
15421
+ function agentStreamSnapshot(updates, agentLabel) {
15422
+ const completedMessages = [];
15423
+ let messageText2 = "";
15424
+ let lastWasMessage = false;
15425
+ let thoughtText2 = "";
15426
+ let thoughtRunOpen = false;
15427
+ for (const { update } of updates) {
15428
+ const kind = typeof update.sessionUpdate === "string" ? update.sessionUpdate : "";
15429
+ if (kind === "agent_message_chunk") {
15430
+ const delta = normalizeStreamDelta(agentMessageChunkText(update), agentLabel);
15431
+ if (!delta)
15432
+ continue;
15433
+ if (!lastWasMessage && messageText2 && !/\s$/.test(messageText2) && !CHUNK_CONTINUES_PREVIOUS_WORD.test(delta)) {
15434
+ if (!completedMessages.includes(messageText2))
15435
+ completedMessages.push(messageText2);
15436
+ messageText2 = "";
15451
15437
  }
15452
- return {
15453
- runId: this.activeRunId(sessionId) ?? `session:${sessionId}`,
15454
- messageId: raw2.outcome
15455
- };
15456
- }
15457
- const expectedRunId = await this.waitForActiveRun(sessionId, Math.min(timeoutMs, 5e3));
15458
- const raw = await this.request("_goose/unstable/session/steer", {
15459
- sessionId,
15460
- prompt: [{ type: "text", text: text2 }],
15461
- expectedRunId
15462
- }, timeoutMs);
15463
- if (!raw.runId || !raw.messageId) {
15464
- throw new Error("ACP session steer response missing runId or messageId");
15438
+ messageText2 += delta;
15439
+ lastWasMessage = true;
15440
+ thoughtRunOpen = false;
15441
+ continue;
15465
15442
  }
15466
- return { runId: raw.runId, messageId: raw.messageId };
15467
- }
15468
- /** Active ACP run for a session, if the agent has advertised one. */
15469
- activeRunId(sessionId) {
15470
- return this.activeRunIds.get(sessionId) ?? (this.activePromptSessions.has(sessionId) ? `session:${sessionId}` : void 0);
15471
- }
15472
- sessionCancel(sessionId) {
15473
- this.notify("session/cancel", { sessionId });
15474
- }
15475
- /**
15476
- * The portable model/effort setter, confirmed round-tripping on pi/codex/
15477
- * claude (report `data/buzzy-multiagent-runtimes/report.md` §3.2). The
15478
- * param is `configId`, not `optionId`. Harnesses that advertise this shape
15479
- * must use it; `session/set_model` is reserved for the distinct standard
15480
- * `session/new.models` shape. Returns the runtime's updated `configOptions`.
15481
- * Callers must validate `configId`/`value` against the
15482
- * allow-listed catalog before calling this — see
15483
- * `apps/body/src/model-config.ts`'s `assertModelConfigOptionAllowed`; this
15484
- * method performs no policy checks of its own.
15485
- */
15486
- async setConfigOption(sessionId, configId, value) {
15487
- return this.request("session/set_config_option", { sessionId, configId, value });
15488
- }
15489
- /**
15490
- * Standard ACP model setter used only when `session/new.models` supplied the
15491
- * corresponding safe model axis. Most Beeline harnesses use configOptions;
15492
- * Grok currently exposes models through this standard method instead.
15493
- */
15494
- async setModel(sessionId, modelId) {
15495
- return this.request("session/set_model", { sessionId, modelId });
15496
- }
15497
- clearTimer(p) {
15498
- if (p.timer) {
15499
- clearTimeout(p.timer);
15500
- p.timer = void 0;
15443
+ if (lastWasMessage && messageText2) {
15444
+ if (!completedMessages.includes(messageText2))
15445
+ completedMessages.push(messageText2);
15446
+ messageText2 = "";
15501
15447
  }
15502
- }
15503
- /**
15504
- * Re-arm a pending request's idle timer from its full `idleTimeoutMs`,
15505
- * called on every ACP activity signal for that request (streamed text
15506
- * delta, session/update, tool call). A request only times out after this
15507
- * much wall-clock time with zero activity, not after a fixed deadline —
15508
- * so an actively-working turn can run indefinitely while a genuinely
15509
- * wedged one still gets caught.
15510
- */
15511
- resetPendingIdleTimeout(id) {
15512
- const p = this.pending.get(id);
15513
- if (!p || p.idleTimeoutMs === void 0)
15514
- return;
15515
- this.clearTimer(p);
15516
- const { idleTimeoutMs, method, reject, detail } = p;
15517
- p.timer = setTimeout(() => {
15518
- this.pending.delete(id);
15519
- reject(new AcpRequestTimeoutError(method ?? "request", idleTimeoutMs, this.stderrTail, true, detail ?? ""));
15520
- }, idleTimeoutMs);
15521
- }
15522
- onData(chunk) {
15523
- this.buf += chunk;
15524
- let nl;
15525
- while ((nl = this.buf.indexOf("\n")) >= 0) {
15526
- const line = this.buf.slice(0, nl).trim();
15527
- this.buf = this.buf.slice(nl + 1);
15528
- if (!line)
15529
- continue;
15530
- let msg;
15531
- try {
15532
- msg = JSON.parse(line);
15533
- } catch {
15448
+ lastWasMessage = false;
15449
+ if (THOUGHT_UPDATE_TYPES.has(kind)) {
15450
+ const delta = normalizeStreamDelta(updateText(update), agentLabel);
15451
+ if (!delta)
15534
15452
  continue;
15535
- }
15536
- this.handleMessage(msg);
15453
+ if (!thoughtRunOpen)
15454
+ thoughtText2 = "";
15455
+ thoughtText2 += delta;
15456
+ thoughtRunOpen = true;
15457
+ } else {
15458
+ thoughtRunOpen = false;
15537
15459
  }
15538
15460
  }
15539
- handleMessage(msg) {
15540
- const id = msg.id;
15541
- const method = msg.method;
15542
- if (id !== void 0 && !method) {
15543
- const n3 = Number(id);
15544
- const p = this.pending.get(n3);
15545
- if (!p)
15546
- return;
15547
- this.pending.delete(n3);
15548
- this.clearTimer(p);
15549
- if (msg.error) {
15550
- const err = msg.error;
15551
- const details = typeof err.data?.details === "string" && err.data.details.trim() ? err.data.details.replace(/\s+/g, " ").trim() : "";
15552
- const meaningful = flattenHarnessCause(this.stderrTail);
15553
- const extra = [details, meaningful ? `harness stderr: ${meaningful}` : ""].filter(Boolean).join("; ");
15554
- p.reject(new Error(`ACP error ${err.code}: ${err.message}${extra ? `; ${extra}` : ""}`));
15555
- } else {
15556
- p.resolve(msg.result);
15557
- }
15558
- return;
15559
- }
15560
- if (method && id !== void 0) {
15561
- if (method === "session/request_permission") {
15562
- void this.handlePermission(id, msg.params);
15563
- return;
15564
- }
15565
- this.write({ jsonrpc: "2.0", id, result: {} });
15566
- return;
15461
+ return {
15462
+ messageText: messageText2,
15463
+ ...thoughtText2 || completedMessages.length ? { thoughtText: thoughtText2 || completedMessages.at(-1) } : {}
15464
+ };
15465
+ }
15466
+ function toolCallEntries(updates) {
15467
+ const calls = /* @__PURE__ */ new Map();
15468
+ let anonymous = 0;
15469
+ let activeAnonymous;
15470
+ for (const { update } of updates) {
15471
+ const sessionUpdate = update.sessionUpdate;
15472
+ if (sessionUpdate !== "tool_call" && sessionUpdate !== "tool_call_update" && sessionUpdate !== "tool_result") {
15473
+ continue;
15567
15474
  }
15568
- if (method === "session/update") {
15569
- const params = msg.params;
15570
- if (params?.sessionId && params.update) {
15571
- if (params.update.sessionUpdate === "available_commands_update") {
15572
- const commands = parseAvailableCommands(params.update.availableCommands);
15573
- this.sessionCommands.set(params.sessionId, commands);
15574
- this.commandsHandler?.(commands);
15575
- this.emit("commands", { sessionId: params.sessionId, commands });
15576
- }
15577
- const toolCallId = params.update.toolCallId;
15578
- const sessionUpdate = params.update.sessionUpdate;
15579
- if (typeof toolCallId === "string" && (sessionUpdate === "tool_call" || sessionUpdate === "tool_call_update")) {
15580
- const metadataKey = `${params.sessionId}\0${toolCallId}`;
15581
- const status = params.update.status;
15582
- if (status === "completed" || status === "failed") {
15583
- this.toolCallMetadata.delete(metadataKey);
15584
- } else {
15585
- const existing = this.toolCallMetadata.get(metadataKey);
15586
- this.toolCallMetadata.set(metadataKey, {
15587
- ...existing,
15588
- toolCallId,
15589
- ...typeof params.update.title === "string" ? { title: params.update.title } : {},
15590
- ...typeof params.update.kind === "string" ? { kind: params.update.kind } : {},
15591
- ..."rawInput" in params.update ? { rawInput: params.update.rawInput } : {},
15592
- // `content`/`locations` carry the touched file paths for adapters
15593
- // whose permission request omits them (codex-acp's file-change
15594
- // request has no rawInput at all) — the corner worktree guard in
15595
- // `session-sandbox.ts` reads them.
15596
- ..."content" in params.update ? { content: params.update.content } : {},
15597
- ..."locations" in params.update ? { locations: params.update.locations } : {}
15598
- });
15599
- }
15600
- }
15601
- const u4 = {
15602
- sessionId: params.sessionId,
15603
- update: params.update
15604
- };
15605
- const activeRunId = this.activeRunIdFromUpdate(params.update);
15606
- if (activeRunId)
15607
- this.activeRunIds.set(params.sessionId, activeRunId);
15608
- this.emit("session/update", u4);
15609
- }
15610
- return;
15475
+ const id = typeof update.toolCallId === "string" ? update.toolCallId : void 0;
15476
+ if (id)
15477
+ activeAnonymous = void 0;
15478
+ if (!id && (sessionUpdate === "tool_call" || !activeAnonymous)) {
15479
+ activeAnonymous = `anonymous-${anonymous++}`;
15611
15480
  }
15481
+ const key = id ?? activeAnonymous;
15482
+ const previous = calls.get(key);
15483
+ calls.set(key, {
15484
+ ...previous,
15485
+ id: key,
15486
+ ...typeof update.title === "string" ? { title: update.title } : {},
15487
+ ...typeof update.kind === "string" ? { kind: update.kind } : {},
15488
+ ...typeof update.status === "string" ? { status: update.status } : {},
15489
+ ...sessionUpdate === "tool_result" ? { resultReceived: true } : {},
15490
+ ..."rawInput" in update ? { rawInput: update.rawInput } : {},
15491
+ ..."content" in update ? { content: update.content } : {},
15492
+ ..."rawOutput" in update ? { rawOutput: update.rawOutput } : {},
15493
+ ..."locations" in update ? { locations: update.locations } : {}
15494
+ });
15495
+ if (!id && sessionUpdate === "tool_result")
15496
+ activeAnonymous = void 0;
15612
15497
  }
15613
- activeRunIdFromUpdate(update) {
15614
- const meta = update._meta;
15615
- const goose = meta?.goose;
15616
- return typeof goose?.activeRunId === "string" && goose.activeRunId ? goose.activeRunId : void 0;
15498
+ return [...calls.values()];
15499
+ }
15500
+ var AcpClient = class extends EventEmitter {
15501
+ child = null;
15502
+ buf = "";
15503
+ nextId = 1;
15504
+ pending = /* @__PURE__ */ new Map();
15505
+ activeRunIds = /* @__PURE__ */ new Map();
15506
+ activePromptSessions = /* @__PURE__ */ new Set();
15507
+ /** Tool metadata arrives in session/update just before a sparse permission request. */
15508
+ toolCallMetadata = /* @__PURE__ */ new Map();
15509
+ /** Latest `available_commands_update` per session, keyed by sessionId. */
15510
+ sessionCommands = /* @__PURE__ */ new Map();
15511
+ supportsStandardSteering = false;
15512
+ supportsSessionLoading = false;
15513
+ supportsImagePrompts = false;
15514
+ alive = false;
15515
+ /** Bounded tail of recent stderr, so a spawn/exit failure's rejection text
15516
+ * carries the real reason (e.g. a harness's own "missing API key" notice)
15517
+ * instead of just the bare exit code, in the daemon's log. */
15518
+ stderrTail = "";
15519
+ agentEnv;
15520
+ agentCommand;
15521
+ agentLabel;
15522
+ agentArgs;
15523
+ agentCwd;
15524
+ inheritProcessEnv;
15525
+ autoApprove;
15526
+ osSandbox;
15527
+ permissionHandler;
15528
+ permissionAllowlist;
15529
+ commandsHandler;
15530
+ constructor(opts) {
15531
+ super();
15532
+ const command = opts.agentCommand ?? opts.agentBinary;
15533
+ if (!command)
15534
+ throw new Error("ACP agent command is required");
15535
+ this.agentCommand = command;
15536
+ this.agentLabel = opts.agentLabel ?? command;
15537
+ this.agentArgs = [...opts.agentArgs ?? []];
15538
+ this.agentEnv = opts.agentEnv;
15539
+ if (opts.agentCwd)
15540
+ this.agentCwd = opts.agentCwd;
15541
+ this.inheritProcessEnv = opts.inheritProcessEnv ?? process.env.BUZZY_BODY_AGENT_ENV_INHERIT === "1";
15542
+ this.autoApprove = opts.autoApprovePermissions ?? true;
15543
+ this.osSandbox = opts.osSandbox ?? false;
15544
+ this.permissionHandler = opts.permissionHandler;
15545
+ this.permissionAllowlist = opts.permissionAllowlist;
15546
+ this.commandsHandler = opts.onCommands;
15617
15547
  }
15618
- waitForActiveRun(sessionId, timeoutMs) {
15619
- const current = this.activeRunIds.get(sessionId);
15620
- if (current)
15621
- return Promise.resolve(current);
15622
- return new Promise((resolve36, reject) => {
15623
- const onUpdate = (update) => {
15624
- if (update.sessionId !== sessionId)
15625
- return;
15626
- const runId = this.activeRunIdFromUpdate(update.update);
15627
- if (!runId)
15628
- return;
15629
- cleanup();
15630
- resolve36(runId);
15631
- };
15632
- const timer = setTimeout(() => {
15633
- cleanup();
15634
- reject(new Error(`ACP session ${sessionId} has no active run to steer`));
15635
- }, timeoutMs);
15636
- const cleanup = () => {
15637
- clearTimeout(timer);
15638
- this.off("session/update", onUpdate);
15639
- };
15640
- this.on("session/update", onUpdate);
15548
+ async start(timeoutMs = 6e4) {
15549
+ if (this.alive)
15550
+ return;
15551
+ this.child = spawn5(this.agentCommand, this.agentArgs, {
15552
+ // agentEnv is the child's whole environment: buildAgentEnv's allowlist is
15553
+ // a real boundary, not a decorative one layered over a full inherit.
15554
+ env: this.inheritProcessEnv ? { ...process.env, ...this.agentEnv } : this.agentEnv,
15555
+ ...this.agentCwd ? { cwd: this.agentCwd } : {},
15556
+ // The harness and every tool it spawns share a disposable process
15557
+ // group. A hard turn deadline can therefore retire the whole tree,
15558
+ // never just the ACP parent while a shell/compiler keeps running.
15559
+ detached: process.platform !== "win32",
15560
+ stdio: ["pipe", "pipe", "pipe"]
15641
15561
  });
15642
- }
15643
- async handlePermission(id, params) {
15644
- const p = params ?? {};
15645
- const toolCallId = p.toolCall?.toolCallId;
15646
- const metadataKey = p.sessionId && toolCallId ? `${p.sessionId}\0${toolCallId}` : void 0;
15647
- const tracked = metadataKey ? this.toolCallMetadata.get(metadataKey) : void 0;
15648
- if (tracked)
15649
- p.toolCall = { ...tracked, ...p.toolCall };
15650
- let decision2 = this.autoApprove ? "allow" : "reject";
15651
- if (!this.autoApprove && this.permissionAllowlist) {
15652
- try {
15653
- decision2 = this.permissionAllowlist(p) ? "allow" : "reject";
15654
- } catch (error) {
15655
- this.emit("permission/error", error);
15656
- decision2 = "reject";
15562
+ this.alive = true;
15563
+ this.stderrTail = "";
15564
+ this.child.stderr.setEncoding("utf8");
15565
+ this.child.stderr.on("data", (chunk) => {
15566
+ this.stderrTail = (this.stderrTail + chunk).slice(-STDERR_TAIL_MAX_CHARS2);
15567
+ this.emit("stderr", chunk);
15568
+ });
15569
+ this.child.stdout.setEncoding("utf8");
15570
+ this.child.stdout.on("data", (chunk) => this.onData(chunk));
15571
+ this.child.on("exit", (code, signal) => {
15572
+ this.alive = false;
15573
+ const meaningful = flattenHarnessCause(this.stderrTail);
15574
+ const stderrSuffix = meaningful ? `: ${meaningful}` : "";
15575
+ for (const [, p] of this.pending) {
15576
+ this.clearTimer(p);
15577
+ p.reject(new Error(`ACP agent ${this.agentLabel} exited code=${code} signal=${signal}${stderrSuffix}`));
15657
15578
  }
15658
- } else if (this.permissionHandler) {
15659
- try {
15660
- decision2 = await this.permissionHandler(p);
15661
- } catch (error) {
15662
- this.emit("permission/error", error);
15663
- decision2 = "reject";
15579
+ this.pending.clear();
15580
+ this.activeRunIds.clear();
15581
+ this.activePromptSessions.clear();
15582
+ this.toolCallMetadata.clear();
15583
+ this.sessionCommands.clear();
15584
+ this.emit("exit", { code, signal });
15585
+ });
15586
+ this.child.on("error", (err) => {
15587
+ this.alive = false;
15588
+ for (const [, p] of this.pending) {
15589
+ clearTimeout(p.timer);
15590
+ p.reject(err);
15591
+ }
15592
+ this.pending.clear();
15593
+ if (this.listenerCount("error") > 0)
15594
+ this.emit("error", err);
15595
+ });
15596
+ const initResult = await this.request("initialize", {
15597
+ protocolVersion: 1,
15598
+ clientCapabilities: {}
15599
+ }, timeoutMs);
15600
+ const initMeta = initResult._meta;
15601
+ const steering = initMeta?.steering;
15602
+ this.supportsStandardSteering = steering?.supported === true;
15603
+ const agentCapabilities = initResult.agentCapabilities;
15604
+ this.supportsSessionLoading = agentCapabilities?.loadSession === true;
15605
+ this.supportsImagePrompts = agentCapabilities?.promptCapabilities?.image === true;
15606
+ this.emit("initialized", initResult);
15607
+ if (!isPiAcpHarness(this.agentLabel))
15608
+ this.notify("notifications/initialized", {});
15609
+ }
15610
+ async stop() {
15611
+ if (!this.alive)
15612
+ return;
15613
+ const child = this.child;
15614
+ if (!child)
15615
+ return;
15616
+ try {
15617
+ if (this.child?.stdin.writable) {
15618
+ this.notify("shutdown", {});
15664
15619
  }
15620
+ } catch {
15665
15621
  }
15666
- if (decision2 === "allow") {
15667
- const allow = p?.options?.find((o) => o.kind === "allow_once" || o.kind === "allow_always") ?? p?.options?.[0];
15668
- if (allow?.optionId) {
15669
- if (metadataKey)
15670
- this.toolCallMetadata.delete(metadataKey);
15671
- this.write({
15672
- jsonrpc: "2.0",
15673
- id,
15674
- result: {
15675
- outcome: { outcome: "selected", optionId: allow.optionId }
15676
- }
15677
- });
15678
- return;
15679
- }
15622
+ await new Promise((r2) => setTimeout(r2, 300));
15623
+ try {
15624
+ killChildProcessGroup(child, "SIGTERM");
15625
+ } catch {
15680
15626
  }
15681
- const reject = p?.options?.find((o) => o.kind === "reject_once" || o.kind === "reject_always");
15682
- if (reject?.optionId) {
15683
- if (metadataKey)
15684
- this.toolCallMetadata.delete(metadataKey);
15685
- this.write({
15686
- jsonrpc: "2.0",
15687
- id,
15688
- result: {
15689
- outcome: { outcome: "selected", optionId: reject.optionId }
15690
- }
15691
- });
15692
- return;
15627
+ await new Promise((r2) => setTimeout(r2, 500));
15628
+ try {
15629
+ killChildProcessGroup(child, "SIGKILL");
15630
+ } catch {
15693
15631
  }
15694
- if (metadataKey)
15695
- this.toolCallMetadata.delete(metadataKey);
15696
- this.write({
15697
- jsonrpc: "2.0",
15698
- id,
15699
- result: { outcome: { outcome: "cancelled" } }
15700
- });
15632
+ this.child = null;
15633
+ this.alive = false;
15634
+ this.activeRunIds.clear();
15635
+ this.activePromptSessions.clear();
15636
+ this.toolCallMetadata.clear();
15637
+ }
15638
+ get isAlive() {
15639
+ return this.alive;
15640
+ }
15641
+ /** Host pid of the harness wrapper (bwrap when enabled). */
15642
+ processPid() {
15643
+ return this.child?.pid;
15644
+ }
15645
+ /** The latest command list this harness advertised for a session, if any. */
15646
+ sessionCommandsFor(sessionId) {
15647
+ return this.sessionCommands.get(sessionId) ?? [];
15648
+ }
15649
+ /** Whether this live ACP process advertised the stable `session/load` method. */
15650
+ canLoadSession() {
15651
+ return this.supportsSessionLoading;
15652
+ }
15653
+ /** Whether this live ACP process accepts `image` prompt blocks (`promptCapabilities.image`). */
15654
+ canPromptWithImages() {
15655
+ return this.supportsImagePrompts;
15656
+ }
15657
+ async sessionNew(opts) {
15658
+ const params = {
15659
+ cwd: opts.cwd,
15660
+ mcpServers: (opts.mcpServers ?? []).map((s) => ({
15661
+ name: s.name,
15662
+ command: s.command,
15663
+ args: s.args ?? [],
15664
+ env: s.env ?? []
15665
+ }))
15666
+ };
15667
+ if (opts.systemPrompt)
15668
+ params.systemPrompt = opts.systemPrompt;
15669
+ const meta = {
15670
+ ...sessionToolScopeMeta(this.agentLabel) ?? {}
15671
+ };
15672
+ if (opts.systemPrompt && harnessReadsMetaSystemPrompt(this.agentLabel)) {
15673
+ meta.systemPrompt = { append: opts.systemPrompt };
15674
+ }
15675
+ if (Object.keys(meta).length > 0)
15676
+ params._meta = meta;
15677
+ const raw = await this.request("session/new", params, opts.timeoutMs ?? 6e4);
15678
+ const sessionId = raw.sessionId;
15679
+ if (!sessionId)
15680
+ throw new Error("session/new missing sessionId");
15681
+ await this.applySessionMode(sessionId, raw, opts.mode);
15682
+ return { sessionId, raw };
15701
15683
  }
15702
15684
  /**
15703
- * `onStart`, when given, receives the assigned request id synchronously
15704
- * and marks the request as idle-resettable at `timeoutMs`: callers can
15705
- * then call `resetPendingIdleTimeout(id)` on activity to defer the
15706
- * timeout instead of it firing at a fixed deadline.
15685
+ * Reopen a provider-persisted logical conversation in this new ACP process.
15686
+ * Unlike `session/new`, load accepts no system prompt: the harness restores
15687
+ * its own conversation and instructions instead of receiving transcript
15688
+ * text from Body again.
15707
15689
  */
15708
- request(method, params, timeoutMs = 6e4, onStart, detail = "") {
15709
- if (!this.child || !this.alive) {
15710
- return Promise.reject(new Error("AcpClient not started"));
15690
+ async sessionLoad(opts) {
15691
+ if (!this.supportsSessionLoading) {
15692
+ throw new Error("ACP agent does not advertise session/load");
15711
15693
  }
15712
- const id = this.nextId++;
15713
- const payload = { jsonrpc: "2.0", id, method, params };
15714
- return new Promise((resolve36, reject) => {
15715
- const timer = setTimeout(() => {
15716
- this.pending.delete(id);
15717
- reject(new AcpRequestTimeoutError(method, timeoutMs, this.stderrTail, Boolean(onStart), detail));
15718
- }, timeoutMs);
15719
- this.pending.set(id, {
15720
- resolve: resolve36,
15721
- reject,
15722
- timer,
15723
- method,
15724
- ...detail ? { detail } : {},
15725
- ...onStart ? { idleTimeoutMs: timeoutMs } : {}
15726
- });
15727
- onStart?.(id);
15728
- this.write(payload);
15729
- });
15730
- }
15731
- notify(method, params) {
15732
- this.write({ jsonrpc: "2.0", method, params });
15694
+ const params = {
15695
+ sessionId: opts.sessionId,
15696
+ cwd: opts.cwd,
15697
+ mcpServers: (opts.mcpServers ?? []).map((server) => ({
15698
+ name: server.name,
15699
+ command: server.command,
15700
+ args: server.args ?? [],
15701
+ env: server.env ?? []
15702
+ }))
15703
+ };
15704
+ const meta = sessionToolScopeMeta(this.agentLabel);
15705
+ if (meta)
15706
+ params._meta = meta;
15707
+ const result = await this.request("session/load", params);
15708
+ const raw = result && typeof result === "object" ? result : {};
15709
+ await this.applySessionMode(opts.sessionId, raw, opts.mode);
15710
+ return { sessionId: opts.sessionId, raw };
15733
15711
  }
15734
- write(obj) {
15735
- if (!this.child?.stdin.writable)
15712
+ async applySessionMode(sessionId, raw, mode) {
15713
+ if (!mode)
15736
15714
  return;
15737
- this.child.stdin.write(JSON.stringify(obj) + "\n");
15738
- }
15739
- };
15740
-
15741
- // apps/body/dist/model-types.js
15742
- var ALLOWED = /* @__PURE__ */ new Set(["model", "thought_level", "effort", "reasoning_effort"]);
15743
- function isAllowedAgentModelConfigCategory(category) {
15744
- return ALLOWED.has(category);
15745
- }
15746
-
15747
- // apps/body/dist/model-config.js
15748
- import { lstatSync as lstatSync2, readFileSync as readFileSync2 } from "node:fs";
15749
- import { basename as basename3, resolve as resolve4 } from "node:path";
15750
- var GROK_SESSION_MODEL_AXIS_ID = "beeline:grok-session-model";
15751
- var GROK_LAUNCH_EFFORT_AXIS_ID = "beeline:grok-launch-reasoning-effort";
15752
- function isGrokAgentCommand(agent) {
15753
- if (agent.kind === "grok")
15754
- return true;
15755
- if (agent.kind && agent.kind !== "custom")
15756
- return false;
15757
- return /^grok(?:\.[a-z0-9]+)?$/i.test(basename3(agent.command));
15758
- }
15759
- function agentArgsWithModelSelection(agent, selection) {
15760
- const original = [...agent.args];
15761
- if (!selection || !isGrokAgentCommand(agent) || !original.includes("agent") || !original.includes("stdio")) {
15762
- return original;
15715
+ const modes = raw.modes;
15716
+ const candidates = mode === "readonly" ? roomModeCandidates(this.agentLabel, { osSandbox: this.osSandbox }) : cornerAutonomyModeCandidates(this.agentLabel);
15717
+ const target = modes?.availableModes?.find((candidate) => candidate.id && candidates.includes(candidate.id))?.id;
15718
+ if (!target || modes?.currentModeId === target)
15719
+ return;
15720
+ await this.request("session/set_mode", { sessionId, modeId: target });
15763
15721
  }
15764
- const stripped = [];
15765
- for (let index = 0; index < original.length; index += 1) {
15766
- const arg = original[index];
15767
- if (arg === "-m" || arg === "--model" || arg === "--reasoning-effort") {
15768
- index += 1;
15769
- continue;
15770
- }
15771
- if (arg.startsWith("--model=") || arg.startsWith("--reasoning-effort=") || /^-m=/.test(arg)) {
15772
- continue;
15722
+ /**
15723
+ * Run one prompt turn. `timeoutMs` is an idle window, not a hard cap on
15724
+ * turn length: every `session/update` for this session (message chunk,
15725
+ * tool call, or otherwise) re-arms it, so an actively-working agent can
15726
+ * run indefinitely while a genuinely wedged one (zero activity for
15727
+ * `timeoutMs`) still gets cancelled. `onChunk` is the ACP-boundary
15728
+ * streaming hook: called for every incremental `agent_message_chunk`
15729
+ * delta as it arrives, so a caller can project live text without waiting
15730
+ * for the turn to finish. A harness that only emits a final message
15731
+ * (never chunks) simply never invokes it — `agentText` is unaffected
15732
+ * either way. `onActivity`, when given, fires on every `session/update`
15733
+ * regardless of kind (the same trigger that re-arms the idle timer above),
15734
+ * so a caller can drive its own shorter "still working" notice off the
15735
+ * identical genuine-activity signal without duplicating the timeout logic.
15736
+ * It receives the current lane snapshot; existing callbacks that ignore
15737
+ * arguments remain valid.
15738
+ */
15739
+ async sessionPrompt(sessionId, text2, timeoutMs = 12e4, onChunk, onActivity, onToolCalls) {
15740
+ const updates = [];
15741
+ let promptRunId;
15742
+ let requestId;
15743
+ const onUpdate = (u4) => {
15744
+ if (u4.sessionId !== sessionId)
15745
+ return;
15746
+ updates.push(u4);
15747
+ promptRunId ??= this.activeRunIdFromUpdate(u4.update);
15748
+ if (requestId !== void 0)
15749
+ this.resetPendingIdleTimeout(requestId);
15750
+ onActivity?.(agentStreamSnapshot(updates, this.agentLabel));
15751
+ onToolCalls?.(toolCallEntries(updates));
15752
+ if (onChunk) {
15753
+ const delta = agentMessageChunkText(u4.update);
15754
+ if (delta) {
15755
+ const runs = agentMessageRuns(updates, this.agentLabel);
15756
+ onChunk(delta, runs.join("\n\n"), runs.at(-1), runs);
15757
+ }
15758
+ }
15759
+ };
15760
+ this.on("session/update", onUpdate);
15761
+ this.activePromptSessions.add(sessionId);
15762
+ try {
15763
+ const result = await this.request("session/prompt", {
15764
+ sessionId,
15765
+ prompt: typeof text2 === "string" ? [{ type: "text", text: text2 }] : [...text2]
15766
+ }, timeoutMs, (id) => {
15767
+ requestId = id;
15768
+ }, promptPayloadNote(text2));
15769
+ const agentText = finalAgentMessageText(updates, this.agentLabel);
15770
+ const toolCalls = toolCallEntries(updates);
15771
+ return {
15772
+ stopReason: result?.stopReason ?? "end_turn",
15773
+ updates,
15774
+ agentText,
15775
+ toolCalls
15776
+ };
15777
+ } finally {
15778
+ this.activePromptSessions.delete(sessionId);
15779
+ if (promptRunId && this.activeRunIds.get(sessionId) === promptRunId) {
15780
+ this.activeRunIds.delete(sessionId);
15781
+ }
15782
+ this.off("session/update", onUpdate);
15773
15783
  }
15774
- stripped.push(arg);
15775
15784
  }
15776
- const stdioIndex = stripped.lastIndexOf("stdio");
15777
- const configured = [
15778
- ...selection.model ? ["--model", selection.model] : [],
15779
- ...selection.effort ? ["--reasoning-effort", selection.effort] : []
15780
- ];
15781
- stripped.splice(stdioIndex, 0, ...configured);
15782
- return stripped;
15783
- }
15784
- function advertisedChoiceId(choice) {
15785
- if (typeof choice.id === "string" && choice.id.length > 0)
15786
- return choice.id;
15787
- if (typeof choice.value === "string" && choice.value.length > 0)
15788
- return choice.value;
15789
- return void 0;
15790
- }
15791
- function sortModelChoicesNewestFirst(choices) {
15792
- return choices.map((choice, index) => ({ choice, index })).sort((left, right) => {
15793
- const leftParts = left.choice.id.match(/\d+/g)?.map(Number) ?? [];
15794
- const rightParts = right.choice.id.match(/\d+/g)?.map(Number) ?? [];
15795
- for (let index = 0; index < Math.max(leftParts.length, rightParts.length); index += 1) {
15796
- const delta = (rightParts[index] ?? -1) - (leftParts[index] ?? -1);
15797
- if (delta !== 0)
15798
- return delta;
15785
+ /**
15786
+ * Inject follow-up input into the prompt currently running for this session.
15787
+ * buzz-agent advertises the target run through ACP session metadata; binding
15788
+ * the request to that id prevents a late message from steering a newer turn.
15789
+ */
15790
+ async sessionSteer(sessionId, text2, timeoutMs = 6e4) {
15791
+ if (this.supportsStandardSteering) {
15792
+ const raw2 = await this.request("_session/steering", {
15793
+ sessionId,
15794
+ prompt: [{ type: "text", text: text2 }]
15795
+ }, timeoutMs);
15796
+ if (!raw2.outcome || raw2.outcome === "failed") {
15797
+ throw new Error("ACP session steering failed");
15798
+ }
15799
+ return {
15800
+ runId: this.activeRunId(sessionId) ?? `session:${sessionId}`,
15801
+ messageId: raw2.outcome
15802
+ };
15799
15803
  }
15800
- return left.index - right.index;
15801
- }).map(({ choice }) => choice);
15802
- }
15803
- function parseAdvertisedConfigOptions(raw, preferredModelId, includeLaunchEffort = false) {
15804
- const configOptions = raw?.configOptions;
15805
- const result = [];
15806
- for (const entry of Array.isArray(configOptions) ? configOptions : []) {
15807
- if (!entry || typeof entry !== "object")
15808
- continue;
15809
- const record3 = entry;
15810
- const id = record3.id;
15811
- const category = record3.category;
15812
- if (typeof id !== "string" || typeof category !== "string")
15813
- continue;
15814
- const rawChoices = Array.isArray(record3.options) ? record3.options : [];
15815
- const choices = [];
15816
- for (const rawChoice of rawChoices) {
15817
- if (!rawChoice || typeof rawChoice !== "object")
15818
- continue;
15819
- const choice = rawChoice;
15820
- const choiceId = advertisedChoiceId(choice);
15821
- if (!choiceId)
15822
- continue;
15823
- choices.push({
15824
- id: choiceId,
15825
- ...typeof choice.name === "string" ? { name: choice.name } : {}
15826
- });
15804
+ const expectedRunId = await this.waitForActiveRun(sessionId, Math.min(timeoutMs, 5e3));
15805
+ const raw = await this.request("_goose/unstable/session/steer", {
15806
+ sessionId,
15807
+ prompt: [{ type: "text", text: text2 }],
15808
+ expectedRunId
15809
+ }, timeoutMs);
15810
+ if (!raw.runId || !raw.messageId) {
15811
+ throw new Error("ACP session steer response missing runId or messageId");
15827
15812
  }
15828
- result.push({
15829
- id,
15830
- category,
15831
- ...typeof record3.currentValue === "string" ? { currentValue: record3.currentValue } : {},
15832
- options: category === "model" ? sortModelChoicesNewestFirst(choices) : choices
15833
- });
15813
+ return { runId: raw.runId, messageId: raw.messageId };
15834
15814
  }
15835
- const models = raw?.models;
15836
- if (!models || typeof models !== "object")
15837
- return result;
15838
- const modelState = models;
15839
- const availableModels = Array.isArray(modelState.availableModels) ? modelState.availableModels : [];
15840
- const modelChoices = [];
15841
- for (const entry of availableModels) {
15842
- if (!entry || typeof entry !== "object")
15843
- continue;
15844
- const model = entry;
15845
- if (typeof model.modelId !== "string" || !model.modelId)
15846
- continue;
15847
- modelChoices.push({
15848
- id: model.modelId,
15849
- ...typeof model.name === "string" ? { name: model.name } : {}
15850
- });
15815
+ /** Active ACP run for a session, if the agent has advertised one. */
15816
+ activeRunId(sessionId) {
15817
+ return this.activeRunIds.get(sessionId) ?? (this.activePromptSessions.has(sessionId) ? `session:${sessionId}` : void 0);
15851
15818
  }
15852
- const currentModelId = typeof modelState.currentModelId === "string" ? modelState.currentModelId : void 0;
15853
- if (modelChoices.length > 0 && !result.some((axis) => axis.category === "model")) {
15854
- result.push({
15855
- id: GROK_SESSION_MODEL_AXIS_ID,
15856
- category: "model",
15857
- ...currentModelId ? { currentValue: currentModelId } : {},
15858
- options: sortModelChoicesNewestFirst(modelChoices)
15859
- });
15819
+ sessionCancel(sessionId) {
15820
+ this.notify("session/cancel", { sessionId });
15860
15821
  }
15861
- const effortModelId = preferredModelId ?? currentModelId;
15862
- const currentModel = availableModels.find((entry) => entry && typeof entry === "object" && entry.modelId === effortModelId);
15863
- const meta = currentModel?._meta;
15864
- const modelMeta = meta && typeof meta === "object" ? meta : void 0;
15865
- const efforts = Array.isArray(modelMeta?.reasoningEfforts) ? modelMeta.reasoningEfforts : [];
15866
- const effortChoices = [];
15867
- for (const entry of efforts) {
15868
- if (!entry || typeof entry !== "object")
15869
- continue;
15870
- const effort = entry;
15871
- const id = advertisedChoiceId(effort);
15872
- if (!id)
15873
- continue;
15874
- effortChoices.push({
15875
- id,
15876
- ...typeof effort.label === "string" ? { name: effort.label } : typeof effort.name === "string" ? { name: effort.name } : {}
15877
- });
15822
+ /**
15823
+ * The portable model/effort setter, confirmed round-tripping on pi/codex/
15824
+ * claude (report `data/buzzy-multiagent-runtimes/report.md` §3.2). The
15825
+ * param is `configId`, not `optionId`. Harnesses that advertise this shape
15826
+ * must use it; `session/set_model` is reserved for the distinct standard
15827
+ * `session/new.models` shape. Returns the runtime's updated `configOptions`.
15828
+ * Callers must validate `configId`/`value` against the
15829
+ * allow-listed catalog before calling this — see
15830
+ * `apps/body/src/model-config.ts`'s `assertModelConfigOptionAllowed`; this
15831
+ * method performs no policy checks of its own.
15832
+ */
15833
+ async setConfigOption(sessionId, configId, value) {
15834
+ return this.request("session/set_config_option", { sessionId, configId, value });
15878
15835
  }
15879
- const hasEffortAxis = result.some((axis) => ["thought_level", "effort", "reasoning_effort"].includes(axis.category));
15880
- if (includeLaunchEffort && effortChoices.length > 0 && !hasEffortAxis) {
15881
- const currentEffort = typeof modelMeta?.reasoningEffort === "string" ? modelMeta.reasoningEffort : efforts.find((entry) => entry && typeof entry === "object" && entry.default)?.value;
15882
- result.push({
15883
- id: GROK_LAUNCH_EFFORT_AXIS_ID,
15884
- category: "reasoning_effort",
15885
- ...typeof currentEffort === "string" ? { currentValue: currentEffort } : {},
15886
- options: effortChoices
15887
- });
15836
+ /**
15837
+ * Standard ACP model setter used only when `session/new.models` supplied the
15838
+ * corresponding safe model axis. Most Beeline harnesses use configOptions;
15839
+ * Grok currently exposes models through this standard method instead.
15840
+ */
15841
+ async setModel(sessionId, modelId) {
15842
+ return this.request("session/set_model", { sessionId, modelId });
15888
15843
  }
15889
- return result;
15890
- }
15891
- function filterAllowedModelConfigOptions(options) {
15892
- return options.filter((option) => isAllowedAgentModelConfigCategory(option.category));
15893
- }
15894
- var PROVIDER_CREDENTIAL_ENV_HINTS = {
15895
- openai: ["OPENAI_API_KEY", "OPENAI_COMPAT_API_KEY"],
15896
- openrouter: ["OPENROUTER_API_KEY"],
15897
- anthropic: ["ANTHROPIC_API_KEY"],
15898
- google: ["GOOGLE_API_KEY", "GEMINI_API_KEY"],
15899
- groq: ["GROQ_API_KEY"],
15900
- mistral: ["MISTRAL_API_KEY"],
15901
- deepseek: ["DEEPSEEK_API_KEY"],
15902
- xai: ["XAI_API_KEY"],
15903
- cohere: ["COHERE_API_KEY"],
15904
- together: ["TOGETHER_API_KEY"],
15905
- fireworks: ["FIREWORKS_API_KEY"]
15906
- };
15907
- function credentialedProviders(env) {
15908
- const held = /* @__PURE__ */ new Set();
15909
- for (const [provider, names] of Object.entries(PROVIDER_CREDENTIAL_ENV_HINTS)) {
15910
- if (names.some((name) => Boolean(env[name]?.trim())))
15911
- held.add(provider);
15844
+ clearTimer(p) {
15845
+ if (p.timer) {
15846
+ clearTimeout(p.timer);
15847
+ p.timer = void 0;
15848
+ }
15912
15849
  }
15913
- for (const provider of credentialedPiCustomProviders(env))
15914
- held.add(provider);
15915
- return held;
15916
- }
15917
- function credentialedPiCustomProviders(env) {
15918
- const agentDir = env.PI_CODING_AGENT_DIR?.trim() ? resolve4(env.PI_CODING_AGENT_DIR) : env.HOME?.trim() ? resolve4(env.HOME, ".pi/agent") : void 0;
15919
- const held = /* @__PURE__ */ new Set();
15920
- if (!agentDir)
15921
- return held;
15922
- const path = resolve4(agentDir, "models.json");
15923
- try {
15924
- const stats = lstatSync2(path);
15925
- if (!stats.isFile() || stats.isSymbolicLink() || stats.nlink !== 1 || stats.size > 1048576) {
15926
- return held;
15850
+ /**
15851
+ * Re-arm a pending request's idle timer from its full `idleTimeoutMs`,
15852
+ * called on every ACP activity signal for that request (streamed text
15853
+ * delta, session/update, tool call). A request only times out after this
15854
+ * much wall-clock time with zero activity, not after a fixed deadline —
15855
+ * so an actively-working turn can run indefinitely while a genuinely
15856
+ * wedged one still gets caught.
15857
+ */
15858
+ resetPendingIdleTimeout(id) {
15859
+ const p = this.pending.get(id);
15860
+ if (!p || p.idleTimeoutMs === void 0)
15861
+ return;
15862
+ this.clearTimer(p);
15863
+ const { idleTimeoutMs, method, reject, detail } = p;
15864
+ p.timer = setTimeout(() => {
15865
+ this.pending.delete(id);
15866
+ reject(new AcpRequestTimeoutError(method ?? "request", idleTimeoutMs, this.stderrTail, true, detail ?? ""));
15867
+ }, idleTimeoutMs);
15868
+ }
15869
+ onData(chunk) {
15870
+ this.buf += chunk;
15871
+ let nl;
15872
+ while ((nl = this.buf.indexOf("\n")) >= 0) {
15873
+ const line = this.buf.slice(0, nl).trim();
15874
+ this.buf = this.buf.slice(nl + 1);
15875
+ if (!line)
15876
+ continue;
15877
+ let msg;
15878
+ try {
15879
+ msg = JSON.parse(line);
15880
+ } catch {
15881
+ continue;
15882
+ }
15883
+ this.handleMessage(msg);
15884
+ }
15885
+ }
15886
+ handleMessage(msg) {
15887
+ const id = msg.id;
15888
+ const method = msg.method;
15889
+ if (id !== void 0 && !method) {
15890
+ const n3 = Number(id);
15891
+ const p = this.pending.get(n3);
15892
+ if (!p)
15893
+ return;
15894
+ this.pending.delete(n3);
15895
+ this.clearTimer(p);
15896
+ if (msg.error) {
15897
+ const err = msg.error;
15898
+ const details = typeof err.data?.details === "string" && err.data.details.trim() ? err.data.details.replace(/\s+/g, " ").trim() : "";
15899
+ const meaningful = flattenHarnessCause(this.stderrTail);
15900
+ const extra = [details, meaningful ? `harness stderr: ${meaningful}` : ""].filter(Boolean).join("; ");
15901
+ p.reject(new Error(`ACP error ${err.code}: ${err.message}${extra ? `; ${extra}` : ""}`));
15902
+ } else {
15903
+ p.resolve(msg.result);
15904
+ }
15905
+ return;
15927
15906
  }
15928
- const parsed = JSON.parse(readFileSync2(path, "utf8"));
15929
- const providers = parsed.providers;
15930
- const remember = (name, config) => {
15931
- if (typeof name !== "string" || !name.trim() || !config || typeof config !== "object")
15907
+ if (method && id !== void 0) {
15908
+ if (method === "session/request_permission") {
15909
+ void this.handlePermission(id, msg.params);
15932
15910
  return;
15933
- const apiKey = config.apiKey;
15934
- if (typeof apiKey === "string" && apiKey.trim())
15935
- held.add(name.trim());
15936
- };
15937
- if (Array.isArray(providers)) {
15938
- for (const provider of providers) {
15939
- const record3 = provider;
15940
- remember(record3?.name ?? record3?.id, record3);
15941
15911
  }
15942
- } else if (providers && typeof providers === "object") {
15943
- for (const [name, config] of Object.entries(providers)) {
15944
- remember(name, config);
15912
+ this.write({ jsonrpc: "2.0", id, result: {} });
15913
+ return;
15914
+ }
15915
+ if (method === "session/update") {
15916
+ const params = msg.params;
15917
+ if (params?.sessionId && params.update) {
15918
+ if (params.update.sessionUpdate === "available_commands_update") {
15919
+ const commands = parseAvailableCommands(params.update.availableCommands);
15920
+ this.sessionCommands.set(params.sessionId, commands);
15921
+ this.commandsHandler?.(commands);
15922
+ this.emit("commands", { sessionId: params.sessionId, commands });
15923
+ }
15924
+ const toolCallId = params.update.toolCallId;
15925
+ const sessionUpdate = params.update.sessionUpdate;
15926
+ if (typeof toolCallId === "string" && (sessionUpdate === "tool_call" || sessionUpdate === "tool_call_update")) {
15927
+ const metadataKey = `${params.sessionId}\0${toolCallId}`;
15928
+ const status = params.update.status;
15929
+ if (status === "completed" || status === "failed") {
15930
+ this.toolCallMetadata.delete(metadataKey);
15931
+ } else {
15932
+ const existing = this.toolCallMetadata.get(metadataKey);
15933
+ this.toolCallMetadata.set(metadataKey, {
15934
+ ...existing,
15935
+ toolCallId,
15936
+ ...typeof params.update.title === "string" ? { title: params.update.title } : {},
15937
+ ...typeof params.update.kind === "string" ? { kind: params.update.kind } : {},
15938
+ ..."rawInput" in params.update ? { rawInput: params.update.rawInput } : {},
15939
+ // `content`/`locations` carry the touched file paths for adapters
15940
+ // whose permission request omits them (codex-acp's file-change
15941
+ // request has no rawInput at all) — the corner worktree guard in
15942
+ // `session-sandbox.ts` reads them.
15943
+ ..."content" in params.update ? { content: params.update.content } : {},
15944
+ ..."locations" in params.update ? { locations: params.update.locations } : {}
15945
+ });
15946
+ }
15947
+ }
15948
+ const u4 = {
15949
+ sessionId: params.sessionId,
15950
+ update: params.update
15951
+ };
15952
+ const activeRunId = this.activeRunIdFromUpdate(params.update);
15953
+ if (activeRunId)
15954
+ this.activeRunIds.set(params.sessionId, activeRunId);
15955
+ this.emit("session/update", u4);
15945
15956
  }
15957
+ return;
15946
15958
  }
15947
- } catch {
15948
15959
  }
15949
- return held;
15950
- }
15951
- function filterModelOptionsByCredentials(options, env) {
15952
- const held = credentialedProviders(env);
15953
- return options.map((option) => {
15954
- if (option.category !== "model")
15955
- return option;
15956
- return {
15957
- ...option,
15958
- options: option.options.filter((choice) => {
15959
- const slash = choice.id.indexOf("/");
15960
- if (slash < 0)
15961
- return true;
15962
- return held.has(choice.id.slice(0, slash));
15963
- })
15964
- };
15965
- });
15966
- }
15967
- var DisallowedModelConfigOptionError = class extends Error {
15968
- constructor(configId) {
15969
- super(`model config option "${configId}" is not in the allow-listed catalog`);
15970
- this.name = "DisallowedModelConfigOptionError";
15960
+ activeRunIdFromUpdate(update) {
15961
+ const meta = update._meta;
15962
+ const goose = meta?.goose;
15963
+ return typeof goose?.activeRunId === "string" && goose.activeRunId ? goose.activeRunId : void 0;
15971
15964
  }
15972
- };
15973
- var ModelSelectionUnavailableError = class extends Error {
15974
- label;
15975
- value;
15976
- reason;
15977
- guidance;
15978
- constructor(input) {
15979
- const recovery = input.guidance ? ` ${input.guidance}` : " Choose one of the values in the live harness catalog.";
15980
- super(`${input.label} "${input.value}" is unavailable.${recovery}`);
15981
- this.name = "ModelSelectionUnavailableError";
15982
- this.label = input.label;
15983
- this.value = input.value;
15984
- this.reason = input.reason;
15985
- this.guidance = input.guidance;
15965
+ waitForActiveRun(sessionId, timeoutMs) {
15966
+ const current = this.activeRunIds.get(sessionId);
15967
+ if (current)
15968
+ return Promise.resolve(current);
15969
+ return new Promise((resolve36, reject) => {
15970
+ const onUpdate = (update) => {
15971
+ if (update.sessionId !== sessionId)
15972
+ return;
15973
+ const runId = this.activeRunIdFromUpdate(update.update);
15974
+ if (!runId)
15975
+ return;
15976
+ cleanup();
15977
+ resolve36(runId);
15978
+ };
15979
+ const timer = setTimeout(() => {
15980
+ cleanup();
15981
+ reject(new Error(`ACP session ${sessionId} has no active run to steer`));
15982
+ }, timeoutMs);
15983
+ const cleanup = () => {
15984
+ clearTimeout(timer);
15985
+ this.off("session/update", onUpdate);
15986
+ };
15987
+ this.on("session/update", onUpdate);
15988
+ });
15986
15989
  }
15987
- };
15988
- function withEffectiveCurrentValues(options, selection) {
15989
- if (!selection || !selection.model && !selection.effort)
15990
- return options;
15991
- return options.map((option) => {
15992
- const target = modelSelectionTargets(selection).find((entry) => entry.categories.includes(option.category));
15993
- if (!target?.value || option.currentValue === target.value)
15994
- return option;
15995
- return { ...option, currentValue: target.value };
15996
- });
15997
- }
15998
- function modelSelectionTargets(selection) {
15999
- return [
16000
- { categories: ["model"], label: "model", value: selection.model },
16001
- {
16002
- categories: ["thought_level", "effort", "reasoning_effort"],
16003
- label: "effort",
16004
- value: selection.effort
15990
+ async handlePermission(id, params) {
15991
+ const p = params ?? {};
15992
+ const toolCallId = p.toolCall?.toolCallId;
15993
+ const metadataKey = p.sessionId && toolCallId ? `${p.sessionId}\0${toolCallId}` : void 0;
15994
+ const tracked = metadataKey ? this.toolCallMetadata.get(metadataKey) : void 0;
15995
+ if (tracked)
15996
+ p.toolCall = { ...tracked, ...p.toolCall };
15997
+ let decision2 = this.autoApprove ? "allow" : "reject";
15998
+ if (!this.autoApprove && this.permissionAllowlist) {
15999
+ try {
16000
+ decision2 = this.permissionAllowlist(p) ? "allow" : "reject";
16001
+ } catch (error) {
16002
+ this.emit("permission/error", error);
16003
+ decision2 = "reject";
16004
+ }
16005
+ } else if (this.permissionHandler) {
16006
+ try {
16007
+ decision2 = await this.permissionHandler(p);
16008
+ } catch (error) {
16009
+ this.emit("permission/error", error);
16010
+ decision2 = "reject";
16011
+ }
16005
16012
  }
16006
- ];
16007
- }
16008
- function assertModelConfigAxisAllowed(configId, advertisedOptions) {
16009
- const axis = advertisedOptions.find((option) => option.id === configId);
16010
- if (!axis || !isAllowedAgentModelConfigCategory(axis.category)) {
16011
- throw new DisallowedModelConfigOptionError(configId);
16012
- }
16013
- }
16014
- async function applyAgentModelSelectionWithUpdatedCatalog(client, sessionId, advertisedOptions, selection) {
16015
- let currentOptions = advertisedOptions;
16016
- for (const target of modelSelectionTargets(selection)) {
16017
- if (!target.value)
16018
- continue;
16019
- const axis = currentOptions.find((option) => target.categories.includes(option.category));
16020
- if (!axis)
16021
- continue;
16022
- assertModelConfigAxisAllowed(axis.id, currentOptions);
16023
- if (!axis.options.some((choice) => choice.id === target.value)) {
16024
- throw new ModelSelectionUnavailableError({
16025
- label: target.label,
16026
- value: target.value,
16027
- reason: "not-advertised"
16028
- });
16013
+ if (decision2 === "allow") {
16014
+ const allow = p?.options?.find((o) => o.kind === "allow_once" || o.kind === "allow_always") ?? p?.options?.[0];
16015
+ if (allow?.optionId) {
16016
+ if (metadataKey)
16017
+ this.toolCallMetadata.delete(metadataKey);
16018
+ this.write({
16019
+ jsonrpc: "2.0",
16020
+ id,
16021
+ result: {
16022
+ outcome: { outcome: "selected", optionId: allow.optionId }
16023
+ }
16024
+ });
16025
+ return;
16026
+ }
16029
16027
  }
16030
- try {
16031
- if (axis.id === GROK_SESSION_MODEL_AXIS_ID) {
16032
- if (!client.setModel)
16033
- throw new Error("ACP client does not support session/set_model");
16034
- await client.setModel(sessionId, target.value);
16035
- } else if (axis.id === GROK_LAUNCH_EFFORT_AXIS_ID) {
16036
- if (axis.currentValue !== target.value) {
16037
- throw new Error(`Grok started with reasoning effort "${axis.currentValue ?? "unknown"}", not "${target.value}"`);
16028
+ const reject = p?.options?.find((o) => o.kind === "reject_once" || o.kind === "reject_always");
16029
+ if (reject?.optionId) {
16030
+ if (metadataKey)
16031
+ this.toolCallMetadata.delete(metadataKey);
16032
+ this.write({
16033
+ jsonrpc: "2.0",
16034
+ id,
16035
+ result: {
16036
+ outcome: { outcome: "selected", optionId: reject.optionId }
16038
16037
  }
16039
- } else {
16040
- const updated = await client.setConfigOption(sessionId, axis.id, target.value);
16041
- const refreshed = filterAllowedModelConfigOptions(parseAdvertisedConfigOptions(updated, selection.model));
16042
- if (refreshed.length > 0)
16043
- currentOptions = refreshed;
16044
- }
16045
- } catch (error) {
16046
- throw new ModelSelectionUnavailableError({
16047
- label: target.label,
16048
- value: target.value,
16049
- reason: "provider-refused",
16050
- guidance: error instanceof Error ? error.message : String(error)
16051
16038
  });
16039
+ return;
16040
+ }
16041
+ if (metadataKey)
16042
+ this.toolCallMetadata.delete(metadataKey);
16043
+ this.write({
16044
+ jsonrpc: "2.0",
16045
+ id,
16046
+ result: { outcome: { outcome: "cancelled" } }
16047
+ });
16048
+ }
16049
+ /**
16050
+ * `onStart`, when given, receives the assigned request id synchronously
16051
+ * and marks the request as idle-resettable at `timeoutMs`: callers can
16052
+ * then call `resetPendingIdleTimeout(id)` on activity to defer the
16053
+ * timeout instead of it firing at a fixed deadline.
16054
+ */
16055
+ request(method, params, timeoutMs = 6e4, onStart, detail = "") {
16056
+ if (!this.child || !this.alive) {
16057
+ return Promise.reject(new Error("AcpClient not started"));
16052
16058
  }
16059
+ const id = this.nextId++;
16060
+ const payload = { jsonrpc: "2.0", id, method, params };
16061
+ return new Promise((resolve36, reject) => {
16062
+ const timer = setTimeout(() => {
16063
+ this.pending.delete(id);
16064
+ reject(new AcpRequestTimeoutError(method, timeoutMs, this.stderrTail, Boolean(onStart), detail));
16065
+ }, timeoutMs);
16066
+ this.pending.set(id, {
16067
+ resolve: resolve36,
16068
+ reject,
16069
+ timer,
16070
+ method,
16071
+ ...detail ? { detail } : {},
16072
+ ...onStart ? { idleTimeoutMs: timeoutMs } : {}
16073
+ });
16074
+ onStart?.(id);
16075
+ this.write(payload);
16076
+ });
16053
16077
  }
16054
- return currentOptions;
16055
- }
16056
- async function applyAgentModelSelection(client, sessionId, advertisedOptions, selection) {
16057
- await applyAgentModelSelectionWithUpdatedCatalog(client, sessionId, advertisedOptions, selection);
16058
- }
16078
+ notify(method, params) {
16079
+ this.write({ jsonrpc: "2.0", method, params });
16080
+ }
16081
+ write(obj) {
16082
+ if (!this.child?.stdin.writable)
16083
+ return;
16084
+ this.child.stdin.write(JSON.stringify(obj) + "\n");
16085
+ }
16086
+ };
16059
16087
 
16060
16088
  // apps/body/dist/model-catalog.js
16061
16089
  function modelCatalogProbeEnvironment(agent, agentEnv, scratchCwd) {
@@ -16162,19 +16190,33 @@ function modelUnavailableState(selection, error) {
16162
16190
  }
16163
16191
 
16164
16192
  // apps/body/dist/runtime-model-validation.js
16165
- async function revalidateRuntimeModelSelection(agent, agentEnv, selection, validate = validateAgentModelSelection) {
16193
+ async function revalidateRuntimeModelSelection(agent, agentEnv, selection, validate = validateAgentModelSelection, refreshAdapter) {
16166
16194
  if (!selection.model && !selection.effort)
16167
16195
  return void 0;
16168
16196
  try {
16169
16197
  await validate(agent, agentEnv, selection);
16170
16198
  return void 0;
16171
16199
  } catch (error) {
16200
+ const selectedModelMissing = error instanceof ModelSelectionUnavailableError && error.label === "model" && (error.reason === "axis-missing" || error.reason === "not-advertised");
16201
+ if (selectedModelMissing && refreshAdapter) {
16202
+ try {
16203
+ await refreshAdapter();
16204
+ } catch {
16205
+ return modelUnavailableState(selection, error);
16206
+ }
16207
+ try {
16208
+ await validate(agent, agentEnv, selection);
16209
+ return void 0;
16210
+ } catch (retryError) {
16211
+ return modelUnavailableState(selection, retryError);
16212
+ }
16213
+ }
16172
16214
  return modelUnavailableState(selection, error);
16173
16215
  }
16174
16216
  }
16175
- async function applyRuntimeModelPreflight(config, agent, selection, validate = validateAgentModelSelection) {
16217
+ async function applyRuntimeModelPreflight(config, agent, selection, validate = validateAgentModelSelection, refreshAdapter) {
16176
16218
  config.modelSelection = selection;
16177
- config.modelUnavailable = await revalidateRuntimeModelSelection(agent, config.agentEnv, selection, validate);
16219
+ config.modelUnavailable = await revalidateRuntimeModelSelection(agent, config.agentEnv, selection, validate, refreshAdapter);
16178
16220
  }
16179
16221
 
16180
16222
  // apps/body/dist/model-catalog-sync.js
@@ -16770,7 +16812,7 @@ function describe(error) {
16770
16812
  }
16771
16813
 
16772
16814
  // apps/body/dist/connector-squire.js
16773
- import { execFile, spawn as spawn5 } from "node:child_process";
16815
+ import { execFile, spawn as spawn6 } from "node:child_process";
16774
16816
  import { createHash as createHash2 } from "node:crypto";
16775
16817
  import { lstatSync as lstatSync3, readFileSync as readFileSync4, realpathSync, rmSync } from "node:fs";
16776
16818
  import { homedir as homedir3, hostname, tmpdir as tmpdir2 } from "node:os";
@@ -16919,7 +16961,7 @@ function killConnectTree(child) {
16919
16961
  child.kill();
16920
16962
  }
16921
16963
  var defaultStreamedRunner = (command, args, env) => new Promise((resolve36) => {
16922
- const child = spawn5(command, args, {
16964
+ const child = spawn6(command, args, {
16923
16965
  stdio: ["ignore", "pipe", "pipe"],
16924
16966
  env: env ?? squireConnectProcessEnv(),
16925
16967
  detached: true
@@ -17319,8 +17361,173 @@ async function revokeGrants(mcp, ref) {
17319
17361
  return { revoked, failed };
17320
17362
  }
17321
17363
 
17364
+ // apps/body/dist/connector-tailscale.js
17365
+ import { execFile as execFile2 } from "node:child_process";
17366
+ import { randomUUID as randomUUID2 } from "node:crypto";
17367
+ import { rm as rm2 } from "node:fs/promises";
17368
+ import { tmpdir as tmpdir3, userInfo } from "node:os";
17369
+ import { join as join5 } from "node:path";
17370
+ var COMMAND_TIMEOUT_MS = 2 * 6e4;
17371
+ var OUTPUT_LIMIT = 32 * 1024;
17372
+ var LOGIN_URL = /https:\/\/login\.tailscale\.com\/[A-Za-z0-9_/-]+/u;
17373
+ function clipped(value) {
17374
+ return value.length <= OUTPUT_LIMIT ? value : value.slice(value.length - OUTPUT_LIMIT);
17375
+ }
17376
+ var runTailscaleCommand = (command, args) => new Promise((resolve36) => {
17377
+ execFile2(command, [...args], { timeout: COMMAND_TIMEOUT_MS, maxBuffer: OUTPUT_LIMIT * 2 }, (error, stdout6, stderr) => {
17378
+ const code = error && "code" in error && typeof error.code === "number" ? error.code : error ? null : 0;
17379
+ resolve36({ code, stdout: clipped(stdout6), stderr: clipped(stderr) });
17380
+ });
17381
+ });
17382
+ function parseStatus(output) {
17383
+ try {
17384
+ const parsed = JSON.parse(output);
17385
+ return parsed && typeof parsed === "object" ? parsed : void 0;
17386
+ } catch {
17387
+ return void 0;
17388
+ }
17389
+ }
17390
+ function accountName(status) {
17391
+ const userId = status.Self?.UserID;
17392
+ const user = userId === void 0 ? void 0 : status.User?.[String(userId)];
17393
+ return user?.LoginName ?? user?.DisplayName ?? status.CurrentTailnet?.Name;
17394
+ }
17395
+ function step3(label, status, details = {}) {
17396
+ return { label, status, ...details };
17397
+ }
17398
+ async function readStatus(run2) {
17399
+ const result = await run2("tailscale", ["status", "--json"]);
17400
+ return parseStatus(result.stdout || result.stderr);
17401
+ }
17402
+ async function ensureInstalled(run2, onProgress, platform) {
17403
+ const version = await run2("tailscale", ["version"]);
17404
+ if (version.code === 0) {
17405
+ return {
17406
+ ok: true,
17407
+ steps: [step3("Tailscale installed", "done", { output: version.stdout.trim() })]
17408
+ };
17409
+ }
17410
+ if (platform !== "linux") {
17411
+ const reason = "Tailscale is not installed; install the Tailscale app on this helper and retry";
17412
+ return {
17413
+ ok: false,
17414
+ result: {
17415
+ status: "error",
17416
+ steps: [step3("Tailscale installed", "failed", { reason })],
17417
+ errorMessage: reason
17418
+ }
17419
+ };
17420
+ }
17421
+ const script = join5(tmpdir3(), `beeline-tailscale-install-${process.pid}-${randomUUID2()}.sh`);
17422
+ const steps = [
17423
+ step3("Tailscale installed", "running", {
17424
+ command: "curl -fsSL https://tailscale.com/install.sh"
17425
+ }),
17426
+ step3("Tailnet signed in", "pending")
17427
+ ];
17428
+ onProgress(steps);
17429
+ try {
17430
+ const download = await run2("curl", ["-fsSL", "https://tailscale.com/install.sh", "-o", script]);
17431
+ if (download.code !== 0) {
17432
+ const reason = (download.stderr || download.stdout || "Tailscale installer download failed").trim();
17433
+ return {
17434
+ ok: false,
17435
+ result: {
17436
+ status: "error",
17437
+ steps: [step3("Tailscale installed", "failed", { reason })],
17438
+ errorMessage: reason
17439
+ }
17440
+ };
17441
+ }
17442
+ const install = await run2("sh", [script]);
17443
+ if (install.code !== 0) {
17444
+ const reason = (install.stderr || install.stdout || "Tailscale installation failed").trim();
17445
+ return {
17446
+ ok: false,
17447
+ result: {
17448
+ status: "error",
17449
+ steps: [step3("Tailscale installed", "failed", { reason })],
17450
+ errorMessage: reason
17451
+ }
17452
+ };
17453
+ }
17454
+ return {
17455
+ ok: true,
17456
+ steps: [step3("Tailscale installed", "done", { output: install.stdout.trim() })]
17457
+ };
17458
+ } finally {
17459
+ await rm2(script, { force: true }).catch(() => {
17460
+ });
17461
+ }
17462
+ }
17463
+ async function installTailscale(options = {}) {
17464
+ const run2 = options.run ?? runTailscaleCommand;
17465
+ const onProgress = options.onProgress ?? (() => {
17466
+ });
17467
+ const platform = options.platform ?? process.platform;
17468
+ const installed = await ensureInstalled(run2, onProgress, platform);
17469
+ if (!installed.ok)
17470
+ return installed.result;
17471
+ const current = await readStatus(run2);
17472
+ if (current?.BackendState === "Running") {
17473
+ return {
17474
+ status: "connected",
17475
+ steps: [...installed.steps, step3("Tailnet signed in", "done")],
17476
+ ...accountName(current) ? { signedInAs: accountName(current) } : {}
17477
+ };
17478
+ }
17479
+ if (options.signIn) {
17480
+ const steps2 = [
17481
+ ...installed.steps,
17482
+ step3("Tailnet signed in", "running", { command: "tailscale up" })
17483
+ ];
17484
+ return { status: "installing", steps: steps2, signIn: options.signIn };
17485
+ }
17486
+ const steps = [
17487
+ ...installed.steps,
17488
+ step3("Tailnet signed in", "running", { command: "tailscale up" })
17489
+ ];
17490
+ onProgress(steps);
17491
+ let accountUsername;
17492
+ try {
17493
+ accountUsername = userInfo().username;
17494
+ } catch {
17495
+ }
17496
+ const operator = [options.operator, process.env.USER, accountUsername].find((candidate) => typeof candidate === "string" && /^[A-Za-z_][A-Za-z0-9_-]*$/u.test(candidate));
17497
+ const upArgs = ["tailscale", "up", "--timeout=10s"];
17498
+ if (operator)
17499
+ upArgs.push(`--operator=${operator}`);
17500
+ const getuid = options.getuid ?? process.getuid;
17501
+ const root = getuid?.() === 0;
17502
+ const up = platform === "linux" && !root ? await run2("sudo", ["-n", ...upArgs]) : await run2("tailscale", upArgs.slice(1));
17503
+ const after = await readStatus(run2);
17504
+ if (after?.BackendState === "Running") {
17505
+ return {
17506
+ status: "connected",
17507
+ steps: [...installed.steps, step3("Tailnet signed in", "done")],
17508
+ ...accountName(after) ? { signedInAs: accountName(after) } : {}
17509
+ };
17510
+ }
17511
+ const output = `${up.stdout}
17512
+ ${up.stderr}`;
17513
+ const url = LOGIN_URL.exec(output)?.[0];
17514
+ if (url) {
17515
+ return {
17516
+ status: "installing",
17517
+ steps,
17518
+ signIn: { method: "oauth", url, browserLocation: { kind: "none" } }
17519
+ };
17520
+ }
17521
+ const reason = (up.stderr || up.stdout || "Tailscale could not start on this helper").trim();
17522
+ return {
17523
+ status: "error",
17524
+ steps: [...installed.steps, step3("Tailnet signed in", "failed", { reason })],
17525
+ errorMessage: reason
17526
+ };
17527
+ }
17528
+
17322
17529
  // apps/body/dist/squire-mcp-client.js
17323
- import { spawn as spawn6 } from "node:child_process";
17530
+ import { spawn as spawn7 } from "node:child_process";
17324
17531
  import { homedir as homedir4 } from "node:os";
17325
17532
  var INITIALIZE_TIMEOUT_MS = 3e4;
17326
17533
  var CALL_TIMEOUT_MS = 12e4;
@@ -17378,7 +17585,7 @@ var StdioSquireMcpClient = class {
17378
17585
  }
17379
17586
  initialize() {
17380
17587
  const launch = squireFacadeLaunch(homedir4());
17381
- const child = (this.options.spawn ?? spawn6)(this.options.command ?? launch.command, [...this.options.args ?? launch.args], { env: this.options.env ?? { ...squireConnectProcessEnv(), ...launch.env } });
17588
+ const child = (this.options.spawn ?? spawn7)(this.options.command ?? launch.command, [...this.options.args ?? launch.args], { env: this.options.env ?? { ...squireConnectProcessEnv(), ...launch.env } });
17382
17589
  this.child = child;
17383
17590
  this.buffer = "";
17384
17591
  child.stdout.setEncoding("utf8");
@@ -17470,6 +17677,7 @@ var ConnectorAssignmentLoop = class {
17470
17677
  log;
17471
17678
  install;
17472
17679
  installGoogle;
17680
+ installTailscale;
17473
17681
  googleHomeDir;
17474
17682
  readVaultFn;
17475
17683
  revokeGrantsFn;
@@ -17478,6 +17686,8 @@ var ConnectorAssignmentLoop = class {
17478
17686
  timer;
17479
17687
  /** Armed only while this helper owns a live sign-in ceremony. */
17480
17688
  connectWatch;
17689
+ /** Armed only while a Tailscale browser login is waiting for its callback. */
17690
+ tailscaleWatch;
17481
17691
  started = false;
17482
17692
  stopped = false;
17483
17693
  /** One install at a time per connector; other polls skip it. */
@@ -17496,6 +17706,7 @@ var ConnectorAssignmentLoop = class {
17496
17706
  onProgress,
17497
17707
  resolvedCredentials: sharedCredentials ? sharedCredentials() : this.resolveGoogleCredentials()
17498
17708
  }));
17709
+ this.installTailscale = options.installTailscale ?? installTailscale;
17499
17710
  this.googleHomeDir = options.googleHome ?? process.env.BEELINE_AGENT_HOME ?? process.cwd();
17500
17711
  this.readVaultFn = options.readVault ?? readVault;
17501
17712
  this.revokeGrantsFn = options.revokeGrants ?? revokeGrants;
@@ -17529,6 +17740,10 @@ var ConnectorAssignmentLoop = class {
17529
17740
  this.cancel(this.connectWatch);
17530
17741
  this.connectWatch = void 0;
17531
17742
  }
17743
+ if (this.tailscaleWatch !== void 0) {
17744
+ this.cancel(this.tailscaleWatch);
17745
+ this.tailscaleWatch = void 0;
17746
+ }
17532
17747
  }
17533
17748
  /**
17534
17749
  * Watch the sign-in this helper is holding open. The phone paints
@@ -17641,12 +17856,57 @@ var ConnectorAssignmentLoop = class {
17641
17856
  }
17642
17857
  async handle(assignment) {
17643
17858
  if (assignment.kind === "install") {
17644
- await this.runInstall(assignment.connectorId);
17645
- } else if (assignment.kind === "sync")
17859
+ if (assignment.connectorType === "tailscale") {
17860
+ await this.runTailscaleInstall(assignment.connectorId);
17861
+ } else {
17862
+ await this.runInstall(assignment.connectorId);
17863
+ }
17864
+ } else if (assignment.kind === "sync" && assignment.connectorType !== "tailscale") {
17646
17865
  await this.runSync();
17647
- else if (assignment.kind === "revoke-grants")
17866
+ } else if (assignment.kind === "revoke-grants")
17648
17867
  await this.runRevoke(assignment.connectorId, assignment.reference);
17649
17868
  }
17869
+ /** Install Tailscale and publish its browser login URL until the tailnet is connected. */
17870
+ async runTailscaleInstall(connectorId) {
17871
+ const report = async (steps) => {
17872
+ try {
17873
+ await this.api.execute("postConnectorStatus", { agentId: this.agentId, connectorId, steps });
17874
+ } catch (error) {
17875
+ this.log(`step report failed: ${describe2(error)}`);
17876
+ }
17877
+ };
17878
+ const existing = await this.connectorRow(connectorId);
17879
+ const result = await this.installTailscale({
17880
+ onProgress: report,
17881
+ ...existing?.signIn ? { signIn: existing.signIn } : {}
17882
+ });
17883
+ if (result.status === "connected") {
17884
+ if (this.tailscaleWatch !== void 0) {
17885
+ this.cancel(this.tailscaleWatch);
17886
+ this.tailscaleWatch = void 0;
17887
+ }
17888
+ await this.api.execute("installConnector", {
17889
+ agentId: this.agentId,
17890
+ connectorId,
17891
+ ...result.signedInAs ? { signedInAs: result.signedInAs } : {}
17892
+ });
17893
+ return;
17894
+ }
17895
+ await this.api.execute("postConnectorStatus", {
17896
+ agentId: this.agentId,
17897
+ connectorId,
17898
+ steps: result.steps,
17899
+ signIn: result.status === "installing" ? result.signIn : null,
17900
+ ...result.status === "error" ? { errorMessage: result.errorMessage } : {}
17901
+ });
17902
+ if (result.status === "installing" && this.tailscaleWatch === void 0) {
17903
+ this.tailscaleWatch = this.schedule(() => {
17904
+ this.tailscaleWatch = void 0;
17905
+ if (!this.stopped)
17906
+ void this.runOnce();
17907
+ }, CONNECT_WATCH_INTERVAL_MS);
17908
+ }
17909
+ }
17650
17910
  /** Google tool connectors keep their manual credentials next to the runtime. */
17651
17911
  googleHome() {
17652
17912
  return this.googleHomeDir;
@@ -17764,7 +18024,7 @@ var ConnectorAssignmentLoop = class {
17764
18024
  const status = await this.connectorRow(connectorId);
17765
18025
  if (!status)
17766
18026
  return false;
17767
- return status.steps.length > 0 && status.steps.every((step3) => step3.status === "pending");
18027
+ return status.steps.length > 0 && status.steps.every((step4) => step4.status === "pending");
17768
18028
  }
17769
18029
  /** One vault report covers every live trusty-squire connector on this helper. */
17770
18030
  async runSync() {
@@ -17800,7 +18060,7 @@ function isAgentCommand(value) {
17800
18060
  "reason"
17801
18061
  ].every((key) => typeof c2[key] === "string" && c2[key].length > 0))
17802
18062
  return false;
17803
- if (!["input", "resume", "stop"].includes(String(c2.action)) || !Number.isInteger(c2.agentDepth) || Number(c2.agentDepth) < 0 || Number(c2.agentDepth) > 3)
18063
+ if (!["input", "resume", "stop", "restart"].includes(String(c2.action)) || !Number.isInteger(c2.agentDepth) || Number(c2.agentDepth) < 0 || Number(c2.agentDepth) > 3)
17804
18064
  return false;
17805
18065
  const source = c2.source;
17806
18066
  return Boolean(source && typeof source.id === "string" && typeof source.authorId === "string" && typeof source.body === "string" && typeof source.createdAt === "number" && Array.isArray(source.attachments));
@@ -17932,7 +18192,7 @@ function hiccupBackoffMs(attempt) {
17932
18192
 
17933
18193
  // apps/body/dist/daemon-api-client.js
17934
18194
  import { resolve as resolve9 } from "node:path";
17935
- import { randomUUID as randomUUID2 } from "node:crypto";
18195
+ import { randomUUID as randomUUID3 } from "node:crypto";
17936
18196
 
17937
18197
  // node_modules/ws/wrapper.mjs
17938
18198
  var import_stream = __toESM(require_stream(), 1);
@@ -17947,12 +18207,12 @@ var wrapper_default = import_websocket.default;
17947
18207
 
17948
18208
  // apps/body/dist/runtime.js
17949
18209
  import { randomBytes as randomBytes5 } from "node:crypto";
17950
- import { execFile as execFile2 } from "node:child_process";
18210
+ import { execFile as execFile3 } from "node:child_process";
17951
18211
  import { closeSync, openSync, readFileSync as readFileSync5 } from "node:fs";
17952
18212
  import { mkdir as mkdir2, readFile as readFile2, readdir, rename as rename2, stat, unlink as unlink2, writeFile as writeFile3 } from "node:fs/promises";
17953
18213
  import { homedir as homedir5 } from "node:os";
17954
18214
  import { dirname as dirname6, resolve as resolve8 } from "node:path";
17955
- import { spawn as spawn7 } from "node:child_process";
18215
+ import { spawn as spawn8 } from "node:child_process";
17956
18216
  import { promisify } from "node:util";
17957
18217
 
17958
18218
  // node_modules/@noble/hashes/_u64.js
@@ -22339,7 +22599,7 @@ function alphabet(letters) {
22339
22599
  };
22340
22600
  }
22341
22601
  // @__NO_SIDE_EFFECTS__
22342
- function join5(separator = "") {
22602
+ function join6(separator = "") {
22343
22603
  astr("join", separator);
22344
22604
  return {
22345
22605
  encode: (from) => {
@@ -22469,8 +22729,8 @@ var base64 = hasBase64Builtin ? {
22469
22729
  decode(s) {
22470
22730
  return decodeBase64Builtin(s, false);
22471
22731
  }
22472
- } : /* @__PURE__ */ chain(/* @__PURE__ */ radix2(6), /* @__PURE__ */ alphabet("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"), /* @__PURE__ */ padding(6), /* @__PURE__ */ join5(""));
22473
- var BECH_ALPHABET = /* @__PURE__ */ chain(/* @__PURE__ */ alphabet("qpzry9x8gf2tvdw0s3jn54khce6mua7l"), /* @__PURE__ */ join5(""));
22732
+ } : /* @__PURE__ */ chain(/* @__PURE__ */ radix2(6), /* @__PURE__ */ alphabet("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"), /* @__PURE__ */ padding(6), /* @__PURE__ */ join6(""));
22733
+ var BECH_ALPHABET = /* @__PURE__ */ chain(/* @__PURE__ */ alphabet("qpzry9x8gf2tvdw0s3jn54khce6mua7l"), /* @__PURE__ */ join6(""));
22474
22734
  var POLYMOD_GENERATORS = [996825010, 642813549, 513874426, 1027748829, 705979059];
22475
22735
  function bech32Polymod(pre) {
22476
22736
  const b = pre >> 25;
@@ -26075,13 +26335,13 @@ var NegentropyStorageVector = class {
26075
26335
  let count = last - first;
26076
26336
  while (count > 0) {
26077
26337
  let it = first;
26078
- let step3 = Math.floor(count / 2);
26079
- it += step3;
26338
+ let step4 = Math.floor(count / 2);
26339
+ it += step4;
26080
26340
  if (cmp(arr[it])) {
26081
26341
  first = ++it;
26082
- count -= step3 + 1;
26342
+ count -= step4 + 1;
26083
26343
  } else {
26084
- count = step3;
26344
+ count = step4;
26085
26345
  }
26086
26346
  }
26087
26347
  return first;
@@ -26483,7 +26743,7 @@ function decodeNsec(nsec) {
26483
26743
  }
26484
26744
 
26485
26745
  // apps/body/dist/runtime.js
26486
- var execFileAsync = promisify(execFile2);
26746
+ var execFileAsync = promisify(execFile3);
26487
26747
  var DEFAULT_AGENT_IDENTITY_NAME = "beeline-agent";
26488
26748
  var DEFAULT_BODY_IDENTITY_NAME = "beeline-body";
26489
26749
  var DEFAULT_DAEMON_MONOLITH_BASE_URL = "https://server.usebeeline.app";
@@ -26731,7 +26991,7 @@ async function launchRuntimeDaemon(configPath, opts = {}) {
26731
26991
  const entrypoint = opts.entrypoint ?? process.argv[1];
26732
26992
  if (!entrypoint)
26733
26993
  throw new Error("cannot resolve daemon CLI entrypoint");
26734
- const child = spawn7(process.execPath, [...opts.execArgv ?? [], entrypoint, "daemon", "--config", resolve8(configPath)], {
26994
+ const child = spawn8(process.execPath, [...opts.execArgv ?? [], entrypoint, "daemon", "--config", resolve8(configPath)], {
26735
26995
  cwd: directory,
26736
26996
  env: opts.env ?? process.env,
26737
26997
  detached: !foreground,
@@ -26792,7 +27052,7 @@ var DaemonApiClient = class {
26792
27052
  agentId;
26793
27053
  fetchImpl;
26794
27054
  webSocketFactory;
26795
- presenceLifecycleId = randomUUID2();
27055
+ presenceLifecycleId = randomUUID3();
26796
27056
  liveSocket;
26797
27057
  liveReconnect;
26798
27058
  liveReconnectDelayMs = 1e3;
@@ -27061,15 +27321,15 @@ async function activateDaemonTransport(path, fetchImpl = fetch) {
27061
27321
  }
27062
27322
 
27063
27323
  // apps/body/dist/room-runtime.js
27064
- import { execFile as execFile7 } from "node:child_process";
27324
+ import { execFile as execFile8 } from "node:child_process";
27065
27325
  import { createHash as createHash7 } from "node:crypto";
27066
27326
  import { existsSync as existsSync6, mkdirSync as mkdirSync3 } from "node:fs";
27067
- import { mkdir as mkdir15, rm as rm5 } from "node:fs/promises";
27327
+ import { mkdir as mkdir15, rm as rm6 } from "node:fs/promises";
27068
27328
  import { dirname as dirname13, resolve as resolve24 } from "node:path";
27069
27329
  import { promisify as promisify5 } from "node:util";
27070
27330
 
27071
27331
  // apps/body/dist/grant-runner.js
27072
- import { execFile as execFile3 } from "node:child_process";
27332
+ import { execFile as execFile4 } from "node:child_process";
27073
27333
  import { createHash as createHash3, randomBytes as randomBytes6 } from "node:crypto";
27074
27334
  import { readFile as readFile4 } from "node:fs/promises";
27075
27335
  import { createServer } from "node:http";
@@ -27118,6 +27378,18 @@ function commandGrantMatches(rule, requested) {
27118
27378
  return rule.argv.every((word, index) => requested[index] === word);
27119
27379
  }
27120
27380
  var DECISION_LINE = new RegExp(`^(.+?) (approved once|approved|declined) (${AGENT_GRANT_KINDS.join("|")}) (.+)$`, "s");
27381
+ function parseGrantDecisionLine(body) {
27382
+ const match = DECISION_LINE.exec(body);
27383
+ if (!match)
27384
+ return void 0;
27385
+ const decision2 = match[2] === "approved once" ? "once" : match[2] === "approved" ? "always" : "deny";
27386
+ return {
27387
+ deciderName: match[1],
27388
+ decision: decision2,
27389
+ kind: match[3],
27390
+ target: match[4]
27391
+ };
27392
+ }
27121
27393
  var AGENT_GRANT_ESCALATION_REASONS = {
27122
27394
  "unseen-script": "it runs a script whose contents nobody has read",
27123
27395
  credential: "it names a credential or environment file"
@@ -27737,7 +28009,7 @@ var GrantCommandRunner = class {
27737
28009
  const sandboxMarker = runsOnHost ? void 0 : `[beeline-sandbox-started:${randomBytes6(16).toString("hex")}]`;
27738
28010
  const spawn13 = runsOnHost ? { command: argv[0], args: argv.slice(1) } : roomSandboxCommand(policy, room.cwd, argv, sandboxMarker);
27739
28011
  const outcome = await new Promise((resolveRun) => {
27740
- const child = execFile3(spawn13.command, spawn13.args, {
28012
+ const child = execFile4(spawn13.command, spawn13.args, {
27741
28013
  cwd: room.cwd,
27742
28014
  env,
27743
28015
  timeout: this.options.timeoutMs ?? GRANT_COMMAND_TIMEOUT_MS,
@@ -28033,16 +28305,16 @@ function captureConnectionUsage(recorder, turn, calls) {
28033
28305
  }
28034
28306
 
28035
28307
  // apps/body/dist/server-command-intake.js
28036
- import { randomUUID as randomUUID3 } from "node:crypto";
28308
+ import { randomUUID as randomUUID4 } from "node:crypto";
28037
28309
  import { mkdir as mkdir4, writeFile as writeFile5 } from "node:fs/promises";
28038
- import { tmpdir as tmpdir3 } from "node:os";
28039
- import { dirname as dirname9, join as join6 } from "node:path";
28310
+ import { tmpdir as tmpdir4 } from "node:os";
28311
+ import { dirname as dirname9, join as join7 } from "node:path";
28040
28312
  var CommandExecutionContext = class {
28041
- generationId = randomUUID3();
28313
+ generationId = randomUUID4();
28042
28314
  path;
28043
28315
  current;
28044
28316
  constructor(root) {
28045
- this.path = join6(root ?? tmpdir3(), `beeline-command-${this.generationId}.json`);
28317
+ this.path = join7(root ?? tmpdir4(), `beeline-command-${this.generationId}.json`);
28046
28318
  }
28047
28319
  async enter(command) {
28048
28320
  this.current = command;
@@ -28133,7 +28405,9 @@ async function runServerCommandIntake(options) {
28133
28405
  return;
28134
28406
  for (const command of [...pending.values()]) {
28135
28407
  validateServerCommand(command, roomId, agentId);
28136
- if (busy && command.action !== "stop")
28408
+ if (busy && command.action !== "stop" && command.action !== "restart")
28409
+ continue;
28410
+ if ((command.action === "input" || command.action === "resume") && options.canStartTurn?.() === false)
28137
28411
  continue;
28138
28412
  pending.delete(command.id);
28139
28413
  claimed.add(command.id);
@@ -28148,7 +28422,11 @@ async function runServerCommandIntake(options) {
28148
28422
  options.onError?.(error);
28149
28423
  continue;
28150
28424
  }
28151
- if (command.action === "stop") {
28425
+ if (command.action === "restart") {
28426
+ if (!options.restart)
28427
+ throw new Error("restart command is not supported by this helper");
28428
+ options.restart(command);
28429
+ } else if (command.action === "stop") {
28152
28430
  options.stop(command.turnRequestId);
28153
28431
  await api.execute("acknowledgeAgentCommand", {
28154
28432
  roomId,
@@ -28180,7 +28458,7 @@ async function runServerCommandIntake(options) {
28180
28458
  wake = done;
28181
28459
  const timer = setTimeout(() => done(true), pushIntakeAcknowledged ? 6e4 : options.pollMs ?? 1e3);
28182
28460
  signal?.addEventListener("abort", aborted, { once: true });
28183
- if (pending.size && !busy)
28461
+ if (!busy && [...pending.values()].some((command) => command.action === "stop" || command.action === "restart" || options.canStartTurn?.() !== false))
28184
28462
  done(false);
28185
28463
  });
28186
28464
  if (signal?.aborted)
@@ -28199,11 +28477,11 @@ async function runServerCommandIntake(options) {
28199
28477
  }
28200
28478
 
28201
28479
  // apps/body/dist/monolith-corner-turn.js
28202
- import { execFile as execFile6 } from "node:child_process";
28480
+ import { execFile as execFile7 } from "node:child_process";
28203
28481
  import { createHash as createHash6 } from "node:crypto";
28204
28482
  import { mkdir as mkdir14 } from "node:fs/promises";
28205
28483
  import { homedir as homedir10 } from "node:os";
28206
- import { join as join12 } from "node:path";
28484
+ import { join as join13 } from "node:path";
28207
28485
  import { promisify as promisify4 } from "node:util";
28208
28486
 
28209
28487
  // apps/body/dist/agent-home.js
@@ -29005,10 +29283,10 @@ function stringify(obj, { maxDepth = 1e3, numbersAsFloat = false } = {}) {
29005
29283
  }
29006
29284
 
29007
29285
  // apps/body/dist/agent-home.js
29008
- import { createHash as createHash4, randomUUID as randomUUID4 } from "node:crypto";
29009
- import { chmod as chmod3, copyFile, lstat as lstat2, mkdir as mkdir6, readFile as readFile6, readdir as readdir2, realpath, rename as rename3, rm as rm2, symlink, unlink as unlink3, writeFile as writeFile7 } from "node:fs/promises";
29286
+ import { createHash as createHash4, randomUUID as randomUUID5 } from "node:crypto";
29287
+ import { chmod as chmod3, copyFile, lstat as lstat2, mkdir as mkdir6, readFile as readFile6, readdir as readdir2, realpath, rename as rename3, rm as rm3, symlink, unlink as unlink3, writeFile as writeFile7 } from "node:fs/promises";
29010
29288
  import { homedir as homedir8 } from "node:os";
29011
- import { basename as basename5, dirname as dirname10, join as join8, relative as relative2, resolve as resolve16, sep } from "node:path";
29289
+ import { basename as basename5, dirname as dirname10, join as join9, relative as relative2, resolve as resolve16, sep } from "node:path";
29012
29290
 
29013
29291
  // apps/body/dist/beeline-skill.js
29014
29292
  import { readFileSync as readFileSync6 } from "node:fs";
@@ -29020,6 +29298,7 @@ var MESSAGE_REACTION_EMOJIS = ["\u{1F44D}", "\u2764\uFE0F", "\u{1F602}", "\u{1F3
29020
29298
  // packages/api-contract/dist/workbench.js
29021
29299
  var CONNECTABLE_CONNECTOR_KINDS = [
29022
29300
  "trusty-squire",
29301
+ "tailscale",
29023
29302
  "google-gmail",
29024
29303
  "google-calendar",
29025
29304
  "google-drive",
@@ -29141,7 +29420,7 @@ When your corner's pull request is ready, merging is your step: once the configu
29141
29420
 
29142
29421
  A **tool** is something you can use once a human adds it; a **key** is the credential that tool holds for that human. You spend a key through the mounted connector and never see the credential itself.
29143
29422
 
29144
- The tools this build knows, what each is for, and which the person you are answering already has are one call away: beeline-agent workbench_status. Trusty Squire is vaulted credentials plus a browser that signs up and signs in for you; the Google tools (Gmail, Calendar, Drive, YouTube) work through the person's own Google sign-in \u2014 the same Workbench overlay, never a second Google window. YouTube Analytics answers only the channel owner account, not a manager. The wallet is created only from the Workbench page, and Tailscale is not available yet.
29423
+ The tools this build knows, what each is for, and which the person you are answering already has are one call away: beeline-agent workbench_status. Trusty Squire is vaulted credentials plus a browser that signs up and signs in for you; the Google tools (Gmail, Calendar, Drive, YouTube) work through the person's own Google sign-in \u2014 the same Workbench overlay, never a second Google window. YouTube Analytics answers only the channel owner account, not a manager. Tailscale installs its CLI on the selected helper and opens Tailscale's own browser sign-in; once connected, use the CLI for tailnet resources and tailscale file cp or tailscale file get for Taildrop. The wallet is created only from the Workbench page.
29145
29424
 
29146
29425
  **Offer the tool at the moment you need it.** When the work in front of you needs a tool the person does not have, do not send them to a settings page and do not stop at naming it. Call workbench_status first - a tool they already have is used, not offered. Then call offer_connector with the connectorType and one short reason: a card appears in this Room, spoken by you, addressed to the person you are answering, with one action. Only that person or a Workspace admin can accept it; accepting adds the tool on your machine, and the sign-in or keys stay theirs. Your turn pauses on the card - say in prose what you are waiting for and end the turn; you are woken when it is added, and then you carry on.
29147
29426
 
@@ -29398,7 +29677,7 @@ function stringArray(value) {
29398
29677
  }
29399
29678
 
29400
29679
  // apps/body/dist/host-mcp-route.js
29401
- import { join as join7, resolve as resolve14 } from "node:path";
29680
+ import { join as join8, resolve as resolve14 } from "node:path";
29402
29681
  var import_yaml = __toESM(require_dist(), 1);
29403
29682
  function grantedMcpServerNames(grants) {
29404
29683
  const names = /* @__PURE__ */ new Set();
@@ -29415,18 +29694,33 @@ function ungatedHostServers(hostServers, granted) {
29415
29694
  const allowed = new Set(granted);
29416
29695
  return hostServers.filter((name) => !allowed.has(name));
29417
29696
  }
29418
- function grantedHostRoutesFromList(result) {
29697
+ async function claimGrantedHostRoutes(result, consumeOnce) {
29419
29698
  if (!result || typeof result !== "object" || !("grants" in result))
29420
29699
  return [];
29421
29700
  const grants = result.grants;
29422
29701
  if (!Array.isArray(grants))
29423
29702
  return [];
29424
- return grantedMcpServerNames(grants.flatMap((entry) => {
29703
+ const mounted = [];
29704
+ for (const entry of grants) {
29425
29705
  if (!entry || typeof entry !== "object")
29426
- return [];
29427
- const row = entry;
29428
- return typeof row.kind === "string" && typeof row.target === "string" ? [{ kind: row.kind, target: row.target }] : [];
29429
- }));
29706
+ continue;
29707
+ const grant = entry;
29708
+ if (grant.kind !== "mcp" || typeof grant.target !== "string")
29709
+ continue;
29710
+ if (grant.status === "once") {
29711
+ if (typeof grant.grantId !== "string" || !grant.grantId)
29712
+ continue;
29713
+ try {
29714
+ await consumeOnce(grant.grantId);
29715
+ } catch {
29716
+ continue;
29717
+ }
29718
+ } else if (grant.status !== "approved") {
29719
+ continue;
29720
+ }
29721
+ mounted.push({ kind: "mcp", target: grant.target });
29722
+ }
29723
+ return grantedMcpServerNames(mounted);
29430
29724
  }
29431
29725
  function recordValue(value) {
29432
29726
  return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
@@ -29449,7 +29743,7 @@ function grantedSquireHostRoute(granted, declarations = {}) {
29449
29743
  });
29450
29744
  }
29451
29745
  function hostRouteEnv(hostHome) {
29452
- return { XDG_CONFIG_HOME: join7(resolve14(hostHome), ".config") };
29746
+ return { XDG_CONFIG_HOME: join8(resolve14(hostHome), ".config") };
29453
29747
  }
29454
29748
  function rewriteHostMcpDeclaration(name, declaration, hostHome) {
29455
29749
  const next = { ...declaration };
@@ -30633,7 +30927,7 @@ async function provisionManagedSkillsDir(target, managedSkills, sharedSkills, op
30633
30927
  const plan = await planManagedSkills(managedSkills, sharedSkills, optionalShares);
30634
30928
  if (await materializedSkillManifest(target) === plan.manifest)
30635
30929
  return;
30636
- const staged = resolve16(parent, `.skills.${process.pid}.${randomUUID4()}.tmp`);
30930
+ const staged = resolve16(parent, `.skills.${process.pid}.${randomUUID5()}.tmp`);
30637
30931
  await mkdir6(staged, { mode: 448 });
30638
30932
  try {
30639
30933
  for (const entry of plan.entries) {
@@ -30647,10 +30941,10 @@ async function provisionManagedSkillsDir(target, managedSkills, sharedSkills, op
30647
30941
  }
30648
30942
  const existing = await lstat2(target).catch(() => void 0);
30649
30943
  if (existing)
30650
- await rm2(target, { recursive: existing.isDirectory(), force: true });
30944
+ await rm3(target, { recursive: existing.isDirectory(), force: true });
30651
30945
  await rename3(staged, target);
30652
30946
  } finally {
30653
- await rm2(staged, { recursive: true, force: true });
30947
+ await rm3(staged, { recursive: true, force: true });
30654
30948
  }
30655
30949
  }
30656
30950
  async function planManagedSkills(managedSkills, sharedSkills, optionalShares) {
@@ -30669,8 +30963,8 @@ async function planManagedSkills(managedSkills, sharedSkills, optionalShares) {
30669
30963
  try {
30670
30964
  const tree = [];
30671
30965
  await walkSafeSkillTree(shared.source, shared.source, {
30672
- directory: async (rel) => void tree.push(`d ${join8(shared.name, rel)}`),
30673
- file: async (rel, realPath) => void tree.push(`f ${join8(shared.name, rel)} ${sha2563(await readFile6(realPath))}`)
30966
+ directory: async (rel) => void tree.push(`d ${join9(shared.name, rel)}`),
30967
+ file: async (rel, realPath) => void tree.push(`f ${join9(shared.name, rel)} ${sha2563(await readFile6(realPath))}`)
30674
30968
  });
30675
30969
  entries.push({ kind: "shared", name: shared.name, source: shared.source });
30676
30970
  lines.push(...tree);
@@ -30690,7 +30984,7 @@ async function materializedSkillManifest(target) {
30690
30984
  const visit = async (directory, prefix) => {
30691
30985
  for (const entry of await readdir2(directory)) {
30692
30986
  const path = resolve16(directory, entry);
30693
- const rel = prefix ? join8(prefix, entry) : entry;
30987
+ const rel = prefix ? join9(prefix, entry) : entry;
30694
30988
  const entryStats = await lstat2(path);
30695
30989
  if (entryStats.isSymbolicLink())
30696
30990
  return false;
@@ -30858,7 +31152,7 @@ async function walkSafeSkillTree(source, sourceRoot, visitor, rel = "") {
30858
31152
  for (const entry of await readdir2(resolvedSource)) {
30859
31153
  if (entry === "." || entry === "..")
30860
31154
  throw new Error("invalid shared skill entry");
30861
- await walkSafeSkillTree(resolve16(source, entry), sourceRoot, visitor, rel ? join8(rel, entry) : entry);
31155
+ await walkSafeSkillTree(resolve16(source, entry), sourceRoot, visitor, rel ? join9(rel, entry) : entry);
30862
31156
  }
30863
31157
  return;
30864
31158
  }
@@ -30885,7 +31179,7 @@ async function writeIsolatedHarnessFile(path, content) {
30885
31179
  if (!parentStats.isDirectory() || parentStats.isSymbolicLink()) {
30886
31180
  throw new Error(`isolated harness parent is not a real directory: ${parent}`);
30887
31181
  }
30888
- const temporary = resolve16(parent, `.${basename5(path)}.${process.pid}.${randomUUID4()}.tmp`);
31182
+ const temporary = resolve16(parent, `.${basename5(path)}.${process.pid}.${randomUUID5()}.tmp`);
30889
31183
  try {
30890
31184
  await writeFile7(temporary, content, { mode: 384, flag: "wx" });
30891
31185
  await chmod3(temporary, 384);
@@ -30944,7 +31238,7 @@ function agentCommandCatalogPublisher(input) {
30944
31238
 
30945
31239
  // apps/body/dist/attachment-delivery.js
30946
31240
  import { mkdir as mkdir7, writeFile as writeFile8 } from "node:fs/promises";
30947
- import { basename as basename6, extname, join as join9 } from "node:path";
31241
+ import { basename as basename6, extname, join as join10 } from "node:path";
30948
31242
  var MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024;
30949
31243
  var EXPIRED_REASON = "expired: these attachment bytes are past their retention window";
30950
31244
  var FETCH_TIMEOUT_MS = 3e4;
@@ -31003,7 +31297,7 @@ async function deliverAttachments(attachments, dir, fetchImpl = fetch) {
31003
31297
  return { attachment, reason: EXPIRED_REASON };
31004
31298
  if (!fetched.ok)
31005
31299
  throw new Error(`HTTP ${fetched.status}`);
31006
- const path = join9(dir, safeFileName(attachment, index, taken));
31300
+ const path = join10(dir, safeFileName(attachment, index, taken));
31007
31301
  await writeFile8(path, fetched.bytes);
31008
31302
  const bytes = fetched.bytes;
31009
31303
  const mimeType = attachment.mimeType ?? fetched.mimeType;
@@ -31611,9 +31905,9 @@ export default async function (pi) {
31611
31905
  `;
31612
31906
 
31613
31907
  // apps/body/dist/corner-branch-sync.js
31614
- import { execFile as execFile4 } from "node:child_process";
31908
+ import { execFile as execFile5 } from "node:child_process";
31615
31909
  import { promisify as promisify2 } from "node:util";
31616
- var execFileAsync2 = promisify2(execFile4);
31910
+ var execFileAsync2 = promisify2(execFile5);
31617
31911
  async function syncCornerBranch(input) {
31618
31912
  const git = input.git ?? (async (args) => (await execFileAsync2("git", ["-C", input.worktreePath, ...args], {
31619
31913
  ...input.env ? { env: input.env } : {},
@@ -32136,11 +32430,11 @@ function youtubeMcpServer(config, accessToken) {
32136
32430
  }
32137
32431
 
32138
32432
  // apps/body/dist/codegraph.js
32139
- import { execFile as execFile5 } from "node:child_process";
32433
+ import { execFile as execFile6 } from "node:child_process";
32140
32434
  import { access, appendFile, mkdir as mkdir9, readFile as readFile7 } from "node:fs/promises";
32141
32435
  import { dirname as dirname11, isAbsolute as isAbsolute3, resolve as resolve19 } from "node:path";
32142
32436
  import { promisify as promisify3 } from "node:util";
32143
- var execFileAsync3 = promisify3(execFile5);
32437
+ var execFileAsync3 = promisify3(execFile6);
32144
32438
  var CODEGRAPH_MCP_SERVER_NAME = "codegraph";
32145
32439
  var CODEGRAPH_INDEX_PATH = ".codegraph/codegraph.db";
32146
32440
  var CODEGRAPH_PREPARE_TIMEOUT_MS = 12e4;
@@ -32441,7 +32735,7 @@ async function withTurnReceiptHeartbeat(api, receipt, task, onHeartbeatError) {
32441
32735
  }
32442
32736
 
32443
32737
  // apps/body/dist/turn-trace.js
32444
- import { appendFile as appendFile2, mkdir as mkdir10, readdir as readdir4, rm as rm3 } from "node:fs/promises";
32738
+ import { appendFile as appendFile2, mkdir as mkdir10, readdir as readdir4, rm as rm4 } from "node:fs/promises";
32445
32739
  import { resolve as resolve21 } from "node:path";
32446
32740
  import { performance as performance2 } from "node:perf_hooks";
32447
32741
  var TURN_PHASES = [
@@ -32713,7 +33007,7 @@ var TurnTraceFile = class {
32713
33007
  this.prunedDay = day;
32714
33008
  const cutoff = traceFileName(new Date(now2.getTime() - TURN_TRACE_RETENTION_DAYS * 864e5));
32715
33009
  const names = await readdir4(this.directory).catch(() => []);
32716
- await Promise.all(names.filter((name) => /^turns-\d{4}-\d{2}-\d{2}\.jsonl$/.test(name) && name < cutoff).map((name) => rm3(resolve21(this.directory, name), { force: true })));
33010
+ await Promise.all(names.filter((name) => /^turns-\d{4}-\d{2}-\d{2}\.jsonl$/.test(name) && name < cutoff).map((name) => rm4(resolve21(this.directory, name), { force: true })));
32717
33011
  }
32718
33012
  };
32719
33013
 
@@ -32872,10 +33166,10 @@ process.exit(result.status ?? 1);
32872
33166
  }
32873
33167
 
32874
33168
  // apps/body/dist/warm-node-modules.js
32875
- import { createHash as createHash5, randomUUID as randomUUID5 } from "node:crypto";
33169
+ import { createHash as createHash5, randomUUID as randomUUID6 } from "node:crypto";
32876
33170
  import { constants as fsConstants } from "node:fs";
32877
- import { chmod as chmod5, copyFile as copyFile2, link, lstat as lstat3, mkdir as mkdir12, readdir as readdir5, readFile as readFile9, readlink, rename as rename4, rm as rm4, stat as stat2, symlink as symlink2, utimes } from "node:fs/promises";
32878
- import { dirname as dirname12, join as join10, resolve as resolve23 } from "node:path";
33171
+ import { chmod as chmod5, copyFile as copyFile2, link, lstat as lstat3, mkdir as mkdir12, readdir as readdir5, readFile as readFile9, readlink, rename as rename4, rm as rm5, stat as stat2, symlink as symlink2, utimes } from "node:fs/promises";
33172
+ import { dirname as dirname12, join as join11, resolve as resolve23 } from "node:path";
32879
33173
  var STORE_FORMAT = "v1";
32880
33174
  var STAGING_PREFIX = ".beeline-warm-";
32881
33175
  var STAGING_SWEEP_MS = 24 * 60 * 60 * 1e3;
@@ -32943,7 +33237,7 @@ async function seedWarmNodeModules(input) {
32943
33237
  if (storeDevice === void 0 || storeDevice !== checkoutDevice) {
32944
33238
  return { reason: "cross-device", key: plan.key };
32945
33239
  }
32946
- const staging = resolve23(input.storeRoot, `${STAGING_PREFIX}${process.pid}.${randomUUID5()}`);
33240
+ const staging = resolve23(input.storeRoot, `${STAGING_PREFIX}${process.pid}.${randomUUID6()}`);
32947
33241
  const placed = [];
32948
33242
  const now2 = (input.now ?? Date.now)();
32949
33243
  try {
@@ -32961,11 +33255,11 @@ async function seedWarmNodeModules(input) {
32961
33255
  return { reason: "seeded", key: plan.key };
32962
33256
  } catch (error) {
32963
33257
  for (const target of placed) {
32964
- await rm4(target, { recursive: true, force: true }).catch(() => void 0);
33258
+ await rm5(target, { recursive: true, force: true }).catch(() => void 0);
32965
33259
  }
32966
33260
  return { reason: "failed", key: plan.key, detail: describe3(error) };
32967
33261
  } finally {
32968
- await rm4(staging, { recursive: true, force: true }).catch(() => void 0);
33262
+ await rm5(staging, { recursive: true, force: true }).catch(() => void 0);
32969
33263
  }
32970
33264
  }
32971
33265
  async function harvestWarmNodeModules(input) {
@@ -32989,7 +33283,7 @@ async function harvestWarmNodeModules(input) {
32989
33283
  detail: `${missing.length} absent, e.g. ${missing.slice(0, 3).join(", ")}`
32990
33284
  };
32991
33285
  }
32992
- const staging = resolve23(input.storeRoot, `${STAGING_PREFIX}${process.pid}.${randomUUID5()}`);
33286
+ const staging = resolve23(input.storeRoot, `${STAGING_PREFIX}${process.pid}.${randomUUID6()}`);
32993
33287
  try {
32994
33288
  await mkdir12(input.storeRoot, { recursive: true, mode: 493 });
32995
33289
  await sweepStaleStaging(input.storeRoot, (input.now ?? Date.now)());
@@ -33008,14 +33302,14 @@ async function harvestWarmNodeModules(input) {
33008
33302
  return { reason: "already-warm", key: plan.key };
33009
33303
  return { reason: "failed", key: plan.key, detail: describe3(error) };
33010
33304
  } finally {
33011
- await rm4(staging, { recursive: true, force: true }).catch(() => void 0);
33305
+ await rm5(staging, { recursive: true, force: true }).catch(() => void 0);
33012
33306
  }
33013
33307
  }
33014
33308
  async function stagedTreeIsPublishable(worktreePath, staging, key) {
33015
33309
  const settled = await readWarmPlan(worktreePath);
33016
33310
  if (isPlanRefusal(settled) || settled.key !== key)
33017
33311
  return false;
33018
- const hidden = join10("node_modules", ".package-lock.json");
33312
+ const hidden = join11("node_modules", ".package-lock.json");
33019
33313
  const [copied, current] = await Promise.all([
33020
33314
  readFile9(resolve23(staging, hidden)).catch(() => void 0),
33021
33315
  readFile9(resolve23(worktreePath, hidden)).catch(() => void 0)
@@ -33053,7 +33347,7 @@ async function missingInstalledPackages(worktreePath, treeRoot = worktreePath) {
33053
33347
  return missing.sort();
33054
33348
  }
33055
33349
  async function isInstalledPackage(path) {
33056
- return stat2(join10(path, "package.json")).then((info) => info.isFile(), () => false);
33350
+ return stat2(join11(path, "package.json")).then((info) => info.isFile(), () => false);
33057
33351
  }
33058
33352
  var INTEGRITY_READ_CONCURRENCY = 64;
33059
33353
  async function mapWithLimit(values, limit, visit) {
@@ -33087,8 +33381,8 @@ function isContainedTreePath(value) {
33087
33381
  async function cloneTree(source, target, file, topLevel = true) {
33088
33382
  await mkdir12(target, { recursive: true, mode: 493 });
33089
33383
  for (const entry of await readdir5(source, { withFileTypes: true })) {
33090
- const from = join10(source, entry.name);
33091
- const to = join10(target, entry.name);
33384
+ const from = join11(source, entry.name);
33385
+ const to = join11(target, entry.name);
33092
33386
  if (entry.isSymbolicLink()) {
33093
33387
  await symlink2(await readlink(from), to);
33094
33388
  continue;
@@ -33118,13 +33412,13 @@ async function pruneWarmStore(storeRoot, keep) {
33118
33412
  const names = (await readdir5(storeRoot).catch(() => [])).filter((name) => !name.startsWith(STAGING_PREFIX));
33119
33413
  const entries = [];
33120
33414
  for (const name of names) {
33121
- const info = await lstat3(join10(storeRoot, name)).catch(() => void 0);
33415
+ const info = await lstat3(join11(storeRoot, name)).catch(() => void 0);
33122
33416
  if (info?.isDirectory())
33123
33417
  entries.push({ name, usedAt: info.mtimeMs });
33124
33418
  }
33125
33419
  const dropped = entries.sort((a2, b) => b.usedAt - a2.usedAt || a2.name.localeCompare(b.name)).slice(keep);
33126
33420
  for (const entry of dropped) {
33127
- await rm4(join10(storeRoot, entry.name), { recursive: true, force: true }).catch(() => void 0);
33421
+ await rm5(join11(storeRoot, entry.name), { recursive: true, force: true }).catch(() => void 0);
33128
33422
  }
33129
33423
  return dropped.map((entry) => entry.name);
33130
33424
  }
@@ -33133,11 +33427,11 @@ async function sweepStaleStaging(storeRoot, now2) {
33133
33427
  for (const name of entries) {
33134
33428
  if (!name.startsWith(STAGING_PREFIX))
33135
33429
  continue;
33136
- const path = join10(storeRoot, name);
33430
+ const path = join11(storeRoot, name);
33137
33431
  const info = await lstat3(path).catch(() => void 0);
33138
33432
  if (!info || now2 - info.mtimeMs < STAGING_SWEEP_MS)
33139
33433
  continue;
33140
- await rm4(path, { recursive: true, force: true }).catch(() => void 0);
33434
+ await rm5(path, { recursive: true, force: true }).catch(() => void 0);
33141
33435
  }
33142
33436
  }
33143
33437
  async function pathExists(path) {
@@ -33156,7 +33450,7 @@ function describe3(error) {
33156
33450
  // apps/body/dist/monolith-room-turn.js
33157
33451
  import { mkdir as mkdir13 } from "node:fs/promises";
33158
33452
  import { homedir as homedir9 } from "node:os";
33159
- import { join as join11 } from "node:path";
33453
+ import { join as join12 } from "node:path";
33160
33454
 
33161
33455
  // packages/api-contract/dist/scheduled-prompts.js
33162
33456
  var SCHEDULE_SCHEDULER_NAME = "Beeline Scheduler";
@@ -33200,6 +33494,14 @@ function resumePrompt(item) {
33200
33494
  'Acknowledge in one short line (for example "Adding Trusty Squire now.") and continue the work that needed it, or say plainly what still has to happen before you can.'
33201
33495
  ].join(" ");
33202
33496
  }
33497
+ const grant = parseGrantDecisionLine(item.body);
33498
+ if (grant?.kind === "mcp" && grant.decision !== "deny") {
33499
+ return [
33500
+ "This is the answer to your grant request; your paused work resumes now.",
33501
+ `The approved ${grant.target} route is mounted in this session.`,
33502
+ "Continue the paused work with that tool now; do not restart, schedule another turn, or request the route again."
33503
+ ].join(" ");
33504
+ }
33203
33505
  return [
33204
33506
  "This is the answer to your grant request; your paused work resumes now.",
33205
33507
  "If it was approved and it is a command grant, run it with run_granted_command and the exact argv.",
@@ -33375,7 +33677,7 @@ var MonolithRoomTurnLoop = class {
33375
33677
  const cached = this.deliveredAttachments.get(item.id);
33376
33678
  if (cached)
33377
33679
  return cached;
33378
- const delivered = await deliverAttachments(item.attachments, join11(this.attachmentDir, item.id.replace(/[^\w-]/g, "_")), this.options.fetchImpl);
33680
+ const delivered = await deliverAttachments(item.attachments, join12(this.attachmentDir, item.id.replace(/[^\w-]/g, "_")), this.options.fetchImpl);
33379
33681
  this.deliveredAttachments.set(item.id, withoutImageData(delivered));
33380
33682
  return delivered;
33381
33683
  }
@@ -33432,9 +33734,10 @@ var MonolithRoomTurnLoop = class {
33432
33734
  }
33433
33735
  async grantedHostRoutes() {
33434
33736
  try {
33435
- return grantedHostRoutesFromList(await this.options.api.execute("listAgentGrants", {
33436
- agentId: this.agent.publicKey
33437
- }));
33737
+ return claimGrantedHostRoutes(await this.options.api.execute("listAgentGrants", {
33738
+ agentId: this.agent.publicKey,
33739
+ roomId: this.options.roomId
33740
+ }), (grantId) => this.options.api.execute("consumeAgentGrant", { grantId }));
33438
33741
  } catch {
33439
33742
  return [];
33440
33743
  }
@@ -33495,7 +33798,7 @@ var MonolithRoomTurnLoop = class {
33495
33798
  args: this.options.config.agentArgs ?? []
33496
33799
  }, selection);
33497
33800
  const { stateDirs, tmpDir } = harnessStateDirsFromEnv(agentEnv);
33498
- this.attachmentDir = tmpDir ? join11(tmpDir, "beeline-attachments") : void 0;
33801
+ this.attachmentDir = tmpDir ? join12(tmpDir, "beeline-attachments") : void 0;
33499
33802
  this.sessionScratchDir = tmpDir;
33500
33803
  this.sessionStateDirs = stateDirs;
33501
33804
  const homeStateDirs = harnessHomeStateDirs(harnessLabel, agentEnv.HOME ?? operatorHome);
@@ -33984,6 +34287,8 @@ ${JSON.stringify((corners.corners ?? []).filter((corner) => !corner.archived))}`
33984
34287
  onPoll: () => this.options.health.poll(),
33985
34288
  onError: (error) => console.error("[thin-core] Room command failed", error),
33986
34289
  stop: (requestId) => this.stopTurn(requestId),
34290
+ restart: () => this.options.onRestartRequested?.(),
34291
+ canStartTurn: this.options.canStartTurn,
33987
34292
  run: async (command) => {
33988
34293
  const item = {
33989
34294
  ...command.source,
@@ -34017,7 +34322,7 @@ function roomMessagePrompt(author, body, attachments, delivered, harnessAcceptsI
34017
34322
  }
34018
34323
 
34019
34324
  // apps/body/dist/monolith-corner-turn.js
34020
- var execFileAsync4 = promisify4(execFile6);
34325
+ var execFileAsync4 = promisify4(execFile7);
34021
34326
  var TOOL_ARGUMENT_MAX_BYTES = 1200;
34022
34327
  var TOOL_OUTPUT_MAX_BYTES = 3200;
34023
34328
  var TOOL_PATH_LIMIT = 12;
@@ -34404,9 +34709,10 @@ var MonolithCornerTurnLoop = class {
34404
34709
  }
34405
34710
  async grantedHostRoutes() {
34406
34711
  try {
34407
- return grantedHostRoutesFromList(await this.options.api.execute("listAgentGrants", {
34408
- agentId: this.agent.publicKey
34409
- }));
34712
+ return claimGrantedHostRoutes(await this.options.api.execute("listAgentGrants", {
34713
+ agentId: this.agent.publicKey,
34714
+ roomId: this.options.cornerId
34715
+ }), (grantId) => this.options.api.execute("consumeAgentGrant", { grantId }));
34410
34716
  } catch {
34411
34717
  return [];
34412
34718
  }
@@ -34493,7 +34799,7 @@ var MonolithCornerTurnLoop = class {
34493
34799
  args: this.options.config.agentArgs ?? []
34494
34800
  }, selection);
34495
34801
  const { stateDirs, tmpDir } = harnessStateDirsFromEnv(agentEnv);
34496
- this.attachmentDir = tmpDir ? join12(tmpDir, "beeline-attachments") : void 0;
34802
+ this.attachmentDir = tmpDir ? join13(tmpDir, "beeline-attachments") : void 0;
34497
34803
  this.sessionScratchDir = tmpDir;
34498
34804
  const homeStateDirs = harnessHomeStateDirs(harnessLabel, agentEnv.HOME ?? operatorHome);
34499
34805
  await Promise.all(homeStateDirs.map((dir) => mkdir14(dir, { recursive: true })));
@@ -34798,7 +35104,7 @@ var MonolithCornerTurnLoop = class {
34798
35104
  const [conversation, roster, delivered, activeReviewerInstruction] = await trace.measure("context-fetch", () => Promise.all([
34799
35105
  api.execute("getRoomConversation", { roomId: cornerId, limit: 200 }),
34800
35106
  this.roster(),
34801
- this.attachmentDir && attachments.length ? deliverAttachments(attachments, join12(this.attachmentDir, requestId.replace(/[^\w-]/g, "_")), this.options.fetchImpl) : Promise.resolve([]),
35107
+ this.attachmentDir && attachments.length ? deliverAttachments(attachments, join13(this.attachmentDir, requestId.replace(/[^\w-]/g, "_")), this.options.fetchImpl) : Promise.resolve([]),
34802
35108
  this.activeReviewerInstruction()
34803
35109
  ]));
34804
35110
  const names = new Map(roster.members.map((member) => [member.identityId, member.name]));
@@ -35105,6 +35411,8 @@ ${trigger}`,
35105
35411
  onPoll: () => this.options.onPoll(),
35106
35412
  onError: (error) => console.error("[thin-core] corner command failed", error),
35107
35413
  stop: (requestId) => this.stopTurn(requestId),
35414
+ restart: () => this.options.onRestartRequested?.(),
35415
+ canStartTurn: this.options.canStartTurn,
35108
35416
  closed: async () => {
35109
35417
  if (this.closePushed) {
35110
35418
  this.closePushed = false;
@@ -35585,6 +35893,10 @@ var DiscoveryWakes = class {
35585
35893
  function shouldPostInitialCornerWorkingState(restore, isOpener = true) {
35586
35894
  return isOpener && !restore.featureBranch && !restore.lifecycle?.branch && !restore.lifecycle?.pr;
35587
35895
  }
35896
+ function cornerBranchIsSafeToDelete(lifecycle) {
35897
+ const pr = lifecycle?.pr;
35898
+ return !pr || Boolean(pr.mergedAt);
35899
+ }
35588
35900
  function cornerStartConfigKey(repository, objective) {
35589
35901
  return JSON.stringify({
35590
35902
  resolution: repository.resolution,
@@ -35625,7 +35937,7 @@ async function materializeCornerWorktree(input) {
35625
35937
  `+refs/heads/${input.featureBranch}:refs/remotes/origin/${input.featureBranch}`
35626
35938
  ], { env: authEnv, maxBuffer: 4 * 1024 * 1024 }).then(() => true, () => false);
35627
35939
  if (!existsSync6(resolve24(path, ".git"))) {
35628
- await rm5(path, { recursive: true, force: true });
35940
+ await rm6(path, { recursive: true, force: true });
35629
35941
  await execFileAsync5("git", [
35630
35942
  `--git-dir=${gitCommonDir}`,
35631
35943
  "worktree",
@@ -35693,7 +36005,8 @@ async function mapWithConcurrency(values, limit, visit) {
35693
36005
  }
35694
36006
  }));
35695
36007
  }
35696
- async function removeCornerWorktreeAndBranches(worktree) {
36008
+ async function removeCornerWorktreeAndBranches(worktree, options = {}) {
36009
+ const preserveRemoteBranch = options.preserveRemoteBranch === true;
35697
36010
  const localRef = `refs/heads/${worktree.branch}`;
35698
36011
  const remoteRef = `refs/heads/${worktree.branch}`;
35699
36012
  const worktreeExists = existsSync6(worktree.path);
@@ -35717,6 +36030,8 @@ async function removeCornerWorktreeAndBranches(worktree) {
35717
36030
  worktree.path
35718
36031
  ]);
35719
36032
  } else if (!localExists) {
36033
+ if (preserveRemoteBranch)
36034
+ return;
35720
36035
  await deleteExactRemoteBranch(worktree.gitCommonDir, remoteRef, worktree.token, {
35721
36036
  requireAbsent: true
35722
36037
  });
@@ -35727,7 +36042,9 @@ async function removeCornerWorktreeAndBranches(worktree) {
35727
36042
  throw new Error(`refusing to delete repository HEAD ${localRef}`);
35728
36043
  }
35729
36044
  }
35730
- await deleteExactRemoteBranch(worktree.gitCommonDir, remoteRef, worktree.token);
36045
+ if (!preserveRemoteBranch) {
36046
+ await deleteExactRemoteBranch(worktree.gitCommonDir, remoteRef, worktree.token);
36047
+ }
35731
36048
  if (await gitRefExists(worktree.gitCommonDir, localRef)) {
35732
36049
  await execFileAsync5("git", [
35733
36050
  `--git-dir=${worktree.gitCommonDir}`,
@@ -35739,13 +36056,13 @@ async function removeCornerWorktreeAndBranches(worktree) {
35739
36056
  ]);
35740
36057
  }
35741
36058
  }
35742
- var execFileAsync5 = promisify5(execFile7);
36059
+ var execFileAsync5 = promisify5(execFile8);
35743
36060
  async function removeCornerScratchWorkspace(input) {
35744
36061
  const expected = resolve24(input.roomRoot, "scratch");
35745
36062
  if (resolve24(input.scratchPath) !== expected) {
35746
36063
  throw new Error(`refusing to remove scratch outside corner ${input.cornerId}`);
35747
36064
  }
35748
- await rm5(expected, { recursive: true, force: true });
36065
+ await rm6(expected, { recursive: true, force: true });
35749
36066
  }
35750
36067
  var RoomRuntimeCoordinator = class {
35751
36068
  configPath;
@@ -35785,6 +36102,7 @@ var RoomRuntimeCoordinator = class {
35785
36102
  workspaceRemovalConfirmations = 0;
35786
36103
  roomRemovalConfirmations = /* @__PURE__ */ new Map();
35787
36104
  confirmationPending = false;
36105
+ restartRequested = false;
35788
36106
  /** Unscoped agent-directed discovery wakes (#1369), counted instead of
35789
36107
  * flagged: a wake that arrives while a reconcile is already running must
35790
36108
  * survive its start-pass clearing, or a reconnect mid-reconcile waits a
@@ -35877,6 +36195,12 @@ var RoomRuntimeCoordinator = class {
35877
36195
  count += 1;
35878
36196
  return count;
35879
36197
  }
36198
+ requestLifecycleRestart() {
36199
+ if (this.restartRequested)
36200
+ return;
36201
+ this.restartRequested = true;
36202
+ this.options.onRestartRequested?.();
36203
+ }
35880
36204
  quiesceForUpdateIfIdle() {
35881
36205
  if (!this.isWorkspaceIdle())
35882
36206
  return false;
@@ -36136,7 +36460,9 @@ var RoomRuntimeCoordinator = class {
36136
36460
  },
36137
36461
  onCornerOpened: () => {
36138
36462
  this.confirmationPending = true;
36139
- }
36463
+ },
36464
+ onRestartRequested: () => this.requestLifecycleRestart(),
36465
+ canStartTurn: () => !this.restartRequested
36140
36466
  });
36141
36467
  const promise = loop.run().catch((error) => {
36142
36468
  if (!controller.signal.aborted)
@@ -36288,7 +36614,9 @@ var RoomRuntimeCoordinator = class {
36288
36614
  branch: featureBranch,
36289
36615
  parentRoomId: corner.parentRoomId,
36290
36616
  token: ""
36291
- }) : this.reapCornerScratch({ path: workspacePath, cornerId: corner.cornerId })
36617
+ }) : this.reapCornerScratch({ path: workspacePath, cornerId: corner.cornerId }),
36618
+ onRestartRequested: () => this.requestLifecycleRestart(),
36619
+ canStartTurn: () => !this.restartRequested
36292
36620
  });
36293
36621
  const promise = loop.run().catch((error) => {
36294
36622
  if (!controller.signal.aborted) {
@@ -36394,25 +36722,44 @@ var RoomRuntimeCoordinator = class {
36394
36722
  }
36395
36723
  async reapCornerWorktree(worktree) {
36396
36724
  try {
36725
+ const preserveRemoteBranch = !await this.cornerBranchMayBeDeleted(worktree.cornerId);
36397
36726
  const parentRoomId = worktree.parentRoomId;
36398
- if (!parentRoomId) {
36727
+ if (!parentRoomId && !preserveRemoteBranch) {
36399
36728
  throw new Error(`corner ${worktree.cornerId} has no parent Room for GitHub token`);
36400
36729
  }
36401
- const token = (await this.options.daemonApi.execute("getRoomGitHubToken", { roomId: parentRoomId })).token;
36402
- await removeCornerWorktreeAndBranches({ ...worktree, token });
36403
- await this.options.daemonApi.execute("postCornerRemoteState", {
36404
- cornerId: worktree.cornerId,
36405
- branch: worktree.branch,
36406
- state: "gone",
36407
- checks: "unknown"
36408
- });
36730
+ const token = preserveRemoteBranch ? "" : (await this.options.daemonApi.execute("getRoomGitHubToken", {
36731
+ roomId: parentRoomId
36732
+ })).token;
36733
+ await removeCornerWorktreeAndBranches({ ...worktree, token }, { preserveRemoteBranch });
36409
36734
  this.pendingCornerReaps.delete(worktree.cornerId);
36735
+ if (!preserveRemoteBranch) {
36736
+ await this.options.daemonApi.execute("postCornerRemoteState", {
36737
+ cornerId: worktree.cornerId,
36738
+ branch: worktree.branch,
36739
+ state: "gone",
36740
+ checks: "unknown"
36741
+ });
36742
+ }
36410
36743
  } catch (error) {
36411
36744
  this.pendingCornerReaps.set(worktree.cornerId, { ...worktree, token: "" });
36412
36745
  this.confirmationPending = true;
36413
36746
  throw error;
36414
36747
  }
36415
36748
  }
36749
+ /**
36750
+ * Whether a corner's feature branch is safe to delete on teardown. A read
36751
+ * that fails — an agent removed from the corner, or a corner a helper can no
36752
+ * longer reach — is not proof its pull request is gone, so the branch is
36753
+ * kept.
36754
+ */
36755
+ async cornerBranchMayBeDeleted(cornerId) {
36756
+ try {
36757
+ const state = await this.options.daemonApi.execute("getCornerRestoreState", { cornerId });
36758
+ return cornerBranchIsSafeToDelete(state.lifecycle);
36759
+ } catch {
36760
+ return false;
36761
+ }
36762
+ }
36416
36763
  async reapCornerScratch(scratch) {
36417
36764
  await removeCornerScratchWorkspace({
36418
36765
  cornerId: scratch.cornerId,
@@ -36651,13 +36998,13 @@ import { mkdir as mkdir17, rename as rename5 } from "node:fs/promises";
36651
36998
  import { dirname as dirname15, resolve as resolve26 } from "node:path";
36652
36999
 
36653
37000
  // apps/body/dist/systemd.js
36654
- import { execFile as execFile8 } from "node:child_process";
37001
+ import { execFile as execFile9 } from "node:child_process";
36655
37002
  import { mkdir as mkdir16, readFile as readFile10, stat as stat3, writeFile as writeFile10 } from "node:fs/promises";
36656
37003
  import { homedir as homedir11 } from "node:os";
36657
37004
  import { dirname as dirname14, resolve as resolve25 } from "node:path";
36658
37005
  import { setTimeout as sleep } from "node:timers/promises";
36659
37006
  import { promisify as promisify6 } from "node:util";
36660
- var execFileAsync6 = promisify6(execFile8);
37007
+ var execFileAsync6 = promisify6(execFile9);
36661
37008
  var DELIBERATE_REMOVAL_EXIT_STATUS = 78;
36662
37009
  var DAEMON_DISTRESS_EXIT_STATUS = 77;
36663
37010
  var UNKNOWN_AGENT_EXIT_STATUS = 79;
@@ -36926,8 +37273,8 @@ init_self_update_manifest();
36926
37273
 
36927
37274
  // apps/body/dist/managed-update.js
36928
37275
  init_self_update();
36929
- import { spawn as spawn9 } from "node:child_process";
36930
- import { mkdir as mkdir20, rm as rm7, stat as stat4, writeFile as writeFile13 } from "node:fs/promises";
37276
+ import { spawn as spawn10 } from "node:child_process";
37277
+ import { mkdir as mkdir20, rm as rm8, stat as stat4, writeFile as writeFile13 } from "node:fs/promises";
36931
37278
  import { dirname as dirname18, resolve as resolve29 } from "node:path";
36932
37279
 
36933
37280
  // apps/body/dist/update-rollback-alert.js
@@ -37020,7 +37367,7 @@ ${now2()}
37020
37367
  throw error;
37021
37368
  const age = now2() - await stat4(lock).then((value) => value.mtimeMs).catch(() => now2());
37022
37369
  if (age > LOCK_STALE_MS) {
37023
- await rm7(lock, { recursive: true, force: true });
37370
+ await rm8(lock, { recursive: true, force: true });
37024
37371
  continue;
37025
37372
  }
37026
37373
  if (now2() >= deadline)
@@ -37031,7 +37378,7 @@ ${now2()}
37031
37378
  try {
37032
37379
  return await work();
37033
37380
  } finally {
37034
- await rm7(lock, { recursive: true, force: true });
37381
+ await rm8(lock, { recursive: true, force: true });
37035
37382
  }
37036
37383
  }
37037
37384
  var ManagedUpdateHandoff = class _ManagedUpdateHandoff {
@@ -37341,7 +37688,7 @@ async function runManagedUpdateWorkerProcess() {
37341
37688
  if (!entrypoint)
37342
37689
  throw new Error("cannot resolve the current Beeline entrypoint");
37343
37690
  await new Promise((resolveWorker, rejectWorker) => {
37344
- const child = spawn9(process.execPath, [entrypoint, "managed-update-worker"], {
37691
+ const child = spawn10(process.execPath, [entrypoint, "managed-update-worker"], {
37345
37692
  detached: true,
37346
37693
  env: { ...process.env, BEELINE_INTERNAL_UPDATE_WORKER: "1" },
37347
37694
  stdio: ["ignore", "pipe", "pipe"]
@@ -37500,6 +37847,32 @@ async function rollbackFailedSuccessor(layout, runtimeDir, cause = {}) {
37500
37847
  }
37501
37848
 
37502
37849
  // apps/body/dist/self-update-cli.js
37850
+ async function refreshRuntimeHarnessAdapters(options = {}) {
37851
+ const log2 = options.log ?? console.log;
37852
+ const readRuntime = options.readRuntime ?? readRuntimeRecord;
37853
+ const install = options.install ?? ((command) => runAdapterInstall(command));
37854
+ const configPaths = options.configPaths ?? [
37855
+ ...await findRuntimeConfigPaths(process.cwd()).catch(() => []),
37856
+ ...await findAgentRuntimeConfigPaths(process.env, process.cwd()).catch(() => [])
37857
+ ];
37858
+ const kinds = /* @__PURE__ */ new Set();
37859
+ for (const path of new Set(configPaths)) {
37860
+ const runtime = await readRuntime(path).catch(() => void 0);
37861
+ if (runtime?.agentKind)
37862
+ kinds.add(runtime.agentKind);
37863
+ }
37864
+ for (const kind of kinds) {
37865
+ const command = latestAdapterInstallCommand(kind);
37866
+ if (!command)
37867
+ continue;
37868
+ log2(`[beeline] refreshing ${kind} adapter: ${formatAdapterInstallCommand(command)}`);
37869
+ try {
37870
+ await install(command);
37871
+ } catch (error) {
37872
+ log2(`[beeline] ${kind} adapter refresh failed; keeping the installed copy (${error instanceof Error ? error.message : String(error)})`);
37873
+ }
37874
+ }
37875
+ }
37503
37876
  function updateUsage() {
37504
37877
  console.log(`
37505
37878
  ${import_picocolors.default.bold("Update the installed Beeline bundle from the published manifest.")}
@@ -37601,6 +37974,8 @@ async function runUpdateCommand(args) {
37601
37974
  await printStatus(layout);
37602
37975
  return;
37603
37976
  }
37977
+ if (!checkOnly)
37978
+ await refreshRuntimeHarnessAdapters();
37604
37979
  const installed = await readInstalledBundleIdentity(layout);
37605
37980
  console.log(`[beeline] installed bundle: ${describeIdentity(installed)}`);
37606
37981
  const raw = await fetch(manifestUrl, { signal: AbortSignal.timeout(3e4) }).then((response) => {
@@ -37767,7 +38142,7 @@ async function runStartCommand(args, interactiveUi, dependencyOverrides = {}) {
37767
38142
 
37768
38143
  // apps/body/dist/connect-command.js
37769
38144
  import { spawn as spawn11 } from "node:child_process";
37770
- import { createHash as createHash9, randomUUID as randomUUID6 } from "node:crypto";
38145
+ import { createHash as createHash9, randomUUID as randomUUID7 } from "node:crypto";
37771
38146
  import { chmod as chmod7, mkdir as mkdir21, readFile as readFile13, unlink as unlink5, writeFile as writeFile14 } from "node:fs/promises";
37772
38147
  import { homedir as homedir13, hostname as hostname2 } from "node:os";
37773
38148
  import { dirname as dirname20, resolve as resolve30 } from "node:path";
@@ -37854,14 +38229,13 @@ async function verifyProviderKey(input) {
37854
38229
  }
37855
38230
 
37856
38231
  // apps/body/dist/pair-agent-selection.js
37857
- import { spawn as spawn10 } from "node:child_process";
37858
38232
  import { stdin as stdin2, stdout as stdout3 } from "node:process";
37859
38233
  var NO_AGENT_MESSAGE = `No supported ACP-capable coding agent was detected.
37860
38234
  Install one of these supported agents:
37861
- codex npm install -g @openai/codex @agentclientprotocol/codex-acp
37862
- claude Install Claude Code and npm install -g @agentclientprotocol/claude-agent-acp
38235
+ codex npm install -g @openai/codex@latest @agentclientprotocol/codex-acp@latest
38236
+ claude Install Claude Code and npm install -g @agentclientprotocol/claude-agent-acp@latest
37863
38237
  goose https://block.github.io/goose/docs/getting-started/installation/
37864
- pi npm install -g @mariozechner/pi-coding-agent pi-acp
38238
+ pi npm install -g @mariozechner/pi-coding-agent@latest pi-acp@latest
37865
38239
  grok curl -fsSL https://x.ai/cli/install.sh | bash
37866
38240
  cursor Install Cursor Agent CLI from https://cursor.com/docs/cli
37867
38241
  Then retry, or explicitly use \`--agent reference\` with an LLM key.
@@ -37882,21 +38256,7 @@ async function clackSelectAgent(candidates) {
37882
38256
  return unwrapPrompt(picked, "Pairing cancelled.");
37883
38257
  }
37884
38258
  async function installAdapter(install, opts) {
37885
- await new Promise((resolveInstall, rejectInstall) => {
37886
- const child = spawn10(install.command, install.args, {
37887
- cwd: opts.cwd,
37888
- env: opts.env ?? process.env,
37889
- stdio: "inherit"
37890
- });
37891
- child.once("error", rejectInstall);
37892
- child.once("exit", (code, signal) => {
37893
- if (code === 0)
37894
- resolveInstall();
37895
- else {
37896
- rejectInstall(new Error(signal ? `${install.command} was terminated by ${signal}` : `${install.command} exited with status ${code ?? "unknown"}`));
37897
- }
37898
- });
37899
- });
38259
+ await runAdapterInstall(install, opts);
37900
38260
  }
37901
38261
  function errorMessage(error) {
37902
38262
  return error instanceof Error ? error.message : String(error);
@@ -37910,14 +38270,38 @@ async function selectPairAgentCommand(opts) {
37910
38270
  const selectAgent = opts.selectAgent ?? clackSelectAgent;
37911
38271
  const confirmInstall = opts.confirmInstall ?? clackConfirmInstall;
37912
38272
  const runInstall = opts.install ?? installAdapter;
38273
+ const useReadyAgent = async (agent, label) => {
38274
+ const refresh = latestAdapterInstallCommand(agent.kind);
38275
+ if (!refresh) {
38276
+ output.write(`[beeline] using ${agent.kind} (${label})
38277
+ `);
38278
+ return agent;
38279
+ }
38280
+ output.write(`[beeline] refreshing ${agent.kind} adapter: ${formatAdapterInstallCommand(refresh)}
38281
+ `);
38282
+ try {
38283
+ await runInstall(refresh, { cwd: opts.cwd, env: opts.env });
38284
+ const refreshed = resolveAgentCommand({
38285
+ kind: agent.kind,
38286
+ env: opts.env,
38287
+ cwd: opts.cwd
38288
+ });
38289
+ output.write(`[beeline] using ${refreshed.kind} (${label}; adapter current)
38290
+ `);
38291
+ return refreshed;
38292
+ } catch (installError) {
38293
+ throw new Error(`Could not refresh the ${agent.kind} adapter: ${errorMessage(installError)}. Install it with: ${formatAdapterInstallCommand(refresh)}`);
38294
+ }
38295
+ };
37913
38296
  if (opts.explicitKind !== void 0) {
37914
38297
  try {
37915
- return resolveAgentCommand({
38298
+ const selected = resolveAgentCommand({
37916
38299
  kind: opts.explicitKind,
37917
38300
  customCommand: opts.customCommand,
37918
38301
  env: opts.env,
37919
38302
  cwd: opts.cwd
37920
38303
  });
38304
+ return await useReadyAgent(selected, "selected");
37921
38305
  } catch (resolutionError) {
37922
38306
  const candidate = detectInstalledAgentCommands({ env: opts.env, cwd: opts.cwd }).find((detected2) => detected2.kind === opts.explicitKind && detected2.status === "missing-adapter");
37923
38307
  if (!candidate || candidate.status !== "missing-adapter")
@@ -37963,25 +38347,19 @@ async function selectPairAgentCommand(opts) {
37963
38347
  throw new Error("Installed coding agents need ACP adapter setup; install an adapter above, then retry.");
37964
38348
  }
37965
38349
  if (ready.length === 1) {
37966
- output.write(`[beeline] using ${ready[0].kind} (auto-detected)
37967
- `);
37968
- return ready[0].agent;
38350
+ return useReadyAgent(ready[0].agent, "auto-detected");
37969
38351
  }
37970
38352
  const readyKinds = ready.map((candidate) => candidate.kind).join(", ");
37971
38353
  throw new Error(`Detected multiple ACP-capable coding agents: ${readyKinds}. This session is non-interactive; pass \`--agent <name>\` explicitly.`);
37972
38354
  }
37973
38355
  if (detected.length === 1 && detected[0].status === "ready") {
37974
- output.write(`[beeline] using ${detected[0].kind} (auto-detected)
37975
- `);
37976
- return detected[0].agent;
38356
+ return useReadyAgent(detected[0].agent, "auto-detected");
37977
38357
  }
37978
38358
  while (true) {
37979
38359
  const pickedKind = await selectAgent(detected);
37980
38360
  const selected = detected.find((candidate) => candidate.kind === pickedKind);
37981
38361
  if (selected.status === "ready") {
37982
- output.write(`[beeline] using ${selected.kind} (selected)
37983
- `);
37984
- return selected.agent;
38362
+ return useReadyAgent(selected.agent, "selected");
37985
38363
  }
37986
38364
  const manual = missingAdapterMessage(selected);
37987
38365
  output.write(`[beeline] installing ${selected.kind} adapter: ${formatAdapterInstallCommand(selected.install)}
@@ -38200,10 +38578,23 @@ function connectModelPickerFromAxes(axes, fallbackModel, harness, rawAxes = axes
38200
38578
  note: `${harness} did not enumerate models; offering the provider default`
38201
38579
  };
38202
38580
  }
38203
- async function loadConnectModelCatalog(input) {
38204
- const agent = resolveAgentCommand({ kind: input.harness });
38581
+ var refreshedConnectAdapters = /* @__PURE__ */ new Map();
38582
+ function refreshConnectAdapter(kind) {
38583
+ const existing = refreshedConnectAdapters.get(kind);
38584
+ if (existing)
38585
+ return existing;
38586
+ const refresh = installLatestAgentAdapter(kind).catch((error) => {
38587
+ refreshedConnectAdapters.delete(kind);
38588
+ throw error;
38589
+ });
38590
+ refreshedConnectAdapters.set(kind, refresh);
38591
+ return refresh;
38592
+ }
38593
+ async function loadConnectModelCatalog(input, dependencies = {}) {
38594
+ await (dependencies.refreshAdapter ?? refreshConnectAdapter)(input.harness);
38595
+ const agent = (dependencies.resolveAgent ?? ((kind) => resolveAgentCommand({ kind })))(input.harness);
38205
38596
  const model = input.model ?? defaultConnectModel(input.harness, input.provider);
38206
- const catalog = await fetchAgentModelCatalog(agent, providerEnvironment({
38597
+ const catalog = await (dependencies.fetchCatalog ?? fetchAgentModelCatalog)(agent, providerEnvironment({
38207
38598
  harness: input.harness,
38208
38599
  ...input.provider ? { provider: input.provider } : {},
38209
38600
  ...input.apiKey ? { apiKey: input.apiKey } : {},
@@ -38409,7 +38800,7 @@ async function readMachineId(env = process.env) {
38409
38800
  if (!/^[0-9a-f-]{32,}$/.test(machineId))
38410
38801
  throw new Error("invalid persisted machine id");
38411
38802
  } catch {
38412
- machineId = randomUUID6();
38803
+ machineId = randomUUID7();
38413
38804
  try {
38414
38805
  await mkdir21(configDir, { recursive: true, mode: 448 });
38415
38806
  await writeFile14(machineIdPath, `${machineId}
@@ -38689,7 +39080,7 @@ async function runConnectFinishCommand(path) {
38689
39080
  init_self_update();
38690
39081
 
38691
39082
  // apps/body/dist/daemon-failure.js
38692
- import { mkdir as mkdir22, readFile as readFile14, rename as rename8, rm as rm8, writeFile as writeFile15 } from "node:fs/promises";
39083
+ import { mkdir as mkdir22, readFile as readFile14, rename as rename8, rm as rm9, writeFile as writeFile15 } from "node:fs/promises";
38693
39084
  import { dirname as dirname21, resolve as resolve31 } from "node:path";
38694
39085
  var DAEMON_FAILURE_LIMIT = 3;
38695
39086
  var DAEMON_FAILURE_WINDOW_MS = 5 * 6e4;
@@ -38729,11 +39120,11 @@ async function recordDaemonStartFailure(runtimeDir, error, now2 = Date.now()) {
38729
39120
  return { distressed, count: failures.length, path: daemonFailurePath(runtimeDir) };
38730
39121
  }
38731
39122
  async function clearDaemonStartFailures(runtimeDir) {
38732
- await rm8(daemonFailurePath(runtimeDir), { force: true });
39123
+ await rm9(daemonFailurePath(runtimeDir), { force: true });
38733
39124
  }
38734
39125
 
38735
39126
  // apps/body/dist/update-functional-probe.js
38736
- import { mkdir as mkdir23, rm as rm9 } from "node:fs/promises";
39127
+ import { mkdir as mkdir23, rm as rm10 } from "node:fs/promises";
38737
39128
  import { homedir as homedir14 } from "node:os";
38738
39129
  import { resolve as resolve32 } from "node:path";
38739
39130
  var UPDATE_PROBE_SESSION_TIMEOUT_MS = 1e4;
@@ -38858,7 +39249,7 @@ async function runUpdateFunctionalProbe(input) {
38858
39249
  const root = input.probeRoot ?? resolve32(input.runtimeDir, "update-functional-probe");
38859
39250
  const cwd = resolve32(root, "checkout");
38860
39251
  const homeRoot = resolve32(root, "agent-home");
38861
- await rm9(root, { recursive: true, force: true });
39252
+ await rm10(root, { recursive: true, force: true });
38862
39253
  await mkdir23(cwd, { recursive: true, mode: 448 });
38863
39254
  let client;
38864
39255
  try {
@@ -39033,13 +39424,13 @@ async function runUpdateFunctionalProbe(input) {
39033
39424
  };
39034
39425
  } finally {
39035
39426
  await client?.stop().catch(() => void 0);
39036
- await rm9(root, { recursive: true, force: true }).catch(() => void 0);
39427
+ await rm10(root, { recursive: true, force: true }).catch(() => void 0);
39037
39428
  }
39038
39429
  }
39039
39430
 
39040
39431
  // apps/body/dist/current-release-probe.js
39041
39432
  import { spawn as spawn12 } from "node:child_process";
39042
- import { dirname as dirname22, join as join14 } from "node:path";
39433
+ import { dirname as dirname22, join as join15 } from "node:path";
39043
39434
  init_self_update();
39044
39435
  var CURRENT_RELEASE_PROBE_TIMEOUT_MS = 12e4;
39045
39436
  var UPDATE_PROBE_COMMAND = "update-probe";
@@ -39082,7 +39473,7 @@ function outcomeFromReport(report) {
39082
39473
  }
39083
39474
  }
39084
39475
  async function probeReleaseInSubprocess(input) {
39085
- const bundleDir = join14(input.layout.releasesRoot, input.releaseId);
39476
+ const bundleDir = join15(input.layout.releasesRoot, input.releaseId);
39086
39477
  const entrypoint = await resolveBundleEntrypoint(bundleDir);
39087
39478
  if (!entrypoint) {
39088
39479
  return { kind: "unavailable", reason: `release ${input.releaseId} has no runnable CLI entrypoint` };
@@ -39145,7 +39536,7 @@ async function runUpdateProbeCommand(args, options = {}) {
39145
39536
  const runtime = await readRuntimeRecord(configPath);
39146
39537
  const agent = runtimeAgentCommand(runtime);
39147
39538
  const config = loadBodyConfig({
39148
- workspaceRoot: join14(dirname22(configPath), "workspace"),
39539
+ workspaceRoot: join15(dirname22(configPath), "workspace"),
39149
39540
  llmEnvFile: runtime.llmEnvFile,
39150
39541
  env: { ...env, BUZZ_AGENT_BIN: agent.command, BUZZ_DEV_MCP_BIN: runtime.mcpBinary },
39151
39542
  agent
@@ -39171,7 +39562,7 @@ async function runUpdateProbeCommand(args, options = {}) {
39171
39562
  sandboxRequired: runtime.sandbox !== "off",
39172
39563
  sandboxUnavailableDetail: sandbox.advisory,
39173
39564
  // The successor's probe still holds `<runtimeDir>/update-functional-probe`.
39174
- probeRoot: join14(runtimeDir, "current-release-probe")
39565
+ probeRoot: join15(runtimeDir, "current-release-probe")
39175
39566
  }));
39176
39567
  const report = outcome.kind === "served" ? { probe: "served" } : outcome.kind === "refused" ? { probe: "refused", status: outcome.status, reason: outcome.reason } : outcome.kind === "sandbox-unavailable" ? { probe: "sandbox-unavailable", reason: outcome.reason } : { probe: "failed", reason: outcome.reason };
39177
39568
  write(JSON.stringify(report));
@@ -39344,6 +39735,21 @@ async function runStoredDaemon(pathOrPointer) {
39344
39735
  throw new Error("monolith daemon transport activation failed");
39345
39736
  runtime = activated.runtime;
39346
39737
  const daemonApi = activated.client;
39738
+ const refreshRuntimeAdapter = async () => {
39739
+ const kind = runtime.agentKind;
39740
+ if (!kind)
39741
+ return;
39742
+ const install = latestAdapterInstallCommand(kind);
39743
+ if (!install)
39744
+ return;
39745
+ console.log(`[body] refreshing ${kind} adapter: ${formatAdapterInstallCommand(install)}`);
39746
+ await installLatestAgentAdapter(kind);
39747
+ };
39748
+ try {
39749
+ await refreshRuntimeAdapter();
39750
+ } catch (error) {
39751
+ console.error(`[body] adapter refresh failed; validating the installed copy (${error instanceof Error ? error.message : String(error)})`);
39752
+ }
39347
39753
  const agent = runtimeAgentCommand(runtime);
39348
39754
  await writeDaemonPidRecord(configPath, process.pid);
39349
39755
  const env = {
@@ -39369,7 +39775,7 @@ async function runStoredDaemon(pathOrPointer) {
39369
39775
  if (runtime.sharedSkills)
39370
39776
  config.sharedSkills = [...runtime.sharedSkills];
39371
39777
  if (runtime.modelSelection) {
39372
- await applyRuntimeModelPreflight(config, agent, runtime.modelSelection);
39778
+ await applyRuntimeModelPreflight(config, agent, runtime.modelSelection, void 0, refreshRuntimeAdapter);
39373
39779
  if (!config.modelUnavailable) {
39374
39780
  console.log("[body] persisted model/effort selection passed live startup validation");
39375
39781
  } else {
@@ -39453,6 +39859,7 @@ async function runStoredDaemon(pathOrPointer) {
39453
39859
  };
39454
39860
  let stoppingStatus = "daemon stopped";
39455
39861
  try {
39862
+ let lifecycleRestartDrain;
39456
39863
  const core = new ThinDaemonCore(runtime, configPath, config, {
39457
39864
  daemonApi,
39458
39865
  onConfigChanged: refreshCatalog,
@@ -39465,6 +39872,30 @@ async function runStoredDaemon(pathOrPointer) {
39465
39872
  }
39466
39873
  const timer = setTimeout(() => process.exit(0), delay);
39467
39874
  timer.unref?.();
39875
+ },
39876
+ onRestartRequested: () => {
39877
+ if (lifecycleRestartDrain)
39878
+ return;
39879
+ const deadlineAt = Date.now() + DEFAULT_DRAIN_DEADLINE_MS;
39880
+ stoppingStatus = `restart requested; active work draining; exit_deadline=${new Date(deadlineAt).toISOString()}`;
39881
+ void notifier.progress(stoppingStatus);
39882
+ core.setDrainDeadlineAt(deadlineAt);
39883
+ lifecycleRestartDrain = (async () => {
39884
+ while (core.activeTurnCount() > 0 && Date.now() < deadlineAt) {
39885
+ await new Promise((resolveWait) => setTimeout(resolveWait, 250));
39886
+ }
39887
+ const forced = core.activeTurnCount() > 0;
39888
+ if (forced)
39889
+ await core.prepareForForcedUpdateRestart();
39890
+ else
39891
+ core.quiesceForUpdateIfIdle();
39892
+ stoppingStatus = forced ? "restart requested; drain deadline reached; active work cancelled" : "restart requested; active work drained";
39893
+ await notifier.stopping(stoppingStatus);
39894
+ controller.abort();
39895
+ })().catch((error) => {
39896
+ console.error("[thin-core] requested restart drain failed:", error);
39897
+ controller.abort();
39898
+ });
39468
39899
  }
39469
39900
  });
39470
39901
  const updateDrain = update ? new ManagedUpdateDrain({