usebeeline 0.0.122 → 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 +1936 -1447
  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,1321 +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 };
15327
- }
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");
15346
- }
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 };
15364
- }
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 });
15374
- }
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
- }
15437
- }
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");
15451
- }
15452
- return {
15453
- runId: this.activeRunId(sessionId) ?? `session:${sessionId}`,
15454
- messageId: raw2.outcome
15455
- };
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;
15456
15343
  }
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");
15344
+ if (!lastWasText && current && !continuesPreviousWord(current, delta)) {
15345
+ runs.push(current);
15346
+ current = "";
15465
15347
  }
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 });
15348
+ current += delta;
15349
+ lastWasText = true;
15496
15350
  }
15497
- clearTimer(p) {
15498
- if (p.timer) {
15499
- clearTimeout(p.timer);
15500
- p.timer = void 0;
15501
- }
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, "");
15502
15381
  }
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);
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);
15521
15397
  }
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 {
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)
15534
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 = "";
15535
15437
  }
15536
- this.handleMessage(msg);
15438
+ messageText2 += delta;
15439
+ lastWasMessage = true;
15440
+ thoughtRunOpen = false;
15441
+ continue;
15537
15442
  }
15538
- }
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;
15443
+ if (lastWasMessage && messageText2) {
15444
+ if (!completedMessages.includes(messageText2))
15445
+ completedMessages.push(messageText2);
15446
+ messageText2 = "";
15447
+ }
15448
+ lastWasMessage = false;
15449
+ if (THOUGHT_UPDATE_TYPES.has(kind)) {
15450
+ const delta = normalizeStreamDelta(updateText(update), agentLabel);
15451
+ if (!delta)
15452
+ continue;
15453
+ if (!thoughtRunOpen)
15454
+ thoughtText2 = "";
15455
+ thoughtText2 += delta;
15456
+ thoughtRunOpen = true;
15457
+ } else {
15458
+ thoughtRunOpen = false;
15559
15459
  }
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;
15460
+ }
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");
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 });
15738
15721
  }
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;
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);
15783
+ }
15763
15784
  }
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;
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
+ };
15770
15803
  }
15771
- if (arg.startsWith("--model=") || arg.startsWith("--reasoning-effort=") || /^-m=/.test(arg)) {
15772
- continue;
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");
15773
15812
  }
15774
- stripped.push(arg);
15813
+ return { runId: raw.runId, messageId: raw.messageId };
15775
15814
  }
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;
15799
- }
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
- });
15827
- }
15828
- result.push({
15829
- id,
15830
- category,
15831
- ...typeof record3.currentValue === "string" ? { currentValue: record3.currentValue } : {},
15832
- options: category === "model" ? sortModelChoicesNewestFirst(choices) : choices
15833
- });
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);
15834
15818
  }
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
- });
15819
+ sessionCancel(sessionId) {
15820
+ this.notify("session/cancel", { sessionId });
15851
15821
  }
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
- });
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 });
15860
15835
  }
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
- });
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 });
15878
15843
  }
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
- });
15844
+ clearTimer(p) {
15845
+ if (p.timer) {
15846
+ clearTimeout(p.timer);
15847
+ p.timer = void 0;
15848
+ }
15888
15849
  }
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);
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);
15912
15868
  }
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;
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);
15927
15884
  }
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")
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;
15906
+ }
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 assertModelSelectionAdvertised(advertisedOptions, selection) {
16009
- for (const target of modelSelectionTargets(selection)) {
16010
- if (!target.value)
16011
- continue;
16012
- const axis = advertisedOptions.find((option) => target.categories.includes(option.category));
16013
- if (!axis) {
16014
- continue;
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
+ }
16015
16027
  }
16016
- assertModelConfigAxisAllowed(axis.id, advertisedOptions);
16017
- if (!axis.options.some((choice) => choice.id === target.value)) {
16018
- throw new ModelSelectionUnavailableError({
16019
- label: target.label,
16020
- value: target.value,
16021
- reason: "not-advertised"
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 }
16037
+ }
16022
16038
  });
16039
+ return;
16023
16040
  }
16041
+ if (metadataKey)
16042
+ this.toolCallMetadata.delete(metadataKey);
16043
+ this.write({
16044
+ jsonrpc: "2.0",
16045
+ id,
16046
+ result: { outcome: { outcome: "cancelled" } }
16047
+ });
16024
16048
  }
16025
- }
16026
- function assertModelConfigAxisAllowed(configId, advertisedOptions) {
16027
- const axis = advertisedOptions.find((option) => option.id === configId);
16028
- if (!axis || !isAllowedAgentModelConfigCategory(axis.category)) {
16029
- throw new DisallowedModelConfigOptionError(configId);
16030
- }
16031
- }
16032
- async function applyAgentModelSelection(client, sessionId, advertisedOptions, selection) {
16033
- assertModelSelectionAdvertised(advertisedOptions, selection);
16034
- for (const target of modelSelectionTargets(selection)) {
16035
- if (!target.value)
16036
- continue;
16037
- const axis = advertisedOptions.find((option) => target.categories.includes(option.category));
16038
- if (!axis)
16039
- continue;
16040
- assertModelConfigAxisAllowed(axis.id, advertisedOptions);
16041
- try {
16042
- if (axis.id === GROK_SESSION_MODEL_AXIS_ID) {
16043
- if (!client.setModel)
16044
- throw new Error("ACP client does not support session/set_model");
16045
- await client.setModel(sessionId, target.value);
16046
- } else if (axis.id === GROK_LAUNCH_EFFORT_AXIS_ID) {
16047
- if (axis.currentValue !== target.value) {
16048
- throw new Error(`Grok started with reasoning effort "${axis.currentValue ?? "unknown"}", not "${target.value}"`);
16049
- }
16050
- } else {
16051
- await client.setConfigOption(sessionId, axis.id, target.value);
16052
- }
16053
- } catch (error) {
16054
- throw new ModelSelectionUnavailableError({
16055
- label: target.label,
16056
- value: target.value,
16057
- reason: "provider-refused",
16058
- guidance: error instanceof Error ? error.message : String(error)
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"));
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 } : {}
16059
16073
  });
16060
- }
16074
+ onStart?.(id);
16075
+ this.write(payload);
16076
+ });
16061
16077
  }
16062
- }
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
+ };
16063
16087
 
16064
16088
  // apps/body/dist/model-catalog.js
16065
16089
  function modelCatalogProbeEnvironment(agent, agentEnv, scratchCwd) {
@@ -16109,27 +16133,34 @@ async function withAgentModelCatalog(agent, agentEnv, selection, inspect, limits
16109
16133
  async function fetchAgentModelCatalog(agent, agentEnv, selection, limits = {}) {
16110
16134
  return withAgentModelCatalog(agent, agentEnv, selection, async ({ client, sessionId, raw, catalog }) => ({
16111
16135
  raw,
16112
- catalog: await filterModelChoicesByLiveValidation(client, sessionId, catalog)
16136
+ catalog: await filterModelChoicesByLiveValidation(client, sessionId, catalog, selection?.model)
16113
16137
  }), limits);
16114
16138
  }
16115
- async function filterModelChoicesByLiveValidation(client, sessionId, catalog) {
16139
+ async function filterModelChoicesByLiveValidation(client, sessionId, catalog, preferredModelId) {
16116
16140
  const modelAxis = catalog.find((axis) => axis.category === "model");
16117
16141
  if (!modelAxis)
16118
16142
  return catalog;
16119
16143
  const available = [];
16144
+ const preferred = preferredModelId ?? modelAxis.currentValue;
16145
+ let preferredCatalog;
16120
16146
  for (const choice of modelAxis.options) {
16121
16147
  try {
16122
- await applyAgentModelSelection(client, sessionId, catalog, { model: choice.id });
16148
+ const updated = await applyAgentModelSelectionWithUpdatedCatalog(client, sessionId, catalog, {
16149
+ model: choice.id
16150
+ });
16123
16151
  available.push(choice);
16152
+ if (choice.id === preferred)
16153
+ preferredCatalog = updated;
16124
16154
  } catch {
16125
16155
  }
16126
16156
  }
16127
- return catalog.map((axis) => axis === modelAxis ? { ...axis, options: available } : axis);
16157
+ const effective = preferredCatalog ?? catalog;
16158
+ return effective.map((axis) => axis.category === "model" ? { ...axis, options: available } : axis);
16128
16159
  }
16129
16160
  async function validateAgentModelSelection(agent, agentEnv, selection) {
16130
16161
  return withAgentModelCatalog(agent, agentEnv, selection, async ({ client, sessionId, raw, catalog }) => {
16131
- await applyAgentModelSelection(client, sessionId, catalog, selection);
16132
- return { raw, catalog };
16162
+ const applied = await applyAgentModelSelectionWithUpdatedCatalog(client, sessionId, catalog, selection);
16163
+ return { raw, catalog: applied };
16133
16164
  });
16134
16165
  }
16135
16166
 
@@ -16159,19 +16190,33 @@ function modelUnavailableState(selection, error) {
16159
16190
  }
16160
16191
 
16161
16192
  // apps/body/dist/runtime-model-validation.js
16162
- async function revalidateRuntimeModelSelection(agent, agentEnv, selection, validate = validateAgentModelSelection) {
16193
+ async function revalidateRuntimeModelSelection(agent, agentEnv, selection, validate = validateAgentModelSelection, refreshAdapter) {
16163
16194
  if (!selection.model && !selection.effort)
16164
16195
  return void 0;
16165
16196
  try {
16166
16197
  await validate(agent, agentEnv, selection);
16167
16198
  return void 0;
16168
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
+ }
16169
16214
  return modelUnavailableState(selection, error);
16170
16215
  }
16171
16216
  }
16172
- async function applyRuntimeModelPreflight(config, agent, selection, validate = validateAgentModelSelection) {
16217
+ async function applyRuntimeModelPreflight(config, agent, selection, validate = validateAgentModelSelection, refreshAdapter) {
16173
16218
  config.modelSelection = selection;
16174
- config.modelUnavailable = await revalidateRuntimeModelSelection(agent, config.agentEnv, selection, validate);
16219
+ config.modelUnavailable = await revalidateRuntimeModelSelection(agent, config.agentEnv, selection, validate, refreshAdapter);
16175
16220
  }
16176
16221
 
16177
16222
  // apps/body/dist/model-catalog-sync.js
@@ -16215,7 +16260,7 @@ async function syncAgentModelCatalog(input) {
16215
16260
  const effectiveCatalog = input.startupUnavailable ? catalog : withEffectiveCurrentValues(catalog, selection);
16216
16261
  const hash = modelCatalogHash(effectiveCatalog, selection, input.startupUnavailable);
16217
16262
  const previous = await readFile(hashPath, "utf8").catch(() => "");
16218
- if (previous.trim() === hash)
16263
+ if (!input.force && previous.trim() === hash)
16219
16264
  return "unchanged";
16220
16265
  await input.api.execute("postAgentModelCatalog", {
16221
16266
  agentId: input.agentId,
@@ -16767,7 +16812,7 @@ function describe(error) {
16767
16812
  }
16768
16813
 
16769
16814
  // apps/body/dist/connector-squire.js
16770
- import { execFile, spawn as spawn5 } from "node:child_process";
16815
+ import { execFile, spawn as spawn6 } from "node:child_process";
16771
16816
  import { createHash as createHash2 } from "node:crypto";
16772
16817
  import { lstatSync as lstatSync3, readFileSync as readFileSync4, realpathSync, rmSync } from "node:fs";
16773
16818
  import { homedir as homedir3, hostname, tmpdir as tmpdir2 } from "node:os";
@@ -16916,7 +16961,7 @@ function killConnectTree(child) {
16916
16961
  child.kill();
16917
16962
  }
16918
16963
  var defaultStreamedRunner = (command, args, env) => new Promise((resolve36) => {
16919
- const child = spawn5(command, args, {
16964
+ const child = spawn6(command, args, {
16920
16965
  stdio: ["ignore", "pipe", "pipe"],
16921
16966
  env: env ?? squireConnectProcessEnv(),
16922
16967
  detached: true
@@ -17316,8 +17361,173 @@ async function revokeGrants(mcp, ref) {
17316
17361
  return { revoked, failed };
17317
17362
  }
17318
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
+
17319
17529
  // apps/body/dist/squire-mcp-client.js
17320
- import { spawn as spawn6 } from "node:child_process";
17530
+ import { spawn as spawn7 } from "node:child_process";
17321
17531
  import { homedir as homedir4 } from "node:os";
17322
17532
  var INITIALIZE_TIMEOUT_MS = 3e4;
17323
17533
  var CALL_TIMEOUT_MS = 12e4;
@@ -17375,7 +17585,7 @@ var StdioSquireMcpClient = class {
17375
17585
  }
17376
17586
  initialize() {
17377
17587
  const launch = squireFacadeLaunch(homedir4());
17378
- 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 } });
17379
17589
  this.child = child;
17380
17590
  this.buffer = "";
17381
17591
  child.stdout.setEncoding("utf8");
@@ -17467,6 +17677,7 @@ var ConnectorAssignmentLoop = class {
17467
17677
  log;
17468
17678
  install;
17469
17679
  installGoogle;
17680
+ installTailscale;
17470
17681
  googleHomeDir;
17471
17682
  readVaultFn;
17472
17683
  revokeGrantsFn;
@@ -17475,6 +17686,8 @@ var ConnectorAssignmentLoop = class {
17475
17686
  timer;
17476
17687
  /** Armed only while this helper owns a live sign-in ceremony. */
17477
17688
  connectWatch;
17689
+ /** Armed only while a Tailscale browser login is waiting for its callback. */
17690
+ tailscaleWatch;
17478
17691
  started = false;
17479
17692
  stopped = false;
17480
17693
  /** One install at a time per connector; other polls skip it. */
@@ -17493,6 +17706,7 @@ var ConnectorAssignmentLoop = class {
17493
17706
  onProgress,
17494
17707
  resolvedCredentials: sharedCredentials ? sharedCredentials() : this.resolveGoogleCredentials()
17495
17708
  }));
17709
+ this.installTailscale = options.installTailscale ?? installTailscale;
17496
17710
  this.googleHomeDir = options.googleHome ?? process.env.BEELINE_AGENT_HOME ?? process.cwd();
17497
17711
  this.readVaultFn = options.readVault ?? readVault;
17498
17712
  this.revokeGrantsFn = options.revokeGrants ?? revokeGrants;
@@ -17526,6 +17740,10 @@ var ConnectorAssignmentLoop = class {
17526
17740
  this.cancel(this.connectWatch);
17527
17741
  this.connectWatch = void 0;
17528
17742
  }
17743
+ if (this.tailscaleWatch !== void 0) {
17744
+ this.cancel(this.tailscaleWatch);
17745
+ this.tailscaleWatch = void 0;
17746
+ }
17529
17747
  }
17530
17748
  /**
17531
17749
  * Watch the sign-in this helper is holding open. The phone paints
@@ -17638,12 +17856,57 @@ var ConnectorAssignmentLoop = class {
17638
17856
  }
17639
17857
  async handle(assignment) {
17640
17858
  if (assignment.kind === "install") {
17641
- await this.runInstall(assignment.connectorId);
17642
- } 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") {
17643
17865
  await this.runSync();
17644
- else if (assignment.kind === "revoke-grants")
17866
+ } else if (assignment.kind === "revoke-grants")
17645
17867
  await this.runRevoke(assignment.connectorId, assignment.reference);
17646
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
+ }
17647
17910
  /** Google tool connectors keep their manual credentials next to the runtime. */
17648
17911
  googleHome() {
17649
17912
  return this.googleHomeDir;
@@ -17761,7 +18024,7 @@ var ConnectorAssignmentLoop = class {
17761
18024
  const status = await this.connectorRow(connectorId);
17762
18025
  if (!status)
17763
18026
  return false;
17764
- 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");
17765
18028
  }
17766
18029
  /** One vault report covers every live trusty-squire connector on this helper. */
17767
18030
  async runSync() {
@@ -17797,7 +18060,7 @@ function isAgentCommand(value) {
17797
18060
  "reason"
17798
18061
  ].every((key) => typeof c2[key] === "string" && c2[key].length > 0))
17799
18062
  return false;
17800
- 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)
17801
18064
  return false;
17802
18065
  const source = c2.source;
17803
18066
  return Boolean(source && typeof source.id === "string" && typeof source.authorId === "string" && typeof source.body === "string" && typeof source.createdAt === "number" && Array.isArray(source.attachments));
@@ -17929,7 +18192,7 @@ function hiccupBackoffMs(attempt) {
17929
18192
 
17930
18193
  // apps/body/dist/daemon-api-client.js
17931
18194
  import { resolve as resolve9 } from "node:path";
17932
- import { randomUUID as randomUUID2 } from "node:crypto";
18195
+ import { randomUUID as randomUUID3 } from "node:crypto";
17933
18196
 
17934
18197
  // node_modules/ws/wrapper.mjs
17935
18198
  var import_stream = __toESM(require_stream(), 1);
@@ -17944,12 +18207,12 @@ var wrapper_default = import_websocket.default;
17944
18207
 
17945
18208
  // apps/body/dist/runtime.js
17946
18209
  import { randomBytes as randomBytes5 } from "node:crypto";
17947
- import { execFile as execFile2 } from "node:child_process";
18210
+ import { execFile as execFile3 } from "node:child_process";
17948
18211
  import { closeSync, openSync, readFileSync as readFileSync5 } from "node:fs";
17949
18212
  import { mkdir as mkdir2, readFile as readFile2, readdir, rename as rename2, stat, unlink as unlink2, writeFile as writeFile3 } from "node:fs/promises";
17950
18213
  import { homedir as homedir5 } from "node:os";
17951
18214
  import { dirname as dirname6, resolve as resolve8 } from "node:path";
17952
- import { spawn as spawn7 } from "node:child_process";
18215
+ import { spawn as spawn8 } from "node:child_process";
17953
18216
  import { promisify } from "node:util";
17954
18217
 
17955
18218
  // node_modules/@noble/hashes/_u64.js
@@ -22336,7 +22599,7 @@ function alphabet(letters) {
22336
22599
  };
22337
22600
  }
22338
22601
  // @__NO_SIDE_EFFECTS__
22339
- function join5(separator = "") {
22602
+ function join6(separator = "") {
22340
22603
  astr("join", separator);
22341
22604
  return {
22342
22605
  encode: (from) => {
@@ -22466,8 +22729,8 @@ var base64 = hasBase64Builtin ? {
22466
22729
  decode(s) {
22467
22730
  return decodeBase64Builtin(s, false);
22468
22731
  }
22469
- } : /* @__PURE__ */ chain(/* @__PURE__ */ radix2(6), /* @__PURE__ */ alphabet("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"), /* @__PURE__ */ padding(6), /* @__PURE__ */ join5(""));
22470
- 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(""));
22471
22734
  var POLYMOD_GENERATORS = [996825010, 642813549, 513874426, 1027748829, 705979059];
22472
22735
  function bech32Polymod(pre) {
22473
22736
  const b = pre >> 25;
@@ -26072,13 +26335,13 @@ var NegentropyStorageVector = class {
26072
26335
  let count = last - first;
26073
26336
  while (count > 0) {
26074
26337
  let it = first;
26075
- let step3 = Math.floor(count / 2);
26076
- it += step3;
26338
+ let step4 = Math.floor(count / 2);
26339
+ it += step4;
26077
26340
  if (cmp(arr[it])) {
26078
26341
  first = ++it;
26079
- count -= step3 + 1;
26342
+ count -= step4 + 1;
26080
26343
  } else {
26081
- count = step3;
26344
+ count = step4;
26082
26345
  }
26083
26346
  }
26084
26347
  return first;
@@ -26480,7 +26743,7 @@ function decodeNsec(nsec) {
26480
26743
  }
26481
26744
 
26482
26745
  // apps/body/dist/runtime.js
26483
- var execFileAsync = promisify(execFile2);
26746
+ var execFileAsync = promisify(execFile3);
26484
26747
  var DEFAULT_AGENT_IDENTITY_NAME = "beeline-agent";
26485
26748
  var DEFAULT_BODY_IDENTITY_NAME = "beeline-body";
26486
26749
  var DEFAULT_DAEMON_MONOLITH_BASE_URL = "https://server.usebeeline.app";
@@ -26728,7 +26991,7 @@ async function launchRuntimeDaemon(configPath, opts = {}) {
26728
26991
  const entrypoint = opts.entrypoint ?? process.argv[1];
26729
26992
  if (!entrypoint)
26730
26993
  throw new Error("cannot resolve daemon CLI entrypoint");
26731
- 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)], {
26732
26995
  cwd: directory,
26733
26996
  env: opts.env ?? process.env,
26734
26997
  detached: !foreground,
@@ -26789,7 +27052,7 @@ var DaemonApiClient = class {
26789
27052
  agentId;
26790
27053
  fetchImpl;
26791
27054
  webSocketFactory;
26792
- presenceLifecycleId = randomUUID2();
27055
+ presenceLifecycleId = randomUUID3();
26793
27056
  liveSocket;
26794
27057
  liveReconnect;
26795
27058
  liveReconnectDelayMs = 1e3;
@@ -27058,15 +27321,15 @@ async function activateDaemonTransport(path, fetchImpl = fetch) {
27058
27321
  }
27059
27322
 
27060
27323
  // apps/body/dist/room-runtime.js
27061
- import { execFile as execFile7 } from "node:child_process";
27324
+ import { execFile as execFile8 } from "node:child_process";
27062
27325
  import { createHash as createHash7 } from "node:crypto";
27063
27326
  import { existsSync as existsSync6, mkdirSync as mkdirSync3 } from "node:fs";
27064
- import { mkdir as mkdir15, rm as rm5 } from "node:fs/promises";
27327
+ import { mkdir as mkdir15, rm as rm6 } from "node:fs/promises";
27065
27328
  import { dirname as dirname13, resolve as resolve24 } from "node:path";
27066
27329
  import { promisify as promisify5 } from "node:util";
27067
27330
 
27068
27331
  // apps/body/dist/grant-runner.js
27069
- import { execFile as execFile3 } from "node:child_process";
27332
+ import { execFile as execFile4 } from "node:child_process";
27070
27333
  import { createHash as createHash3, randomBytes as randomBytes6 } from "node:crypto";
27071
27334
  import { readFile as readFile4 } from "node:fs/promises";
27072
27335
  import { createServer } from "node:http";
@@ -27115,6 +27378,18 @@ function commandGrantMatches(rule, requested) {
27115
27378
  return rule.argv.every((word, index) => requested[index] === word);
27116
27379
  }
27117
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
+ }
27118
27393
  var AGENT_GRANT_ESCALATION_REASONS = {
27119
27394
  "unseen-script": "it runs a script whose contents nobody has read",
27120
27395
  credential: "it names a credential or environment file"
@@ -27734,7 +28009,7 @@ var GrantCommandRunner = class {
27734
28009
  const sandboxMarker = runsOnHost ? void 0 : `[beeline-sandbox-started:${randomBytes6(16).toString("hex")}]`;
27735
28010
  const spawn13 = runsOnHost ? { command: argv[0], args: argv.slice(1) } : roomSandboxCommand(policy, room.cwd, argv, sandboxMarker);
27736
28011
  const outcome = await new Promise((resolveRun) => {
27737
- const child = execFile3(spawn13.command, spawn13.args, {
28012
+ const child = execFile4(spawn13.command, spawn13.args, {
27738
28013
  cwd: room.cwd,
27739
28014
  env,
27740
28015
  timeout: this.options.timeoutMs ?? GRANT_COMMAND_TIMEOUT_MS,
@@ -28030,16 +28305,16 @@ function captureConnectionUsage(recorder, turn, calls) {
28030
28305
  }
28031
28306
 
28032
28307
  // apps/body/dist/server-command-intake.js
28033
- import { randomUUID as randomUUID3 } from "node:crypto";
28308
+ import { randomUUID as randomUUID4 } from "node:crypto";
28034
28309
  import { mkdir as mkdir4, writeFile as writeFile5 } from "node:fs/promises";
28035
- import { tmpdir as tmpdir3 } from "node:os";
28036
- 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";
28037
28312
  var CommandExecutionContext = class {
28038
- generationId = randomUUID3();
28313
+ generationId = randomUUID4();
28039
28314
  path;
28040
28315
  current;
28041
28316
  constructor(root) {
28042
- this.path = join6(root ?? tmpdir3(), `beeline-command-${this.generationId}.json`);
28317
+ this.path = join7(root ?? tmpdir4(), `beeline-command-${this.generationId}.json`);
28043
28318
  }
28044
28319
  async enter(command) {
28045
28320
  this.current = command;
@@ -28130,7 +28405,9 @@ async function runServerCommandIntake(options) {
28130
28405
  return;
28131
28406
  for (const command of [...pending.values()]) {
28132
28407
  validateServerCommand(command, roomId, agentId);
28133
- 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)
28134
28411
  continue;
28135
28412
  pending.delete(command.id);
28136
28413
  claimed.add(command.id);
@@ -28145,7 +28422,11 @@ async function runServerCommandIntake(options) {
28145
28422
  options.onError?.(error);
28146
28423
  continue;
28147
28424
  }
28148
- 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") {
28149
28430
  options.stop(command.turnRequestId);
28150
28431
  await api.execute("acknowledgeAgentCommand", {
28151
28432
  roomId,
@@ -28177,7 +28458,7 @@ async function runServerCommandIntake(options) {
28177
28458
  wake = done;
28178
28459
  const timer = setTimeout(() => done(true), pushIntakeAcknowledged ? 6e4 : options.pollMs ?? 1e3);
28179
28460
  signal?.addEventListener("abort", aborted, { once: true });
28180
- if (pending.size && !busy)
28461
+ if (!busy && [...pending.values()].some((command) => command.action === "stop" || command.action === "restart" || options.canStartTurn?.() !== false))
28181
28462
  done(false);
28182
28463
  });
28183
28464
  if (signal?.aborted)
@@ -28196,11 +28477,11 @@ async function runServerCommandIntake(options) {
28196
28477
  }
28197
28478
 
28198
28479
  // apps/body/dist/monolith-corner-turn.js
28199
- import { execFile as execFile6 } from "node:child_process";
28480
+ import { execFile as execFile7 } from "node:child_process";
28200
28481
  import { createHash as createHash6 } from "node:crypto";
28201
28482
  import { mkdir as mkdir14 } from "node:fs/promises";
28202
28483
  import { homedir as homedir10 } from "node:os";
28203
- import { join as join12 } from "node:path";
28484
+ import { join as join13 } from "node:path";
28204
28485
  import { promisify as promisify4 } from "node:util";
28205
28486
 
28206
28487
  // apps/body/dist/agent-home.js
@@ -29002,10 +29283,10 @@ function stringify(obj, { maxDepth = 1e3, numbersAsFloat = false } = {}) {
29002
29283
  }
29003
29284
 
29004
29285
  // apps/body/dist/agent-home.js
29005
- import { createHash as createHash4, randomUUID as randomUUID4 } from "node:crypto";
29006
- 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";
29007
29288
  import { homedir as homedir8 } from "node:os";
29008
- 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";
29009
29290
 
29010
29291
  // apps/body/dist/beeline-skill.js
29011
29292
  import { readFileSync as readFileSync6 } from "node:fs";
@@ -29017,6 +29298,7 @@ var MESSAGE_REACTION_EMOJIS = ["\u{1F44D}", "\u2764\uFE0F", "\u{1F602}", "\u{1F3
29017
29298
  // packages/api-contract/dist/workbench.js
29018
29299
  var CONNECTABLE_CONNECTOR_KINDS = [
29019
29300
  "trusty-squire",
29301
+ "tailscale",
29020
29302
  "google-gmail",
29021
29303
  "google-calendar",
29022
29304
  "google-drive",
@@ -29138,7 +29420,7 @@ When your corner's pull request is ready, merging is your step: once the configu
29138
29420
 
29139
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.
29140
29422
 
29141
- 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.
29142
29424
 
29143
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.
29144
29426
 
@@ -29395,7 +29677,7 @@ function stringArray(value) {
29395
29677
  }
29396
29678
 
29397
29679
  // apps/body/dist/host-mcp-route.js
29398
- import { join as join7, resolve as resolve14 } from "node:path";
29680
+ import { join as join8, resolve as resolve14 } from "node:path";
29399
29681
  var import_yaml = __toESM(require_dist(), 1);
29400
29682
  function grantedMcpServerNames(grants) {
29401
29683
  const names = /* @__PURE__ */ new Set();
@@ -29412,18 +29694,33 @@ function ungatedHostServers(hostServers, granted) {
29412
29694
  const allowed = new Set(granted);
29413
29695
  return hostServers.filter((name) => !allowed.has(name));
29414
29696
  }
29415
- function grantedHostRoutesFromList(result) {
29697
+ async function claimGrantedHostRoutes(result, consumeOnce) {
29416
29698
  if (!result || typeof result !== "object" || !("grants" in result))
29417
29699
  return [];
29418
29700
  const grants = result.grants;
29419
29701
  if (!Array.isArray(grants))
29420
29702
  return [];
29421
- return grantedMcpServerNames(grants.flatMap((entry) => {
29703
+ const mounted = [];
29704
+ for (const entry of grants) {
29422
29705
  if (!entry || typeof entry !== "object")
29423
- return [];
29424
- const row = entry;
29425
- return typeof row.kind === "string" && typeof row.target === "string" ? [{ kind: row.kind, target: row.target }] : [];
29426
- }));
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);
29427
29724
  }
29428
29725
  function recordValue(value) {
29429
29726
  return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
@@ -29446,7 +29743,7 @@ function grantedSquireHostRoute(granted, declarations = {}) {
29446
29743
  });
29447
29744
  }
29448
29745
  function hostRouteEnv(hostHome) {
29449
- return { XDG_CONFIG_HOME: join7(resolve14(hostHome), ".config") };
29746
+ return { XDG_CONFIG_HOME: join8(resolve14(hostHome), ".config") };
29450
29747
  }
29451
29748
  function rewriteHostMcpDeclaration(name, declaration, hostHome) {
29452
29749
  const next = { ...declaration };
@@ -30630,7 +30927,7 @@ async function provisionManagedSkillsDir(target, managedSkills, sharedSkills, op
30630
30927
  const plan = await planManagedSkills(managedSkills, sharedSkills, optionalShares);
30631
30928
  if (await materializedSkillManifest(target) === plan.manifest)
30632
30929
  return;
30633
- const staged = resolve16(parent, `.skills.${process.pid}.${randomUUID4()}.tmp`);
30930
+ const staged = resolve16(parent, `.skills.${process.pid}.${randomUUID5()}.tmp`);
30634
30931
  await mkdir6(staged, { mode: 448 });
30635
30932
  try {
30636
30933
  for (const entry of plan.entries) {
@@ -30644,10 +30941,10 @@ async function provisionManagedSkillsDir(target, managedSkills, sharedSkills, op
30644
30941
  }
30645
30942
  const existing = await lstat2(target).catch(() => void 0);
30646
30943
  if (existing)
30647
- await rm2(target, { recursive: existing.isDirectory(), force: true });
30944
+ await rm3(target, { recursive: existing.isDirectory(), force: true });
30648
30945
  await rename3(staged, target);
30649
30946
  } finally {
30650
- await rm2(staged, { recursive: true, force: true });
30947
+ await rm3(staged, { recursive: true, force: true });
30651
30948
  }
30652
30949
  }
30653
30950
  async function planManagedSkills(managedSkills, sharedSkills, optionalShares) {
@@ -30666,8 +30963,8 @@ async function planManagedSkills(managedSkills, sharedSkills, optionalShares) {
30666
30963
  try {
30667
30964
  const tree = [];
30668
30965
  await walkSafeSkillTree(shared.source, shared.source, {
30669
- directory: async (rel) => void tree.push(`d ${join8(shared.name, rel)}`),
30670
- 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))}`)
30671
30968
  });
30672
30969
  entries.push({ kind: "shared", name: shared.name, source: shared.source });
30673
30970
  lines.push(...tree);
@@ -30687,7 +30984,7 @@ async function materializedSkillManifest(target) {
30687
30984
  const visit = async (directory, prefix) => {
30688
30985
  for (const entry of await readdir2(directory)) {
30689
30986
  const path = resolve16(directory, entry);
30690
- const rel = prefix ? join8(prefix, entry) : entry;
30987
+ const rel = prefix ? join9(prefix, entry) : entry;
30691
30988
  const entryStats = await lstat2(path);
30692
30989
  if (entryStats.isSymbolicLink())
30693
30990
  return false;
@@ -30855,7 +31152,7 @@ async function walkSafeSkillTree(source, sourceRoot, visitor, rel = "") {
30855
31152
  for (const entry of await readdir2(resolvedSource)) {
30856
31153
  if (entry === "." || entry === "..")
30857
31154
  throw new Error("invalid shared skill entry");
30858
- 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);
30859
31156
  }
30860
31157
  return;
30861
31158
  }
@@ -30882,7 +31179,7 @@ async function writeIsolatedHarnessFile(path, content) {
30882
31179
  if (!parentStats.isDirectory() || parentStats.isSymbolicLink()) {
30883
31180
  throw new Error(`isolated harness parent is not a real directory: ${parent}`);
30884
31181
  }
30885
- const temporary = resolve16(parent, `.${basename5(path)}.${process.pid}.${randomUUID4()}.tmp`);
31182
+ const temporary = resolve16(parent, `.${basename5(path)}.${process.pid}.${randomUUID5()}.tmp`);
30886
31183
  try {
30887
31184
  await writeFile7(temporary, content, { mode: 384, flag: "wx" });
30888
31185
  await chmod3(temporary, 384);
@@ -30941,7 +31238,7 @@ function agentCommandCatalogPublisher(input) {
30941
31238
 
30942
31239
  // apps/body/dist/attachment-delivery.js
30943
31240
  import { mkdir as mkdir7, writeFile as writeFile8 } from "node:fs/promises";
30944
- import { basename as basename6, extname, join as join9 } from "node:path";
31241
+ import { basename as basename6, extname, join as join10 } from "node:path";
30945
31242
  var MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024;
30946
31243
  var EXPIRED_REASON = "expired: these attachment bytes are past their retention window";
30947
31244
  var FETCH_TIMEOUT_MS = 3e4;
@@ -31000,7 +31297,7 @@ async function deliverAttachments(attachments, dir, fetchImpl = fetch) {
31000
31297
  return { attachment, reason: EXPIRED_REASON };
31001
31298
  if (!fetched.ok)
31002
31299
  throw new Error(`HTTP ${fetched.status}`);
31003
- const path = join9(dir, safeFileName(attachment, index, taken));
31300
+ const path = join10(dir, safeFileName(attachment, index, taken));
31004
31301
  await writeFile8(path, fetched.bytes);
31005
31302
  const bytes = fetched.bytes;
31006
31303
  const mimeType = attachment.mimeType ?? fetched.mimeType;
@@ -31580,14 +31877,18 @@ export default async function (pi) {
31580
31877
  label: labelOf(tool.name),
31581
31878
  description: typeof tool.description === 'string' ? tool.description : tool.name,
31582
31879
  parameters: schema,
31583
- async execute(_toolCallId, params, signal) {
31880
+ async execute(toolCallId, params, signal) {
31584
31881
  const result = await callServer(
31585
31882
  server,
31586
31883
  {
31587
31884
  jsonrpc: '2.0',
31588
31885
  id: 1,
31589
31886
  method: 'tools/call',
31590
- params: { name: tool.name, arguments: params ?? {} },
31887
+ params: {
31888
+ name: tool.name,
31889
+ arguments: params ?? {},
31890
+ _meta: { beelineToolCallId: toolCallId },
31891
+ },
31591
31892
  },
31592
31893
  signal,
31593
31894
  );
@@ -31604,9 +31905,9 @@ export default async function (pi) {
31604
31905
  `;
31605
31906
 
31606
31907
  // apps/body/dist/corner-branch-sync.js
31607
- import { execFile as execFile4 } from "node:child_process";
31908
+ import { execFile as execFile5 } from "node:child_process";
31608
31909
  import { promisify as promisify2 } from "node:util";
31609
- var execFileAsync2 = promisify2(execFile4);
31910
+ var execFileAsync2 = promisify2(execFile5);
31610
31911
  async function syncCornerBranch(input) {
31611
31912
  const git = input.git ?? (async (args) => (await execFileAsync2("git", ["-C", input.worktreePath, ...args], {
31612
31913
  ...input.env ? { env: input.env } : {},
@@ -32082,6 +32383,7 @@ function beelineAgentMcpServer(config, api, context) {
32082
32383
  { name: "BEELINE_DAEMON_ROOM_ID", value: context.roomId },
32083
32384
  { name: "BEELINE_DAEMON_WORKSPACE_ID", value: context.workspaceId },
32084
32385
  ...context.cornerId ? [{ name: "BEELINE_DAEMON_CORNER_ID", value: context.cornerId }] : [],
32386
+ ...context.agentMayCloseCorner ? [{ name: "BEELINE_CORNER_AGENT_CLOSE", value: "1" }] : [],
32085
32387
  ...context.reviewer ? [{ name: "BEELINE_CORNER_REVIEWER", value: "1" }] : [],
32086
32388
  ...context.attachRoot ? [{ name: "BEELINE_ATTACH_ROOT", value: context.attachRoot }] : [],
32087
32389
  ...context.attachScratchRoot ? [{ name: "BEELINE_ATTACH_SCRATCH_ROOT", value: context.attachScratchRoot }] : [],
@@ -32128,11 +32430,11 @@ function youtubeMcpServer(config, accessToken) {
32128
32430
  }
32129
32431
 
32130
32432
  // apps/body/dist/codegraph.js
32131
- import { execFile as execFile5 } from "node:child_process";
32433
+ import { execFile as execFile6 } from "node:child_process";
32132
32434
  import { access, appendFile, mkdir as mkdir9, readFile as readFile7 } from "node:fs/promises";
32133
32435
  import { dirname as dirname11, isAbsolute as isAbsolute3, resolve as resolve19 } from "node:path";
32134
32436
  import { promisify as promisify3 } from "node:util";
32135
- var execFileAsync3 = promisify3(execFile5);
32437
+ var execFileAsync3 = promisify3(execFile6);
32136
32438
  var CODEGRAPH_MCP_SERVER_NAME = "codegraph";
32137
32439
  var CODEGRAPH_INDEX_PATH = ".codegraph/codegraph.db";
32138
32440
  var CODEGRAPH_PREPARE_TIMEOUT_MS = 12e4;
@@ -32433,7 +32735,7 @@ async function withTurnReceiptHeartbeat(api, receipt, task, onHeartbeatError) {
32433
32735
  }
32434
32736
 
32435
32737
  // apps/body/dist/turn-trace.js
32436
- 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";
32437
32739
  import { resolve as resolve21 } from "node:path";
32438
32740
  import { performance as performance2 } from "node:perf_hooks";
32439
32741
  var TURN_PHASES = [
@@ -32705,7 +33007,7 @@ var TurnTraceFile = class {
32705
33007
  this.prunedDay = day;
32706
33008
  const cutoff = traceFileName(new Date(now2.getTime() - TURN_TRACE_RETENTION_DAYS * 864e5));
32707
33009
  const names = await readdir4(this.directory).catch(() => []);
32708
- 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 })));
32709
33011
  }
32710
33012
  };
32711
33013
 
@@ -32864,10 +33166,10 @@ process.exit(result.status ?? 1);
32864
33166
  }
32865
33167
 
32866
33168
  // apps/body/dist/warm-node-modules.js
32867
- import { createHash as createHash5, randomUUID as randomUUID5 } from "node:crypto";
33169
+ import { createHash as createHash5, randomUUID as randomUUID6 } from "node:crypto";
32868
33170
  import { constants as fsConstants } from "node:fs";
32869
- 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";
32870
- 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";
32871
33173
  var STORE_FORMAT = "v1";
32872
33174
  var STAGING_PREFIX = ".beeline-warm-";
32873
33175
  var STAGING_SWEEP_MS = 24 * 60 * 60 * 1e3;
@@ -32935,7 +33237,7 @@ async function seedWarmNodeModules(input) {
32935
33237
  if (storeDevice === void 0 || storeDevice !== checkoutDevice) {
32936
33238
  return { reason: "cross-device", key: plan.key };
32937
33239
  }
32938
- const staging = resolve23(input.storeRoot, `${STAGING_PREFIX}${process.pid}.${randomUUID5()}`);
33240
+ const staging = resolve23(input.storeRoot, `${STAGING_PREFIX}${process.pid}.${randomUUID6()}`);
32939
33241
  const placed = [];
32940
33242
  const now2 = (input.now ?? Date.now)();
32941
33243
  try {
@@ -32953,11 +33255,11 @@ async function seedWarmNodeModules(input) {
32953
33255
  return { reason: "seeded", key: plan.key };
32954
33256
  } catch (error) {
32955
33257
  for (const target of placed) {
32956
- await rm4(target, { recursive: true, force: true }).catch(() => void 0);
33258
+ await rm5(target, { recursive: true, force: true }).catch(() => void 0);
32957
33259
  }
32958
33260
  return { reason: "failed", key: plan.key, detail: describe3(error) };
32959
33261
  } finally {
32960
- await rm4(staging, { recursive: true, force: true }).catch(() => void 0);
33262
+ await rm5(staging, { recursive: true, force: true }).catch(() => void 0);
32961
33263
  }
32962
33264
  }
32963
33265
  async function harvestWarmNodeModules(input) {
@@ -32981,7 +33283,7 @@ async function harvestWarmNodeModules(input) {
32981
33283
  detail: `${missing.length} absent, e.g. ${missing.slice(0, 3).join(", ")}`
32982
33284
  };
32983
33285
  }
32984
- const staging = resolve23(input.storeRoot, `${STAGING_PREFIX}${process.pid}.${randomUUID5()}`);
33286
+ const staging = resolve23(input.storeRoot, `${STAGING_PREFIX}${process.pid}.${randomUUID6()}`);
32985
33287
  try {
32986
33288
  await mkdir12(input.storeRoot, { recursive: true, mode: 493 });
32987
33289
  await sweepStaleStaging(input.storeRoot, (input.now ?? Date.now)());
@@ -33000,14 +33302,14 @@ async function harvestWarmNodeModules(input) {
33000
33302
  return { reason: "already-warm", key: plan.key };
33001
33303
  return { reason: "failed", key: plan.key, detail: describe3(error) };
33002
33304
  } finally {
33003
- await rm4(staging, { recursive: true, force: true }).catch(() => void 0);
33305
+ await rm5(staging, { recursive: true, force: true }).catch(() => void 0);
33004
33306
  }
33005
33307
  }
33006
33308
  async function stagedTreeIsPublishable(worktreePath, staging, key) {
33007
33309
  const settled = await readWarmPlan(worktreePath);
33008
33310
  if (isPlanRefusal(settled) || settled.key !== key)
33009
33311
  return false;
33010
- const hidden = join10("node_modules", ".package-lock.json");
33312
+ const hidden = join11("node_modules", ".package-lock.json");
33011
33313
  const [copied, current] = await Promise.all([
33012
33314
  readFile9(resolve23(staging, hidden)).catch(() => void 0),
33013
33315
  readFile9(resolve23(worktreePath, hidden)).catch(() => void 0)
@@ -33045,7 +33347,7 @@ async function missingInstalledPackages(worktreePath, treeRoot = worktreePath) {
33045
33347
  return missing.sort();
33046
33348
  }
33047
33349
  async function isInstalledPackage(path) {
33048
- return stat2(join10(path, "package.json")).then((info) => info.isFile(), () => false);
33350
+ return stat2(join11(path, "package.json")).then((info) => info.isFile(), () => false);
33049
33351
  }
33050
33352
  var INTEGRITY_READ_CONCURRENCY = 64;
33051
33353
  async function mapWithLimit(values, limit, visit) {
@@ -33079,8 +33381,8 @@ function isContainedTreePath(value) {
33079
33381
  async function cloneTree(source, target, file, topLevel = true) {
33080
33382
  await mkdir12(target, { recursive: true, mode: 493 });
33081
33383
  for (const entry of await readdir5(source, { withFileTypes: true })) {
33082
- const from = join10(source, entry.name);
33083
- const to = join10(target, entry.name);
33384
+ const from = join11(source, entry.name);
33385
+ const to = join11(target, entry.name);
33084
33386
  if (entry.isSymbolicLink()) {
33085
33387
  await symlink2(await readlink(from), to);
33086
33388
  continue;
@@ -33110,13 +33412,13 @@ async function pruneWarmStore(storeRoot, keep) {
33110
33412
  const names = (await readdir5(storeRoot).catch(() => [])).filter((name) => !name.startsWith(STAGING_PREFIX));
33111
33413
  const entries = [];
33112
33414
  for (const name of names) {
33113
- const info = await lstat3(join10(storeRoot, name)).catch(() => void 0);
33415
+ const info = await lstat3(join11(storeRoot, name)).catch(() => void 0);
33114
33416
  if (info?.isDirectory())
33115
33417
  entries.push({ name, usedAt: info.mtimeMs });
33116
33418
  }
33117
33419
  const dropped = entries.sort((a2, b) => b.usedAt - a2.usedAt || a2.name.localeCompare(b.name)).slice(keep);
33118
33420
  for (const entry of dropped) {
33119
- 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);
33120
33422
  }
33121
33423
  return dropped.map((entry) => entry.name);
33122
33424
  }
@@ -33125,11 +33427,11 @@ async function sweepStaleStaging(storeRoot, now2) {
33125
33427
  for (const name of entries) {
33126
33428
  if (!name.startsWith(STAGING_PREFIX))
33127
33429
  continue;
33128
- const path = join10(storeRoot, name);
33430
+ const path = join11(storeRoot, name);
33129
33431
  const info = await lstat3(path).catch(() => void 0);
33130
33432
  if (!info || now2 - info.mtimeMs < STAGING_SWEEP_MS)
33131
33433
  continue;
33132
- await rm4(path, { recursive: true, force: true }).catch(() => void 0);
33434
+ await rm5(path, { recursive: true, force: true }).catch(() => void 0);
33133
33435
  }
33134
33436
  }
33135
33437
  async function pathExists(path) {
@@ -33148,7 +33450,7 @@ function describe3(error) {
33148
33450
  // apps/body/dist/monolith-room-turn.js
33149
33451
  import { mkdir as mkdir13 } from "node:fs/promises";
33150
33452
  import { homedir as homedir9 } from "node:os";
33151
- import { join as join11 } from "node:path";
33453
+ import { join as join12 } from "node:path";
33152
33454
 
33153
33455
  // packages/api-contract/dist/scheduled-prompts.js
33154
33456
  var SCHEDULE_SCHEDULER_NAME = "Beeline Scheduler";
@@ -33192,6 +33494,14 @@ function resumePrompt(item) {
33192
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.'
33193
33495
  ].join(" ");
33194
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
+ }
33195
33505
  return [
33196
33506
  "This is the answer to your grant request; your paused work resumes now.",
33197
33507
  "If it was approved and it is a command grant, run it with run_granted_command and the exact argv.",
@@ -33367,7 +33677,7 @@ var MonolithRoomTurnLoop = class {
33367
33677
  const cached = this.deliveredAttachments.get(item.id);
33368
33678
  if (cached)
33369
33679
  return cached;
33370
- 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);
33371
33681
  this.deliveredAttachments.set(item.id, withoutImageData(delivered));
33372
33682
  return delivered;
33373
33683
  }
@@ -33424,9 +33734,10 @@ var MonolithRoomTurnLoop = class {
33424
33734
  }
33425
33735
  async grantedHostRoutes() {
33426
33736
  try {
33427
- return grantedHostRoutesFromList(await this.options.api.execute("listAgentGrants", {
33428
- agentId: this.agent.publicKey
33429
- }));
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 }));
33430
33741
  } catch {
33431
33742
  return [];
33432
33743
  }
@@ -33487,7 +33798,7 @@ var MonolithRoomTurnLoop = class {
33487
33798
  args: this.options.config.agentArgs ?? []
33488
33799
  }, selection);
33489
33800
  const { stateDirs, tmpDir } = harnessStateDirsFromEnv(agentEnv);
33490
- this.attachmentDir = tmpDir ? join11(tmpDir, "beeline-attachments") : void 0;
33801
+ this.attachmentDir = tmpDir ? join12(tmpDir, "beeline-attachments") : void 0;
33491
33802
  this.sessionScratchDir = tmpDir;
33492
33803
  this.sessionStateDirs = stateDirs;
33493
33804
  const homeStateDirs = harnessHomeStateDirs(harnessLabel, agentEnv.HOME ?? operatorHome);
@@ -33976,6 +34287,8 @@ ${JSON.stringify((corners.corners ?? []).filter((corner) => !corner.archived))}`
33976
34287
  onPoll: () => this.options.health.poll(),
33977
34288
  onError: (error) => console.error("[thin-core] Room command failed", error),
33978
34289
  stop: (requestId) => this.stopTurn(requestId),
34290
+ restart: () => this.options.onRestartRequested?.(),
34291
+ canStartTurn: this.options.canStartTurn,
33979
34292
  run: async (command) => {
33980
34293
  const item = {
33981
34294
  ...command.source,
@@ -34009,7 +34322,7 @@ function roomMessagePrompt(author, body, attachments, delivered, harnessAcceptsI
34009
34322
  }
34010
34323
 
34011
34324
  // apps/body/dist/monolith-corner-turn.js
34012
- var execFileAsync4 = promisify4(execFile6);
34325
+ var execFileAsync4 = promisify4(execFile7);
34013
34326
  var TOOL_ARGUMENT_MAX_BYTES = 1200;
34014
34327
  var TOOL_OUTPUT_MAX_BYTES = 3200;
34015
34328
  var TOOL_PATH_LIMIT = 12;
@@ -34026,6 +34339,8 @@ function cornerReviewerInstruction(input) {
34026
34339
  const headSha = input.headSha ?? "<head sha>";
34027
34340
  return `Checks are green on PR #${number} at ${headSha}. Review it now with the beeline-review skill against that exact head. FAIL: reply \`@${author}\` with the confirmed findings to fix. PASS: call the approve_merge tool for ${headSha}, then reply \`@${author} approved ${headSha}, merge\`. Never merge yourself. Never say you are holding or waiting for checks.`;
34028
34341
  }
34342
+ var CORNER_REVIEWER_SESSION_INSTRUCTION = "You are this Room's configured reviewer. The active turn prompt names the latest stable green PR head. Review and approve only that exact head; if no stable green head is named, end the turn without a verdict. Never merge yourself.";
34343
+ var CORNER_REVIEWER_UNSTABLE_HEAD_INSTRUCTION = "There is no stable green PR head for this active reviewer turn. Do not review or call approve_merge. End this turn without a verdict; the next green transition will wake you.";
34029
34344
  function cornerSelfReviewerInstruction(input) {
34030
34345
  if (!input.reviewerHandle || !input.agentHandle || !input.openedByAgent || input.agentHandle.replace(/^@/, "") !== input.reviewerHandle.replace(/^@/, ""))
34031
34346
  return void 0;
@@ -34247,6 +34562,8 @@ var MonolithCornerTurnLoop = class {
34247
34562
  yoloMode = false;
34248
34563
  /** The live parent-Room reviewer baked into the current session. */
34249
34564
  reviewerHandle;
34565
+ /** Identity-only reviewer context; the exact PR head is refreshed inside each active turn. */
34566
+ reviewerInstructionInput;
34250
34567
  /** The role-specific second-chance instruction for this session. */
34251
34568
  cornerTurnEndNudge = CORNER_DELIVERY_NUDGE;
34252
34569
  /** Repository state already given a delivery reminder, until that state changes. */
@@ -34392,9 +34709,10 @@ var MonolithCornerTurnLoop = class {
34392
34709
  }
34393
34710
  async grantedHostRoutes() {
34394
34711
  try {
34395
- return grantedHostRoutesFromList(await this.options.api.execute("listAgentGrants", {
34396
- agentId: this.agent.publicKey
34397
- }));
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 }));
34398
34716
  } catch {
34399
34717
  return [];
34400
34718
  }
@@ -34421,17 +34739,8 @@ var MonolithCornerTurnLoop = class {
34421
34739
  authorHandle: opener?.handle,
34422
34740
  openedByAgent: !this.options.openedBy || this.options.openedBy === this.agent.publicKey
34423
34741
  };
34424
- let reviewerInstruction = cornerReviewerInstruction(reviewerInput);
34425
- if (reviewerInstruction) {
34426
- const restore = await this.options.api.execute("getCornerRestoreState", {
34427
- cornerId: this.options.cornerId
34428
- });
34429
- reviewerInstruction = cornerReviewerInstruction({
34430
- ...reviewerInput,
34431
- pullRequestNumber: restore.lifecycle?.pr?.number,
34432
- headSha: restore.lifecycle?.pr?.headSha
34433
- });
34434
- }
34742
+ const reviewerInstruction = cornerReviewerInstruction(reviewerInput) ? CORNER_REVIEWER_SESSION_INSTRUCTION : void 0;
34743
+ this.reviewerInstructionInput = reviewerInstruction ? reviewerInput : void 0;
34435
34744
  const selfReviewerInstruction = cornerSelfReviewerInstruction(reviewerInput);
34436
34745
  this.cornerTurnEndNudge = reviewerInstruction ?? selfReviewerInstruction ?? cornerMergeInstruction(configuration.yoloMode, configuration.reviewerHandle);
34437
34746
  await mkdir14(this.options.worktreePath, { recursive: true });
@@ -34490,7 +34799,7 @@ var MonolithCornerTurnLoop = class {
34490
34799
  args: this.options.config.agentArgs ?? []
34491
34800
  }, selection);
34492
34801
  const { stateDirs, tmpDir } = harnessStateDirsFromEnv(agentEnv);
34493
- this.attachmentDir = tmpDir ? join12(tmpDir, "beeline-attachments") : void 0;
34802
+ this.attachmentDir = tmpDir ? join13(tmpDir, "beeline-attachments") : void 0;
34494
34803
  this.sessionScratchDir = tmpDir;
34495
34804
  const homeStateDirs = harnessHomeStateDirs(harnessLabel, agentEnv.HOME ?? operatorHome);
34496
34805
  await Promise.all(homeStateDirs.map((dir) => mkdir14(dir, { recursive: true })));
@@ -34579,6 +34888,7 @@ var MonolithCornerTurnLoop = class {
34579
34888
  roomId: this.options.parentRoomId,
34580
34889
  workspaceId: this.options.workspaceId,
34581
34890
  cornerId: this.options.cornerId,
34891
+ agentMayCloseCorner: Boolean(repository),
34582
34892
  reviewer: Boolean(reviewerInstruction),
34583
34893
  attachRoot: this.options.worktreePath,
34584
34894
  // The whole per-session overlay, not an enumerated subset: see
@@ -34636,10 +34946,9 @@ var MonolithCornerTurnLoop = class {
34636
34946
  "Work in this corner's writable workspace. Use write_scratch_file or ordinary tools to create files, then post_artifact with the path to send them back to the corner.",
34637
34947
  "Do not initialize a repository, create a branch, commit, push, open a pull request, or wait for GitHub checks.",
34638
34948
  // This lane has no pull request URL and no merge card, so its
34639
- // attached final reply is the requester-facing completion
34640
- // signal. close_corner is still the lifecycle completion: it
34641
- // archives the corner and reaps this scratch workspace.
34642
- this.options.requesterHandle ? `Deliver the result as artifacts: post_artifact everything the objective asked for, then call close_corner. Finish the turn by replying with @${this.options.requesterHandle} and one line on what you posted; that attached reply is the requester-facing completion signal.` : `Deliver the result as artifacts: post_artifact everything the objective asked for, then call close_corner. Finish the turn by replying with one line on what you posted; that attached reply is the requester-facing completion signal.`
34949
+ // attached final reply reports delivery to the requester. The
34950
+ // corner remains open until a human explicitly closes it.
34951
+ this.options.requesterHandle ? `Deliver the result as artifacts: post_artifact everything the objective asked for. Finish the turn by replying with @${this.options.requesterHandle} and one line on what you posted. The corner stays open until a human explicitly closes it.` : `Deliver the result as artifacts: post_artifact everything the objective asked for. Finish the turn by replying with one line on what you posted. The corner stays open until a human explicitly closes it.`
34643
34952
  ]
34644
34953
  ].filter(Boolean).join("\n\n")
34645
34954
  });
@@ -34652,6 +34961,41 @@ var MonolithCornerTurnLoop = class {
34652
34961
  }
34653
34962
  return opened.sessionId;
34654
34963
  }
34964
+ /**
34965
+ * Resolve the review target at prompt time, not session activation time.
34966
+ *
34967
+ * A push can land after a reviewer session starts but before its first
34968
+ * prompt, or while that prompt is running. The active prompt and its bounded
34969
+ * second pass therefore each read the current green lifecycle head. The
34970
+ * server still compares approve_merge's SHA with the current head, so a push
34971
+ * after this read fails closed too.
34972
+ */
34973
+ async activeReviewerInstruction() {
34974
+ const input = this.reviewerInstructionInput;
34975
+ if (!input)
34976
+ return void 0;
34977
+ try {
34978
+ const [restore, localHead] = await Promise.all([
34979
+ this.options.api.execute("getCornerRestoreState", {
34980
+ cornerId: this.options.cornerId
34981
+ }),
34982
+ execFileAsync4("git", ["-C", this.options.worktreePath, "rev-parse", "HEAD"]).then(({ stdout: stdout6 }) => stdout6.trim())
34983
+ ]);
34984
+ const pr = restore.lifecycle?.pr;
34985
+ if (restore.lifecycle?.checks !== "passing" || !pr?.number || !pr.headSha || pr.headSha !== localHead)
34986
+ return CORNER_REVIEWER_UNSTABLE_HEAD_INSTRUCTION;
34987
+ return [
34988
+ "Current stable reviewer target for this active turn; it supersedes any older head in the trigger or transcript:",
34989
+ cornerReviewerInstruction({
34990
+ ...input,
34991
+ pullRequestNumber: pr.number,
34992
+ headSha: pr.headSha
34993
+ })
34994
+ ].join("\n");
34995
+ } catch {
34996
+ return CORNER_REVIEWER_UNSTABLE_HEAD_INSTRUCTION;
34997
+ }
34998
+ }
34655
34999
  /** The scheduler seam: `queue-wait` closes when a slot buys a session. */
34656
35000
  lifecycle(trace) {
34657
35001
  return {
@@ -34757,10 +35101,11 @@ var MonolithCornerTurnLoop = class {
34757
35101
  throw new Error("corner turn stopped for daemon handoff");
34758
35102
  this.busy = true;
34759
35103
  await this.syncBranch();
34760
- const [conversation, roster, delivered] = await trace.measure("context-fetch", () => Promise.all([
35104
+ const [conversation, roster, delivered, activeReviewerInstruction] = await trace.measure("context-fetch", () => Promise.all([
34761
35105
  api.execute("getRoomConversation", { roomId: cornerId, limit: 200 }),
34762
35106
  this.roster(),
34763
- 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([]),
35108
+ this.activeReviewerInstruction()
34764
35109
  ]));
34765
35110
  const names = new Map(roster.members.map((member) => [member.identityId, member.name]));
34766
35111
  const requestedBy = requester && !requester.name && names.get(requester.pubkey) ? { ...requester, name: names.get(requester.pubkey) } : requester;
@@ -34779,13 +35124,14 @@ var MonolithCornerTurnLoop = class {
34779
35124
  ${this.options.objective}`,
34780
35125
  WarmTranscript.render(this.warmTranscript.select(this.sessionId, transcriptRows), "Corner transcript:", "New in the corner since your last turn (the earlier transcript is already in this session):"),
34781
35126
  roomMentionDirectory(roster, this.agent.publicKey),
35127
+ activeReviewerInstruction,
34782
35128
  [
34783
35129
  ...sourceMessageId ? [`Reaction target message id: ${sourceMessageId}`] : [],
34784
35130
  `Newest trigger:
34785
35131
  ${trigger}`,
34786
35132
  ...attachmentPromptLines(attachments, delivered, this.acceptsImages())
34787
35133
  ].join("\n"),
34788
- this.options.repository ? "Continue the objective. Obey the PR checks and human hold rules in your session instructions." : "Continue the objective. Attach completed files before calling close_corner.",
35134
+ this.options.repository ? "Continue the objective. Obey the PR checks and human hold rules in your session instructions." : "Continue the objective. Attach completed files; only a human can close this corner.",
34789
35135
  MAINTAIN_ASSIGNED_IDENTITY_DIRECTIVE
34790
35136
  ].filter(Boolean).join("\n\n");
34791
35137
  const stream = new AgentTurnStream({
@@ -34926,7 +35272,9 @@ ${trigger}`,
34926
35272
  this.lastDeliveryNudgeState = deliveryState;
34927
35273
  replyBeforeNudge = durableReplyText(result.agentText);
34928
35274
  await flushToolCalls(result.toolCalls, "");
34929
- result = await runPrompt(this.reviewerHandle ? this.cornerTurnEndNudge : checksTurn ? CORNER_YOLO_MERGE_NUDGE : CORNER_DELIVERY_NUDGE);
35275
+ if (this.reviewerInstructionInput)
35276
+ await this.syncBranch();
35277
+ result = await runPrompt(this.reviewerHandle ? await this.activeReviewerInstruction() ?? this.cornerTurnEndNudge : checksTurn ? CORNER_YOLO_MERGE_NUDGE : CORNER_DELIVERY_NUDGE);
34930
35278
  trace.promptSettled();
34931
35279
  explained = await this.explainEmpty(result);
34932
35280
  }
@@ -35063,6 +35411,8 @@ ${trigger}`,
35063
35411
  onPoll: () => this.options.onPoll(),
35064
35412
  onError: (error) => console.error("[thin-core] corner command failed", error),
35065
35413
  stop: (requestId) => this.stopTurn(requestId),
35414
+ restart: () => this.options.onRestartRequested?.(),
35415
+ canStartTurn: this.options.canStartTurn,
35066
35416
  closed: async () => {
35067
35417
  if (this.closePushed) {
35068
35418
  this.closePushed = false;
@@ -35543,6 +35893,10 @@ var DiscoveryWakes = class {
35543
35893
  function shouldPostInitialCornerWorkingState(restore, isOpener = true) {
35544
35894
  return isOpener && !restore.featureBranch && !restore.lifecycle?.branch && !restore.lifecycle?.pr;
35545
35895
  }
35896
+ function cornerBranchIsSafeToDelete(lifecycle) {
35897
+ const pr = lifecycle?.pr;
35898
+ return !pr || Boolean(pr.mergedAt);
35899
+ }
35546
35900
  function cornerStartConfigKey(repository, objective) {
35547
35901
  return JSON.stringify({
35548
35902
  resolution: repository.resolution,
@@ -35583,7 +35937,7 @@ async function materializeCornerWorktree(input) {
35583
35937
  `+refs/heads/${input.featureBranch}:refs/remotes/origin/${input.featureBranch}`
35584
35938
  ], { env: authEnv, maxBuffer: 4 * 1024 * 1024 }).then(() => true, () => false);
35585
35939
  if (!existsSync6(resolve24(path, ".git"))) {
35586
- await rm5(path, { recursive: true, force: true });
35940
+ await rm6(path, { recursive: true, force: true });
35587
35941
  await execFileAsync5("git", [
35588
35942
  `--git-dir=${gitCommonDir}`,
35589
35943
  "worktree",
@@ -35651,7 +36005,8 @@ async function mapWithConcurrency(values, limit, visit) {
35651
36005
  }
35652
36006
  }));
35653
36007
  }
35654
- async function removeCornerWorktreeAndBranches(worktree) {
36008
+ async function removeCornerWorktreeAndBranches(worktree, options = {}) {
36009
+ const preserveRemoteBranch = options.preserveRemoteBranch === true;
35655
36010
  const localRef = `refs/heads/${worktree.branch}`;
35656
36011
  const remoteRef = `refs/heads/${worktree.branch}`;
35657
36012
  const worktreeExists = existsSync6(worktree.path);
@@ -35675,6 +36030,8 @@ async function removeCornerWorktreeAndBranches(worktree) {
35675
36030
  worktree.path
35676
36031
  ]);
35677
36032
  } else if (!localExists) {
36033
+ if (preserveRemoteBranch)
36034
+ return;
35678
36035
  await deleteExactRemoteBranch(worktree.gitCommonDir, remoteRef, worktree.token, {
35679
36036
  requireAbsent: true
35680
36037
  });
@@ -35685,7 +36042,9 @@ async function removeCornerWorktreeAndBranches(worktree) {
35685
36042
  throw new Error(`refusing to delete repository HEAD ${localRef}`);
35686
36043
  }
35687
36044
  }
35688
- await deleteExactRemoteBranch(worktree.gitCommonDir, remoteRef, worktree.token);
36045
+ if (!preserveRemoteBranch) {
36046
+ await deleteExactRemoteBranch(worktree.gitCommonDir, remoteRef, worktree.token);
36047
+ }
35689
36048
  if (await gitRefExists(worktree.gitCommonDir, localRef)) {
35690
36049
  await execFileAsync5("git", [
35691
36050
  `--git-dir=${worktree.gitCommonDir}`,
@@ -35697,13 +36056,13 @@ async function removeCornerWorktreeAndBranches(worktree) {
35697
36056
  ]);
35698
36057
  }
35699
36058
  }
35700
- var execFileAsync5 = promisify5(execFile7);
36059
+ var execFileAsync5 = promisify5(execFile8);
35701
36060
  async function removeCornerScratchWorkspace(input) {
35702
36061
  const expected = resolve24(input.roomRoot, "scratch");
35703
36062
  if (resolve24(input.scratchPath) !== expected) {
35704
36063
  throw new Error(`refusing to remove scratch outside corner ${input.cornerId}`);
35705
36064
  }
35706
- await rm5(expected, { recursive: true, force: true });
36065
+ await rm6(expected, { recursive: true, force: true });
35707
36066
  }
35708
36067
  var RoomRuntimeCoordinator = class {
35709
36068
  configPath;
@@ -35743,6 +36102,7 @@ var RoomRuntimeCoordinator = class {
35743
36102
  workspaceRemovalConfirmations = 0;
35744
36103
  roomRemovalConfirmations = /* @__PURE__ */ new Map();
35745
36104
  confirmationPending = false;
36105
+ restartRequested = false;
35746
36106
  /** Unscoped agent-directed discovery wakes (#1369), counted instead of
35747
36107
  * flagged: a wake that arrives while a reconcile is already running must
35748
36108
  * survive its start-pass clearing, or a reconnect mid-reconcile waits a
@@ -35784,6 +36144,7 @@ var RoomRuntimeCoordinator = class {
35784
36144
  });
35785
36145
  this.options.daemonApi.setConfigChangedListener?.(() => {
35786
36146
  void this.scheduler.suspendIdle().catch((error) => console.error("[body] config-change session restart failed", error));
36147
+ void Promise.resolve(this.options.onConfigChanged?.()).catch((error) => console.error("[body] config-change catalog refresh failed", error));
35787
36148
  });
35788
36149
  this.options.daemonApi.setHiccupRestartListener?.((attempt) => {
35789
36150
  this.options.onHiccupRestart?.(attempt);
@@ -35834,6 +36195,12 @@ var RoomRuntimeCoordinator = class {
35834
36195
  count += 1;
35835
36196
  return count;
35836
36197
  }
36198
+ requestLifecycleRestart() {
36199
+ if (this.restartRequested)
36200
+ return;
36201
+ this.restartRequested = true;
36202
+ this.options.onRestartRequested?.();
36203
+ }
35837
36204
  quiesceForUpdateIfIdle() {
35838
36205
  if (!this.isWorkspaceIdle())
35839
36206
  return false;
@@ -36093,7 +36460,9 @@ var RoomRuntimeCoordinator = class {
36093
36460
  },
36094
36461
  onCornerOpened: () => {
36095
36462
  this.confirmationPending = true;
36096
- }
36463
+ },
36464
+ onRestartRequested: () => this.requestLifecycleRestart(),
36465
+ canStartTurn: () => !this.restartRequested
36097
36466
  });
36098
36467
  const promise = loop.run().catch((error) => {
36099
36468
  if (!controller.signal.aborted)
@@ -36245,7 +36614,9 @@ var RoomRuntimeCoordinator = class {
36245
36614
  branch: featureBranch,
36246
36615
  parentRoomId: corner.parentRoomId,
36247
36616
  token: ""
36248
- }) : this.reapCornerScratch({ path: workspacePath, cornerId: corner.cornerId })
36617
+ }) : this.reapCornerScratch({ path: workspacePath, cornerId: corner.cornerId }),
36618
+ onRestartRequested: () => this.requestLifecycleRestart(),
36619
+ canStartTurn: () => !this.restartRequested
36249
36620
  });
36250
36621
  const promise = loop.run().catch((error) => {
36251
36622
  if (!controller.signal.aborted) {
@@ -36351,25 +36722,44 @@ var RoomRuntimeCoordinator = class {
36351
36722
  }
36352
36723
  async reapCornerWorktree(worktree) {
36353
36724
  try {
36725
+ const preserveRemoteBranch = !await this.cornerBranchMayBeDeleted(worktree.cornerId);
36354
36726
  const parentRoomId = worktree.parentRoomId;
36355
- if (!parentRoomId) {
36727
+ if (!parentRoomId && !preserveRemoteBranch) {
36356
36728
  throw new Error(`corner ${worktree.cornerId} has no parent Room for GitHub token`);
36357
36729
  }
36358
- const token = (await this.options.daemonApi.execute("getRoomGitHubToken", { roomId: parentRoomId })).token;
36359
- await removeCornerWorktreeAndBranches({ ...worktree, token });
36360
- await this.options.daemonApi.execute("postCornerRemoteState", {
36361
- cornerId: worktree.cornerId,
36362
- branch: worktree.branch,
36363
- state: "gone",
36364
- checks: "unknown"
36365
- });
36730
+ const token = preserveRemoteBranch ? "" : (await this.options.daemonApi.execute("getRoomGitHubToken", {
36731
+ roomId: parentRoomId
36732
+ })).token;
36733
+ await removeCornerWorktreeAndBranches({ ...worktree, token }, { preserveRemoteBranch });
36366
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
+ }
36367
36743
  } catch (error) {
36368
36744
  this.pendingCornerReaps.set(worktree.cornerId, { ...worktree, token: "" });
36369
36745
  this.confirmationPending = true;
36370
36746
  throw error;
36371
36747
  }
36372
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
+ }
36373
36763
  async reapCornerScratch(scratch) {
36374
36764
  await removeCornerScratchWorkspace({
36375
36765
  cornerId: scratch.cornerId,
@@ -36608,13 +36998,13 @@ import { mkdir as mkdir17, rename as rename5 } from "node:fs/promises";
36608
36998
  import { dirname as dirname15, resolve as resolve26 } from "node:path";
36609
36999
 
36610
37000
  // apps/body/dist/systemd.js
36611
- import { execFile as execFile8 } from "node:child_process";
37001
+ import { execFile as execFile9 } from "node:child_process";
36612
37002
  import { mkdir as mkdir16, readFile as readFile10, stat as stat3, writeFile as writeFile10 } from "node:fs/promises";
36613
37003
  import { homedir as homedir11 } from "node:os";
36614
37004
  import { dirname as dirname14, resolve as resolve25 } from "node:path";
36615
37005
  import { setTimeout as sleep } from "node:timers/promises";
36616
37006
  import { promisify as promisify6 } from "node:util";
36617
- var execFileAsync6 = promisify6(execFile8);
37007
+ var execFileAsync6 = promisify6(execFile9);
36618
37008
  var DELIBERATE_REMOVAL_EXIT_STATUS = 78;
36619
37009
  var DAEMON_DISTRESS_EXIT_STATUS = 77;
36620
37010
  var UNKNOWN_AGENT_EXIT_STATUS = 79;
@@ -36883,8 +37273,8 @@ init_self_update_manifest();
36883
37273
 
36884
37274
  // apps/body/dist/managed-update.js
36885
37275
  init_self_update();
36886
- import { spawn as spawn9 } from "node:child_process";
36887
- 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";
36888
37278
  import { dirname as dirname18, resolve as resolve29 } from "node:path";
36889
37279
 
36890
37280
  // apps/body/dist/update-rollback-alert.js
@@ -36977,7 +37367,7 @@ ${now2()}
36977
37367
  throw error;
36978
37368
  const age = now2() - await stat4(lock).then((value) => value.mtimeMs).catch(() => now2());
36979
37369
  if (age > LOCK_STALE_MS) {
36980
- await rm7(lock, { recursive: true, force: true });
37370
+ await rm8(lock, { recursive: true, force: true });
36981
37371
  continue;
36982
37372
  }
36983
37373
  if (now2() >= deadline)
@@ -36988,7 +37378,7 @@ ${now2()}
36988
37378
  try {
36989
37379
  return await work();
36990
37380
  } finally {
36991
- await rm7(lock, { recursive: true, force: true });
37381
+ await rm8(lock, { recursive: true, force: true });
36992
37382
  }
36993
37383
  }
36994
37384
  var ManagedUpdateHandoff = class _ManagedUpdateHandoff {
@@ -37298,7 +37688,7 @@ async function runManagedUpdateWorkerProcess() {
37298
37688
  if (!entrypoint)
37299
37689
  throw new Error("cannot resolve the current Beeline entrypoint");
37300
37690
  await new Promise((resolveWorker, rejectWorker) => {
37301
- const child = spawn9(process.execPath, [entrypoint, "managed-update-worker"], {
37691
+ const child = spawn10(process.execPath, [entrypoint, "managed-update-worker"], {
37302
37692
  detached: true,
37303
37693
  env: { ...process.env, BEELINE_INTERNAL_UPDATE_WORKER: "1" },
37304
37694
  stdio: ["ignore", "pipe", "pipe"]
@@ -37457,6 +37847,32 @@ async function rollbackFailedSuccessor(layout, runtimeDir, cause = {}) {
37457
37847
  }
37458
37848
 
37459
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
+ }
37460
37876
  function updateUsage() {
37461
37877
  console.log(`
37462
37878
  ${import_picocolors.default.bold("Update the installed Beeline bundle from the published manifest.")}
@@ -37558,6 +37974,8 @@ async function runUpdateCommand(args) {
37558
37974
  await printStatus(layout);
37559
37975
  return;
37560
37976
  }
37977
+ if (!checkOnly)
37978
+ await refreshRuntimeHarnessAdapters();
37561
37979
  const installed = await readInstalledBundleIdentity(layout);
37562
37980
  console.log(`[beeline] installed bundle: ${describeIdentity(installed)}`);
37563
37981
  const raw = await fetch(manifestUrl, { signal: AbortSignal.timeout(3e4) }).then((response) => {
@@ -37724,7 +38142,7 @@ async function runStartCommand(args, interactiveUi, dependencyOverrides = {}) {
37724
38142
 
37725
38143
  // apps/body/dist/connect-command.js
37726
38144
  import { spawn as spawn11 } from "node:child_process";
37727
- import { createHash as createHash9, randomUUID as randomUUID6 } from "node:crypto";
38145
+ import { createHash as createHash9, randomUUID as randomUUID7 } from "node:crypto";
37728
38146
  import { chmod as chmod7, mkdir as mkdir21, readFile as readFile13, unlink as unlink5, writeFile as writeFile14 } from "node:fs/promises";
37729
38147
  import { homedir as homedir13, hostname as hostname2 } from "node:os";
37730
38148
  import { dirname as dirname20, resolve as resolve30 } from "node:path";
@@ -37811,14 +38229,13 @@ async function verifyProviderKey(input) {
37811
38229
  }
37812
38230
 
37813
38231
  // apps/body/dist/pair-agent-selection.js
37814
- import { spawn as spawn10 } from "node:child_process";
37815
38232
  import { stdin as stdin2, stdout as stdout3 } from "node:process";
37816
38233
  var NO_AGENT_MESSAGE = `No supported ACP-capable coding agent was detected.
37817
38234
  Install one of these supported agents:
37818
- codex npm install -g @openai/codex @agentclientprotocol/codex-acp
37819
- 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
37820
38237
  goose https://block.github.io/goose/docs/getting-started/installation/
37821
- pi npm install -g @mariozechner/pi-coding-agent pi-acp
38238
+ pi npm install -g @mariozechner/pi-coding-agent@latest pi-acp@latest
37822
38239
  grok curl -fsSL https://x.ai/cli/install.sh | bash
37823
38240
  cursor Install Cursor Agent CLI from https://cursor.com/docs/cli
37824
38241
  Then retry, or explicitly use \`--agent reference\` with an LLM key.
@@ -37839,21 +38256,7 @@ async function clackSelectAgent(candidates) {
37839
38256
  return unwrapPrompt(picked, "Pairing cancelled.");
37840
38257
  }
37841
38258
  async function installAdapter(install, opts) {
37842
- await new Promise((resolveInstall, rejectInstall) => {
37843
- const child = spawn10(install.command, install.args, {
37844
- cwd: opts.cwd,
37845
- env: opts.env ?? process.env,
37846
- stdio: "inherit"
37847
- });
37848
- child.once("error", rejectInstall);
37849
- child.once("exit", (code, signal) => {
37850
- if (code === 0)
37851
- resolveInstall();
37852
- else {
37853
- rejectInstall(new Error(signal ? `${install.command} was terminated by ${signal}` : `${install.command} exited with status ${code ?? "unknown"}`));
37854
- }
37855
- });
37856
- });
38259
+ await runAdapterInstall(install, opts);
37857
38260
  }
37858
38261
  function errorMessage(error) {
37859
38262
  return error instanceof Error ? error.message : String(error);
@@ -37867,14 +38270,38 @@ async function selectPairAgentCommand(opts) {
37867
38270
  const selectAgent = opts.selectAgent ?? clackSelectAgent;
37868
38271
  const confirmInstall = opts.confirmInstall ?? clackConfirmInstall;
37869
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
+ };
37870
38296
  if (opts.explicitKind !== void 0) {
37871
38297
  try {
37872
- return resolveAgentCommand({
38298
+ const selected = resolveAgentCommand({
37873
38299
  kind: opts.explicitKind,
37874
38300
  customCommand: opts.customCommand,
37875
38301
  env: opts.env,
37876
38302
  cwd: opts.cwd
37877
38303
  });
38304
+ return await useReadyAgent(selected, "selected");
37878
38305
  } catch (resolutionError) {
37879
38306
  const candidate = detectInstalledAgentCommands({ env: opts.env, cwd: opts.cwd }).find((detected2) => detected2.kind === opts.explicitKind && detected2.status === "missing-adapter");
37880
38307
  if (!candidate || candidate.status !== "missing-adapter")
@@ -37920,25 +38347,19 @@ async function selectPairAgentCommand(opts) {
37920
38347
  throw new Error("Installed coding agents need ACP adapter setup; install an adapter above, then retry.");
37921
38348
  }
37922
38349
  if (ready.length === 1) {
37923
- output.write(`[beeline] using ${ready[0].kind} (auto-detected)
37924
- `);
37925
- return ready[0].agent;
38350
+ return useReadyAgent(ready[0].agent, "auto-detected");
37926
38351
  }
37927
38352
  const readyKinds = ready.map((candidate) => candidate.kind).join(", ");
37928
38353
  throw new Error(`Detected multiple ACP-capable coding agents: ${readyKinds}. This session is non-interactive; pass \`--agent <name>\` explicitly.`);
37929
38354
  }
37930
38355
  if (detected.length === 1 && detected[0].status === "ready") {
37931
- output.write(`[beeline] using ${detected[0].kind} (auto-detected)
37932
- `);
37933
- return detected[0].agent;
38356
+ return useReadyAgent(detected[0].agent, "auto-detected");
37934
38357
  }
37935
38358
  while (true) {
37936
38359
  const pickedKind = await selectAgent(detected);
37937
38360
  const selected = detected.find((candidate) => candidate.kind === pickedKind);
37938
38361
  if (selected.status === "ready") {
37939
- output.write(`[beeline] using ${selected.kind} (selected)
37940
- `);
37941
- return selected.agent;
38362
+ return useReadyAgent(selected.agent, "selected");
37942
38363
  }
37943
38364
  const manual = missingAdapterMessage(selected);
37944
38365
  output.write(`[beeline] installing ${selected.kind} adapter: ${formatAdapterInstallCommand(selected.install)}
@@ -38157,10 +38578,23 @@ function connectModelPickerFromAxes(axes, fallbackModel, harness, rawAxes = axes
38157
38578
  note: `${harness} did not enumerate models; offering the provider default`
38158
38579
  };
38159
38580
  }
38160
- async function loadConnectModelCatalog(input) {
38161
- 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);
38162
38596
  const model = input.model ?? defaultConnectModel(input.harness, input.provider);
38163
- const catalog = await fetchAgentModelCatalog(agent, providerEnvironment({
38597
+ const catalog = await (dependencies.fetchCatalog ?? fetchAgentModelCatalog)(agent, providerEnvironment({
38164
38598
  harness: input.harness,
38165
38599
  ...input.provider ? { provider: input.provider } : {},
38166
38600
  ...input.apiKey ? { apiKey: input.apiKey } : {},
@@ -38211,8 +38645,6 @@ async function collectConnectWizard(prompts = clackPrompts, loadModels = loadCon
38211
38645
  return (picked ?? "").trim();
38212
38646
  };
38213
38647
  const askEffort = async (catalog2, forHarness, model2, forProvider, forKey) => {
38214
- if (!catalog2.effort)
38215
- return void 0;
38216
38648
  let axis = catalog2.effort;
38217
38649
  if (model2 !== catalog2.currentValue) {
38218
38650
  const reread = await loadModels({
@@ -38224,7 +38656,7 @@ async function collectConnectWizard(prompts = clackPrompts, loadModels = loadCon
38224
38656
  }).catch(() => void 0);
38225
38657
  axis = reread?.effort ?? axis;
38226
38658
  }
38227
- if (!axis.options.length)
38659
+ if (!axis?.options.length)
38228
38660
  return void 0;
38229
38661
  const picked = await prompts.select({
38230
38662
  message: brass("Choose reasoning effort"),
@@ -38368,7 +38800,7 @@ async function readMachineId(env = process.env) {
38368
38800
  if (!/^[0-9a-f-]{32,}$/.test(machineId))
38369
38801
  throw new Error("invalid persisted machine id");
38370
38802
  } catch {
38371
- machineId = randomUUID6();
38803
+ machineId = randomUUID7();
38372
38804
  try {
38373
38805
  await mkdir21(configDir, { recursive: true, mode: 448 });
38374
38806
  await writeFile14(machineIdPath, `${machineId}
@@ -38648,7 +39080,7 @@ async function runConnectFinishCommand(path) {
38648
39080
  init_self_update();
38649
39081
 
38650
39082
  // apps/body/dist/daemon-failure.js
38651
- 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";
38652
39084
  import { dirname as dirname21, resolve as resolve31 } from "node:path";
38653
39085
  var DAEMON_FAILURE_LIMIT = 3;
38654
39086
  var DAEMON_FAILURE_WINDOW_MS = 5 * 6e4;
@@ -38688,11 +39120,11 @@ async function recordDaemonStartFailure(runtimeDir, error, now2 = Date.now()) {
38688
39120
  return { distressed, count: failures.length, path: daemonFailurePath(runtimeDir) };
38689
39121
  }
38690
39122
  async function clearDaemonStartFailures(runtimeDir) {
38691
- await rm8(daemonFailurePath(runtimeDir), { force: true });
39123
+ await rm9(daemonFailurePath(runtimeDir), { force: true });
38692
39124
  }
38693
39125
 
38694
39126
  // apps/body/dist/update-functional-probe.js
38695
- import { mkdir as mkdir23, rm as rm9 } from "node:fs/promises";
39127
+ import { mkdir as mkdir23, rm as rm10 } from "node:fs/promises";
38696
39128
  import { homedir as homedir14 } from "node:os";
38697
39129
  import { resolve as resolve32 } from "node:path";
38698
39130
  var UPDATE_PROBE_SESSION_TIMEOUT_MS = 1e4;
@@ -38817,7 +39249,7 @@ async function runUpdateFunctionalProbe(input) {
38817
39249
  const root = input.probeRoot ?? resolve32(input.runtimeDir, "update-functional-probe");
38818
39250
  const cwd = resolve32(root, "checkout");
38819
39251
  const homeRoot = resolve32(root, "agent-home");
38820
- await rm9(root, { recursive: true, force: true });
39252
+ await rm10(root, { recursive: true, force: true });
38821
39253
  await mkdir23(cwd, { recursive: true, mode: 448 });
38822
39254
  let client;
38823
39255
  try {
@@ -38992,13 +39424,13 @@ async function runUpdateFunctionalProbe(input) {
38992
39424
  };
38993
39425
  } finally {
38994
39426
  await client?.stop().catch(() => void 0);
38995
- await rm9(root, { recursive: true, force: true }).catch(() => void 0);
39427
+ await rm10(root, { recursive: true, force: true }).catch(() => void 0);
38996
39428
  }
38997
39429
  }
38998
39430
 
38999
39431
  // apps/body/dist/current-release-probe.js
39000
39432
  import { spawn as spawn12 } from "node:child_process";
39001
- import { dirname as dirname22, join as join14 } from "node:path";
39433
+ import { dirname as dirname22, join as join15 } from "node:path";
39002
39434
  init_self_update();
39003
39435
  var CURRENT_RELEASE_PROBE_TIMEOUT_MS = 12e4;
39004
39436
  var UPDATE_PROBE_COMMAND = "update-probe";
@@ -39041,7 +39473,7 @@ function outcomeFromReport(report) {
39041
39473
  }
39042
39474
  }
39043
39475
  async function probeReleaseInSubprocess(input) {
39044
- const bundleDir = join14(input.layout.releasesRoot, input.releaseId);
39476
+ const bundleDir = join15(input.layout.releasesRoot, input.releaseId);
39045
39477
  const entrypoint = await resolveBundleEntrypoint(bundleDir);
39046
39478
  if (!entrypoint) {
39047
39479
  return { kind: "unavailable", reason: `release ${input.releaseId} has no runnable CLI entrypoint` };
@@ -39104,7 +39536,7 @@ async function runUpdateProbeCommand(args, options = {}) {
39104
39536
  const runtime = await readRuntimeRecord(configPath);
39105
39537
  const agent = runtimeAgentCommand(runtime);
39106
39538
  const config = loadBodyConfig({
39107
- workspaceRoot: join14(dirname22(configPath), "workspace"),
39539
+ workspaceRoot: join15(dirname22(configPath), "workspace"),
39108
39540
  llmEnvFile: runtime.llmEnvFile,
39109
39541
  env: { ...env, BUZZ_AGENT_BIN: agent.command, BUZZ_DEV_MCP_BIN: runtime.mcpBinary },
39110
39542
  agent
@@ -39130,7 +39562,7 @@ async function runUpdateProbeCommand(args, options = {}) {
39130
39562
  sandboxRequired: runtime.sandbox !== "off",
39131
39563
  sandboxUnavailableDetail: sandbox.advisory,
39132
39564
  // The successor's probe still holds `<runtimeDir>/update-functional-probe`.
39133
- probeRoot: join14(runtimeDir, "current-release-probe")
39565
+ probeRoot: join15(runtimeDir, "current-release-probe")
39134
39566
  }));
39135
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 };
39136
39568
  write(JSON.stringify(report));
@@ -39303,6 +39735,21 @@ async function runStoredDaemon(pathOrPointer) {
39303
39735
  throw new Error("monolith daemon transport activation failed");
39304
39736
  runtime = activated.runtime;
39305
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
+ }
39306
39753
  const agent = runtimeAgentCommand(runtime);
39307
39754
  await writeDaemonPidRecord(configPath, process.pid);
39308
39755
  const env = {
@@ -39328,7 +39775,7 @@ async function runStoredDaemon(pathOrPointer) {
39328
39775
  if (runtime.sharedSkills)
39329
39776
  config.sharedSkills = [...runtime.sharedSkills];
39330
39777
  if (runtime.modelSelection) {
39331
- await applyRuntimeModelPreflight(config, agent, runtime.modelSelection);
39778
+ await applyRuntimeModelPreflight(config, agent, runtime.modelSelection, void 0, refreshRuntimeAdapter);
39332
39779
  if (!config.modelUnavailable) {
39333
39780
  console.log("[body] persisted model/effort selection passed live startup validation");
39334
39781
  } else {
@@ -39394,10 +39841,28 @@ async function runStoredDaemon(pathOrPointer) {
39394
39841
  scratchSweepTimer.unref();
39395
39842
  let ready = false;
39396
39843
  let connectorLoop;
39844
+ let catalogRefresh;
39845
+ const refreshCatalog = () => {
39846
+ catalogRefresh ??= syncAgentModelCatalog({
39847
+ api: daemonApi,
39848
+ agent,
39849
+ agentEnv: config.agentEnv,
39850
+ agentId: runtime.agent.publicKey,
39851
+ workspaceId: runtime.communityId,
39852
+ runtimeDir,
39853
+ ...runtime.modelSelection ? { runtimeSelection: runtime.modelSelection } : {},
39854
+ force: true
39855
+ }).then(() => void 0).finally(() => {
39856
+ catalogRefresh = void 0;
39857
+ });
39858
+ return catalogRefresh;
39859
+ };
39397
39860
  let stoppingStatus = "daemon stopped";
39398
39861
  try {
39862
+ let lifecycleRestartDrain;
39399
39863
  const core = new ThinDaemonCore(runtime, configPath, config, {
39400
39864
  daemonApi,
39865
+ onConfigChanged: refreshCatalog,
39401
39866
  onHiccupRestart: (attempt) => {
39402
39867
  const delay = hiccupBackoffMs(attempt);
39403
39868
  console.warn(`[thin-core] hiccup restart attempt ${attempt}; exiting so systemd can start a fresh helper`);
@@ -39407,6 +39872,30 @@ async function runStoredDaemon(pathOrPointer) {
39407
39872
  }
39408
39873
  const timer = setTimeout(() => process.exit(0), delay);
39409
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
+ });
39410
39899
  }
39411
39900
  });
39412
39901
  const updateDrain = update ? new ManagedUpdateDrain({