usebeeline 0.0.63 → 0.0.69

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 +2 -2
  2. package/dist/usebeeline.mjs +1412 -1395
  3. package/package.json +1 -1
@@ -2415,7 +2415,7 @@ var require_websocket = __commonJS({
2415
2415
  var http = __require("http");
2416
2416
  var net = __require("net");
2417
2417
  var tls = __require("tls");
2418
- var { randomBytes: randomBytes7, createHash: createHash7 } = __require("crypto");
2418
+ var { randomBytes: randomBytes7, createHash: createHash8 } = __require("crypto");
2419
2419
  var { Duplex, Readable } = __require("stream");
2420
2420
  var { URL: URL2 } = __require("url");
2421
2421
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -3083,7 +3083,7 @@ var require_websocket = __commonJS({
3083
3083
  abortHandshake(websocket, socket, "Invalid Upgrade header");
3084
3084
  return;
3085
3085
  }
3086
- const digest = createHash7("sha1").update(key + GUID).digest("base64");
3086
+ const digest = createHash8("sha1").update(key + GUID).digest("base64");
3087
3087
  if (res.headers["sec-websocket-accept"] !== digest) {
3088
3088
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
3089
3089
  return;
@@ -3452,7 +3452,7 @@ var require_websocket_server = __commonJS({
3452
3452
  var EventEmitter2 = __require("events");
3453
3453
  var http = __require("http");
3454
3454
  var { Duplex } = __require("stream");
3455
- var { createHash: createHash7 } = __require("crypto");
3455
+ var { createHash: createHash8 } = __require("crypto");
3456
3456
  var extension2 = require_extension();
3457
3457
  var PerMessageDeflate2 = require_permessage_deflate();
3458
3458
  var subprotocol2 = require_subprotocol();
@@ -3759,7 +3759,7 @@ var require_websocket_server = __commonJS({
3759
3759
  );
3760
3760
  }
3761
3761
  if (this._state > RUNNING) return abortHandshake(socket, 503);
3762
- const digest = createHash7("sha1").update(key + GUID).digest("base64");
3762
+ const digest = createHash8("sha1").update(key + GUID).digest("base64");
3763
3763
  const headers = [
3764
3764
  "HTTP/1.1 101 Switching Protocols",
3765
3765
  "Upgrade: websocket",
@@ -3984,12 +3984,12 @@ __export(self_update_exports, {
3984
3984
  writeUpdateAttemptFixture: () => writeUpdateAttemptFixture,
3985
3985
  writeUpdateState: () => writeUpdateState
3986
3986
  });
3987
- import { createHash as createHash5 } from "node:crypto";
3987
+ import { createHash as createHash6 } from "node:crypto";
3988
3988
  import { constants as fsConstants } from "node:fs";
3989
- import { access, chmod as chmod4, lstat as lstat2, mkdir as mkdir13, open, readFile as readFile9, rename as rename3, rm as rm5, symlink as symlink2, writeFile as writeFile9 } from "node:fs/promises";
3989
+ import { access, chmod as chmod4, lstat as lstat2, mkdir as mkdir14, open, readFile as readFile9, rename as rename3, rm as rm5, symlink as symlink2, writeFile as writeFile10 } from "node:fs/promises";
3990
3990
  import { spawn as spawn4 } from "node:child_process";
3991
3991
  import { homedir as homedir9 } from "node:os";
3992
- import { dirname as dirname9, join as join7, resolve as resolve21 } from "node:path";
3992
+ import { dirname as dirname10, join as join8, resolve as resolve21 } from "node:path";
3993
3993
  function anchorLayout(rawLibDir) {
3994
3994
  const libDir = resolve21(rawLibDir);
3995
3995
  const segments = libDir.split(/[/\\]/);
@@ -4028,7 +4028,7 @@ function discoveredBeelineInstallLayout(env = process.env) {
4028
4028
  return anchorLayout(explicitAnchor);
4029
4029
  const explicitBinDir = env.BEELINE_INSTALL_DIR?.trim();
4030
4030
  if (explicitBinDir)
4031
- return anchorLayout(resolve21(dirname9(resolve21(explicitBinDir)), "lib", "beeline"));
4031
+ return anchorLayout(resolve21(dirname10(resolve21(explicitBinDir)), "lib", "beeline"));
4032
4032
  return defaultBeelineInstallLayout(env);
4033
4033
  }
4034
4034
  function hostPlatformKey() {
@@ -4039,7 +4039,7 @@ function hostPlatformKey() {
4039
4039
  return `${os}-${arch}`;
4040
4040
  }
4041
4041
  function bundleJsonCandidates(bundleDir) {
4042
- return [join7(bundleDir, "lib", "beeline", "bundle.json"), join7(bundleDir, "bundle.json")];
4042
+ return [join8(bundleDir, "lib", "beeline", "bundle.json"), join8(bundleDir, "bundle.json")];
4043
4043
  }
4044
4044
  async function readBundleJson(bundleDir) {
4045
4045
  let raw;
@@ -4063,7 +4063,7 @@ async function readBundleJson(bundleDir) {
4063
4063
  }
4064
4064
  }
4065
4065
  function updateStatePath(layout) {
4066
- return join7(layout.releasesRoot, ".state", "update-state.json");
4066
+ return join8(layout.releasesRoot, ".state", "update-state.json");
4067
4067
  }
4068
4068
  async function readUpdateState(layout) {
4069
4069
  try {
@@ -4073,8 +4073,8 @@ async function readUpdateState(layout) {
4073
4073
  }
4074
4074
  }
4075
4075
  async function writeUpdateState(layout, state) {
4076
- await mkdir13(join7(layout.releasesRoot, ".state"), { recursive: true });
4077
- await writeFile9(updateStatePath(layout), `${JSON.stringify(state, null, 2)}
4076
+ await mkdir14(join8(layout.releasesRoot, ".state"), { recursive: true });
4077
+ await writeFile10(updateStatePath(layout), `${JSON.stringify(state, null, 2)}
4078
4078
  `, "utf8");
4079
4079
  }
4080
4080
  async function readInstalledBundleIdentity(layout, _state = {}) {
@@ -4155,7 +4155,7 @@ function run(command, args, timeoutMs) {
4155
4155
  });
4156
4156
  }
4157
4157
  function entrypointCandidates(bundleDir) {
4158
- return [join7(bundleDir, BUNDLE_ENTRYPOINT), join7(bundleDir, "beeline-cli.mjs")];
4158
+ return [join8(bundleDir, BUNDLE_ENTRYPOINT), join8(bundleDir, "beeline-cli.mjs")];
4159
4159
  }
4160
4160
  async function resolveBundleEntrypoint(bundleDir) {
4161
4161
  for (const candidate of entrypointCandidates(bundleDir)) {
@@ -4177,8 +4177,8 @@ async function stageRelease(layout, manifestUrl, published, opts = {}) {
4177
4177
  const fetchImpl = opts.fetchImpl ?? fetch;
4178
4178
  const log2 = opts.logger ?? ((line) => console.log(`[body] self-update: ${line}`));
4179
4179
  const releaseId = sanitizeReleaseId(published.commit ?? published.version ?? `release-${Date.now()}`);
4180
- const releaseDir = join7(layout.releasesRoot, releaseId);
4181
- const okMarker = join7(releaseDir, ".stage-ok");
4180
+ const releaseDir = join8(layout.releasesRoot, releaseId);
4181
+ const okMarker = join8(releaseDir, ".stage-ok");
4182
4182
  let previouslyVerified = false;
4183
4183
  try {
4184
4184
  const recorded = await readFile9(okMarker, "utf8");
@@ -4187,8 +4187,8 @@ async function stageRelease(layout, manifestUrl, published, opts = {}) {
4187
4187
  previouslyVerified = true;
4188
4188
  } catch {
4189
4189
  }
4190
- await mkdir13(releaseDir, { recursive: true });
4191
- const tempArchive = join7(layout.releasesRoot, `.download-${releaseId}-${process.pid}.tar.gz`);
4190
+ await mkdir14(releaseDir, { recursive: true });
4191
+ const tempArchive = join8(layout.releasesRoot, `.download-${releaseId}-${process.pid}.tar.gz`);
4192
4192
  try {
4193
4193
  log2(`downloading ${published.file}`);
4194
4194
  const response = await fetchImpl(archiveUrlFor(manifestUrl, published.file), {
@@ -4197,7 +4197,7 @@ async function stageRelease(layout, manifestUrl, published, opts = {}) {
4197
4197
  if (!response.ok || !response.body) {
4198
4198
  throw new Error(`downloading ${published.file} failed: HTTP ${response.status}`);
4199
4199
  }
4200
- const hash = createHash5("sha256");
4200
+ const hash = createHash6("sha256");
4201
4201
  const chunks = [];
4202
4202
  for await (const chunk of response.body) {
4203
4203
  const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
@@ -4208,7 +4208,7 @@ async function stageRelease(layout, manifestUrl, published, opts = {}) {
4208
4208
  if (actual !== published.sha256.toLowerCase()) {
4209
4209
  throw new Error(`checksum mismatch for ${published.file}: expected ${published.sha256}, got ${actual} \u2014 aborting without touching the installed bundle`);
4210
4210
  }
4211
- await writeFile9(tempArchive, Buffer.concat(chunks), { mode: 384 });
4211
+ await writeFile10(tempArchive, Buffer.concat(chunks), { mode: 384 });
4212
4212
  const entries = (await new Promise((resolveList, rejectList) => {
4213
4213
  const child = spawn4("tar", ["-tzf", tempArchive], { stdio: ["ignore", "pipe", "inherit"] });
4214
4214
  let out = "";
@@ -4228,18 +4228,18 @@ async function stageRelease(layout, manifestUrl, published, opts = {}) {
4228
4228
  throw new Error(`extracting bundle failed: ${extract2.stderr}`);
4229
4229
  for (const relative3 of requiredBundlePaths()) {
4230
4230
  try {
4231
- await access(join7(releaseDir, relative3), fsConstants.F_OK);
4231
+ await access(join8(releaseDir, relative3), fsConstants.F_OK);
4232
4232
  } catch {
4233
4233
  throw new Error(`staged bundle is missing ${relative3}`);
4234
4234
  }
4235
4235
  }
4236
4236
  if (opts.smokeTestCli !== false) {
4237
- const probe = await run(process.execPath, [join7(releaseDir, BUNDLE_ENTRYPOINT), "--version"], 6e4);
4237
+ const probe = await run(process.execPath, [join8(releaseDir, BUNDLE_ENTRYPOINT), "--version"], 6e4);
4238
4238
  if (probe.status !== 0) {
4239
4239
  throw new Error(`staged bundle failed its startup smoke test (--version exited ${probe.status})${probe.stderr ? `: ${probe.stderr.trim()}` : ""}`);
4240
4240
  }
4241
4241
  }
4242
- await writeFile9(okMarker, `${published.sha256}
4242
+ await writeFile10(okMarker, `${published.sha256}
4243
4243
  `, "utf8");
4244
4244
  log2(`staged release ${releaseId} (sha256 verified)`);
4245
4245
  return releaseId;
@@ -4267,26 +4267,26 @@ function forwarderScript(tool) {
4267
4267
  }
4268
4268
  async function replaceFile(path, contents, mode) {
4269
4269
  const temp = `${path}.new-${process.pid}`;
4270
- await writeFile9(temp, contents, { mode });
4270
+ await writeFile10(temp, contents, { mode });
4271
4271
  await chmod4(temp, mode);
4272
4272
  await rename3(temp, path);
4273
4273
  }
4274
4274
  async function activateRelease(layout, releaseId) {
4275
- const releaseDir = join7(layout.releasesRoot, releaseId);
4276
- await access(join7(releaseDir, BUNDLE_ENTRYPOINT), fsConstants.F_OK);
4277
- await mkdir13(layout.releasesRoot, { recursive: true });
4278
- await mkdir13(layout.binDir, { recursive: true });
4275
+ const releaseDir = join8(layout.releasesRoot, releaseId);
4276
+ await access(join8(releaseDir, BUNDLE_ENTRYPOINT), fsConstants.F_OK);
4277
+ await mkdir14(layout.releasesRoot, { recursive: true });
4278
+ await mkdir14(layout.binDir, { recursive: true });
4279
4279
  let previousReleaseId = await activeReleaseId(layout);
4280
4280
  const kind = await pathKind(layout.libDir);
4281
4281
  if (kind === "directory") {
4282
4282
  const legacyIdentity = await readBundleJson(layout.libDir);
4283
4283
  const legacyId = sanitizeReleaseId(legacyIdentity?.commit ?? legacyIdentity?.version ?? `legacy-${Date.now()}`);
4284
- const legacyDir = join7(layout.releasesRoot, legacyId);
4284
+ const legacyDir = join8(layout.releasesRoot, legacyId);
4285
4285
  try {
4286
4286
  await access(legacyDir, fsConstants.F_OK);
4287
4287
  previousReleaseId = `${legacyId}-${Date.now()}`;
4288
- await rename3(layout.libDir, join7(layout.releasesRoot, previousReleaseId));
4289
- await normalizeLegacyBundleShape(join7(layout.releasesRoot, previousReleaseId));
4288
+ await rename3(layout.libDir, join8(layout.releasesRoot, previousReleaseId));
4289
+ await normalizeLegacyBundleShape(join8(layout.releasesRoot, previousReleaseId));
4290
4290
  } catch {
4291
4291
  await rename3(layout.libDir, legacyDir);
4292
4292
  await normalizeLegacyBundleShape(legacyDir);
@@ -4295,18 +4295,18 @@ async function activateRelease(layout, releaseId) {
4295
4295
  }
4296
4296
  const tempLink = `${layout.libDir}.new-${process.pid}`;
4297
4297
  await rm5(tempLink, { force: true });
4298
- await symlink2(join7("beeline-releases", releaseId), tempLink);
4298
+ await symlink2(join8("beeline-releases", releaseId), tempLink);
4299
4299
  await rename3(tempLink, layout.libDir);
4300
- await fsyncDir(dirname9(layout.libDir));
4300
+ await fsyncDir(dirname10(layout.libDir));
4301
4301
  await writeBinForwarders(layout, releaseDir);
4302
4302
  return { previousReleaseId };
4303
4303
  }
4304
4304
  async function normalizeLegacyBundleShape(bundleDir) {
4305
- const innerLib = join7(bundleDir, "lib", "beeline");
4305
+ const innerLib = join8(bundleDir, "lib", "beeline");
4306
4306
  let anyFlat = false;
4307
4307
  for (const name of LEGACY_FLAT_BUNDLE_FILES) {
4308
4308
  try {
4309
- await access(join7(bundleDir, name), fsConstants.F_OK);
4309
+ await access(join8(bundleDir, name), fsConstants.F_OK);
4310
4310
  anyFlat = true;
4311
4311
  break;
4312
4312
  } catch {
@@ -4314,14 +4314,14 @@ async function normalizeLegacyBundleShape(bundleDir) {
4314
4314
  }
4315
4315
  if (!anyFlat)
4316
4316
  return;
4317
- await mkdir13(innerLib, { recursive: true });
4317
+ await mkdir14(innerLib, { recursive: true });
4318
4318
  for (const name of LEGACY_FLAT_BUNDLE_FILES) {
4319
4319
  try {
4320
- await access(join7(innerLib, name), fsConstants.F_OK);
4320
+ await access(join8(innerLib, name), fsConstants.F_OK);
4321
4321
  continue;
4322
4322
  } catch {
4323
4323
  }
4324
- await rename3(join7(bundleDir, name), join7(innerLib, name)).catch(() => void 0);
4324
+ await rename3(join8(bundleDir, name), join8(innerLib, name)).catch(() => void 0);
4325
4325
  }
4326
4326
  }
4327
4327
  async function writeBinForwarders(layout, activeBundleRoot) {
@@ -4330,13 +4330,13 @@ async function writeBinForwarders(layout, activeBundleRoot) {
4330
4330
  ...Object.entries(FORWARDER_ALIASES).map(([alias, tool]) => [alias, tool])
4331
4331
  ];
4332
4332
  for (const [name, tool] of entries) {
4333
- const target = join7(activeBundleRoot, "bin", tool);
4333
+ const target = join8(activeBundleRoot, "bin", tool);
4334
4334
  try {
4335
4335
  await access(target, fsConstants.X_OK);
4336
4336
  } catch {
4337
4337
  continue;
4338
4338
  }
4339
- await replaceFile(join7(layout.binDir, name), forwarderScript(tool), 493);
4339
+ await replaceFile(join8(layout.binDir, name), forwarderScript(tool), 493);
4340
4340
  }
4341
4341
  }
4342
4342
  async function repairInstallForwarders(layout, opts = {}) {
@@ -4345,7 +4345,7 @@ async function repairInstallForwarders(layout, opts = {}) {
4345
4345
  const forwarderHealthy = async (name, tool) => {
4346
4346
  let current;
4347
4347
  try {
4348
- current = await readFile9(join7(layout.binDir, name), "utf8");
4348
+ current = await readFile9(join8(layout.binDir, name), "utf8");
4349
4349
  } catch {
4350
4350
  current = void 0;
4351
4351
  }
@@ -4358,25 +4358,25 @@ async function repairInstallForwarders(layout, opts = {}) {
4358
4358
  }
4359
4359
  if (healthy)
4360
4360
  return false;
4361
- await mkdir13(layout.binDir, { recursive: true });
4361
+ await mkdir14(layout.binDir, { recursive: true });
4362
4362
  await writeBinForwarders(layout, layout.libDir);
4363
4363
  opts.logger?.(`[body] self-update: repaired <prefix>/bin forwarders to follow the active-bundle anchor (${layout.libDir})`);
4364
4364
  return true;
4365
4365
  }
4366
4366
  async function rollbackToPreviousRelease(layout, previousReleaseId) {
4367
- const releaseDir = join7(layout.releasesRoot, previousReleaseId);
4367
+ const releaseDir = join8(layout.releasesRoot, previousReleaseId);
4368
4368
  const entrypoint = await resolveBundleEntrypoint(releaseDir);
4369
4369
  if (!entrypoint) {
4370
4370
  throw new Error(`release ${previousReleaseId} has no runnable CLI entrypoint`);
4371
4371
  }
4372
4372
  const tempLink = `${layout.libDir}.rollback-${process.pid}`;
4373
4373
  await rm5(tempLink, { force: true });
4374
- await symlink2(join7("beeline-releases", previousReleaseId), tempLink);
4374
+ await symlink2(join8("beeline-releases", previousReleaseId), tempLink);
4375
4375
  await rename3(tempLink, layout.libDir);
4376
- await fsyncDir(dirname9(layout.libDir));
4376
+ await fsyncDir(dirname10(layout.libDir));
4377
4377
  }
4378
4378
  function updateAttemptPath(layout) {
4379
- return join7(layout.releasesRoot, ".state", "update-attempt.json");
4379
+ return join8(layout.releasesRoot, ".state", "update-attempt.json");
4380
4380
  }
4381
4381
  async function readUpdateAttempt(layout) {
4382
4382
  try {
@@ -4390,10 +4390,10 @@ async function readUpdateAttempt(layout) {
4390
4390
  }
4391
4391
  }
4392
4392
  async function writeUpdateAttempt(layout, record2) {
4393
- await mkdir13(join7(layout.releasesRoot, ".state"), { recursive: true });
4393
+ await mkdir14(join8(layout.releasesRoot, ".state"), { recursive: true });
4394
4394
  const path = updateAttemptPath(layout);
4395
4395
  const staged = `${path}.${process.pid}.tmp`;
4396
- await writeFile9(staged, `${JSON.stringify(record2, null, 2)}
4396
+ await writeFile10(staged, `${JSON.stringify(record2, null, 2)}
4397
4397
  `, { mode: 384 });
4398
4398
  await rename3(staged, path);
4399
4399
  }
@@ -4655,8 +4655,8 @@ var init_self_update = __esm({
4655
4655
  });
4656
4656
 
4657
4657
  // apps/body/dist/cli.js
4658
- import { dirname as dirname15, resolve as resolve29 } from "node:path";
4659
- import { readFile as readFile14, unlink as unlink5, writeFile as writeFile15 } from "node:fs/promises";
4658
+ import { dirname as dirname16, resolve as resolve29 } from "node:path";
4659
+ import { readFile as readFile14, unlink as unlink5, writeFile as writeFile16 } from "node:fs/promises";
4660
4660
  import { stdin as stdin4, stdout as stdout5 } from "node:process";
4661
4661
 
4662
4662
  // node_modules/@clack/core/dist/index.mjs
@@ -6663,20 +6663,6 @@ var ACCESS_NOTICE_WINDOW_MS = 10 * 6e4;
6663
6663
  var DEFAULT_ACCESS_POLICY = DEFAULT_AGENT_ACCESS_POLICY;
6664
6664
  var LEGACY_ACCESS_POLICY = "everyone";
6665
6665
  var ACCESS_REFUSAL_WINDOW_MS = 60 * 60 * 1e3;
6666
- function isSenderPermitted(policy, senderPubkey, ownerPubkey, allowlist = void 0) {
6667
- if (!senderPubkey)
6668
- return false;
6669
- switch (policy) {
6670
- case "everyone":
6671
- return true;
6672
- case "creator":
6673
- return Boolean(ownerPubkey) && senderPubkey === ownerPubkey;
6674
- case "allowlist":
6675
- return Boolean(allowlist?.includes(senderPubkey));
6676
- default:
6677
- return false;
6678
- }
6679
- }
6680
6666
 
6681
6667
  // apps/body/dist/model-catalog.js
6682
6668
  import { mkdtemp, rm } from "node:fs/promises";
@@ -6912,8 +6898,13 @@ function agentMessageRuns(updates, agentLabel) {
6912
6898
  let current = "";
6913
6899
  let lastWasText = false;
6914
6900
  for (const u4 of updates) {
6901
+ const isToolCall = u4.update.sessionUpdate === "tool_call";
6915
6902
  const delta = normalizeStreamDelta(agentMessageChunkText(u4.update), agentLabel);
6916
6903
  if (!delta) {
6904
+ if (isToolCall && current) {
6905
+ runs.push(current);
6906
+ current = "";
6907
+ }
6917
6908
  lastWasText = false;
6918
6909
  continue;
6919
6910
  }
@@ -6934,9 +6925,6 @@ function agentMessageRuns(updates, agentLabel) {
6934
6925
  return true;
6935
6926
  });
6936
6927
  }
6937
- function joinAgentMessageChunks(updates, agentLabel) {
6938
- return agentMessageRuns(updates, agentLabel).join("\n\n");
6939
- }
6940
6928
  var RETRY_NARRATION_FRAGMENTS = [
6941
6929
  // `Retrying (attempt 2/3, waiting 4s)` — the pi/ox-alpha shape.
6942
6930
  /retrying\s*\((?:attempt|try)\s*\d+\s*\/\s*\d+(?:,\s*(?:waiting|backoff)\s*\d+(?:\.\d+)?s)?\)/gi,
@@ -7065,6 +7053,7 @@ function toolCallEntries(updates) {
7065
7053
  ...typeof update.title === "string" ? { title: update.title } : {},
7066
7054
  ...typeof update.kind === "string" ? { kind: update.kind } : {},
7067
7055
  ...typeof update.status === "string" ? { status: update.status } : {},
7056
+ ...sessionUpdate === "tool_result" ? { resultReceived: true } : {},
7068
7057
  ..."rawInput" in update ? { rawInput: update.rawInput } : {},
7069
7058
  ..."content" in update ? { content: update.content } : {},
7070
7059
  ..."rawOutput" in update ? { rawOutput: update.rawOutput } : {},
@@ -7327,8 +7316,10 @@ var AcpClient = class extends EventEmitter {
7327
7316
  onToolCalls?.(toolCallEntries(updates));
7328
7317
  if (onChunk) {
7329
7318
  const delta = agentMessageChunkText(u4.update);
7330
- if (delta)
7331
- onChunk(delta, joinAgentMessageChunks(updates, this.agentLabel));
7319
+ if (delta) {
7320
+ const runs = agentMessageRuns(updates, this.agentLabel);
7321
+ onChunk(delta, runs.join("\n\n"), runs.at(-1), runs);
7322
+ }
7332
7323
  }
7333
7324
  };
7334
7325
  this.on("session/update", onUpdate);
@@ -8124,6 +8115,40 @@ async function syncAgentModelCatalog(input) {
8124
8115
  }
8125
8116
  }
8126
8117
 
8118
+ // packages/api-contract/dist/daemon-operations.js
8119
+ function isAgentCommand(value) {
8120
+ if (!value || typeof value !== "object")
8121
+ return false;
8122
+ const c2 = value;
8123
+ if (![
8124
+ "id",
8125
+ "roomId",
8126
+ "agentId",
8127
+ "sourceMessageId",
8128
+ "turnRequestId",
8129
+ "rootCommandId",
8130
+ "rootSourceMessageId",
8131
+ "reason"
8132
+ ].every((key) => typeof c2[key] === "string" && c2[key].length > 0))
8133
+ return false;
8134
+ if (!["input", "resume", "stop"].includes(String(c2.action)) || !Number.isInteger(c2.agentDepth) || Number(c2.agentDepth) < 0 || Number(c2.agentDepth) > 3)
8135
+ return false;
8136
+ const source = c2.source;
8137
+ return Boolean(source && typeof source.id === "string" && typeof source.authorId === "string" && typeof source.body === "string" && typeof source.createdAt === "number" && Array.isArray(source.attachments) && Array.isArray(source.mentionIds));
8138
+ }
8139
+
8140
+ // packages/api-contract/dist/system-events.js
8141
+ var SERVER_EVENT_KINDS = [
8142
+ "joined",
8143
+ "schedule-ran",
8144
+ "corner-opened",
8145
+ "check-passed",
8146
+ "check-failed",
8147
+ "merged",
8148
+ "grant-decided",
8149
+ "turn-cancelled"
8150
+ ];
8151
+
8127
8152
  // apps/body/dist/daemon-api-client.js
8128
8153
  import { resolve as resolve7 } from "node:path";
8129
8154
  import { randomUUID } from "node:crypto";
@@ -16905,20 +16930,6 @@ var DaemonApiError = class extends Error {
16905
16930
  }
16906
16931
  };
16907
16932
  var AGENT_REMOVED_CODE = "agent_removed";
16908
- function laterInboxCursor(left, right) {
16909
- if (!left)
16910
- return right;
16911
- if (!right)
16912
- return left;
16913
- const leftMatch = left.match(/^(\d+),([0-9a-f]{64})$/);
16914
- const rightMatch = right.match(/^(\d+),([0-9a-f]{64})$/);
16915
- if (!leftMatch || !rightMatch)
16916
- return right;
16917
- const timeOrder = BigInt(leftMatch[1]) - BigInt(rightMatch[1]);
16918
- if (timeOrder !== 0n)
16919
- return timeOrder > 0n ? left : right;
16920
- return leftMatch[2] >= rightMatch[2] ? left : right;
16921
- }
16922
16933
  function isAgentRemovedError(error) {
16923
16934
  return error instanceof DaemonApiError && error.status === 403 && error.code === AGENT_REMOVED_CODE;
16924
16935
  }
@@ -16958,20 +16969,23 @@ var DaemonApiClient = class {
16958
16969
  return { baseUrl: this.baseUrl, daemonToken: this.daemonToken, agentId: this.agentId };
16959
16970
  }
16960
16971
  /** Add one Room to this agent's shared live socket. */
16961
- liveSubscribe(roomId, cursor3, onItems, onState, presence) {
16972
+ liveSubscribe(roomId, cursor3, onItems, onState, presence, onCommands) {
16962
16973
  const existing = this.liveRooms.get(roomId);
16963
16974
  if (existing) {
16964
16975
  existing.cursor = cursor3 ?? existing.cursor;
16965
16976
  existing.onItems = onItems ?? existing.onItems;
16966
16977
  existing.onState = onState ?? existing.onState;
16967
16978
  existing.presence = presence ?? existing.presence;
16979
+ existing.onCommands = onCommands ?? existing.onCommands;
16968
16980
  } else {
16969
16981
  this.liveRooms.set(roomId, {
16970
16982
  ...cursor3 ? { cursor: cursor3 } : {},
16971
16983
  pushedIds: /* @__PURE__ */ new Set(),
16984
+ pushedCommandIds: /* @__PURE__ */ new Set(),
16972
16985
  ...onItems ? { onItems } : {},
16973
16986
  ...onState ? { onState } : {},
16974
- ...presence ? { presence } : {}
16987
+ ...presence ? { presence } : {},
16988
+ ...onCommands ? { onCommands } : {}
16975
16989
  });
16976
16990
  }
16977
16991
  this.ensureLiveSocket();
@@ -17041,6 +17055,22 @@ var DaemonApiClient = class {
17041
17055
  });
17042
17056
  return;
17043
17057
  }
17058
+ if (event.type === "commands" && typeof event.roomId === "string" && event.commandProtocol === 1 && Array.isArray(event.commands)) {
17059
+ const room2 = this.liveRooms.get(event.roomId);
17060
+ if (!room2)
17061
+ return;
17062
+ const commands = event.commands.filter((command) => {
17063
+ if (!isAgentCommand(command) || command.roomId !== event.roomId || command.agentId !== this.agentId || room2.pushedCommandIds.has(command.id))
17064
+ return false;
17065
+ room2.pushedCommandIds.add(command.id);
17066
+ return true;
17067
+ });
17068
+ while (room2.pushedCommandIds.size > 1e4)
17069
+ room2.pushedCommandIds.delete(room2.pushedCommandIds.values().next().value);
17070
+ if (commands.length)
17071
+ room2.onCommands?.(commands);
17072
+ return;
17073
+ }
17044
17074
  if (event.type !== "inbox" || typeof event.roomId !== "string" || !Array.isArray(event.items))
17045
17075
  return;
17046
17076
  const room = this.liveRooms.get(event.roomId);
@@ -17134,10 +17164,10 @@ async function activateDaemonTransport(path, fetchImpl = fetch) {
17134
17164
 
17135
17165
  // apps/body/dist/room-runtime.js
17136
17166
  import { execFile as execFile5 } from "node:child_process";
17137
- import { createHash as createHash4 } from "node:crypto";
17167
+ import { createHash as createHash5 } from "node:crypto";
17138
17168
  import { existsSync as existsSync4, mkdirSync } from "node:fs";
17139
- import { mkdir as mkdir11, rm as rm4 } from "node:fs/promises";
17140
- import { dirname as dirname6, resolve as resolve19 } from "node:path";
17169
+ import { mkdir as mkdir12, rm as rm4 } from "node:fs/promises";
17170
+ import { dirname as dirname7, resolve as resolve19 } from "node:path";
17141
17171
  import { promisify as promisify4 } from "node:util";
17142
17172
 
17143
17173
  // apps/body/dist/grant-runner.js
@@ -17190,18 +17220,6 @@ function commandGrantMatches(rule, requested) {
17190
17220
  return rule.argv.every((word, index) => requested[index] === word);
17191
17221
  }
17192
17222
  var DECISION_LINE = new RegExp(`^(.+?) (approved once|approved|declined) (${AGENT_GRANT_KINDS.join("|")}) (.+)$`, "s");
17193
- function parseGrantDecisionLine(body) {
17194
- const match = DECISION_LINE.exec(body);
17195
- if (!match)
17196
- return void 0;
17197
- const decision2 = match[2] === "approved once" ? "once" : match[2] === "approved" ? "always" : "deny";
17198
- return {
17199
- deciderName: match[1],
17200
- decision: decision2,
17201
- kind: match[3],
17202
- target: match[4]
17203
- };
17204
- }
17205
17223
  var AGENT_GRANT_ESCALATION_REASONS = {
17206
17224
  "unseen-script": "it runs a script whose contents nobody has read",
17207
17225
  credential: "it names a credential or environment file"
@@ -17781,7 +17799,9 @@ var GrantCommandRunner = class {
17781
17799
  if (!room)
17782
17800
  throw new Error("this daemon is not serving that Room");
17783
17801
  const argv = validateGrantArgv(input.argv);
17784
- const live = await this.options.api.execute("listAgentGrants", { agentId: this.options.agentId });
17802
+ const live = await this.options.api.execute("listAgentGrants", {
17803
+ agentId: this.options.agentId
17804
+ });
17785
17805
  const match = matchCommandGrant(live.grants, room.workspaceId, argv);
17786
17806
  if (!match) {
17787
17807
  throw new Error(`no approved command grant matches: ${argv.join(" ")}. Ask with request_grant kind=command first.`);
@@ -17844,6 +17864,7 @@ ${ROOM_WRITE_REFUSED_NOTE}` : outcome.output, secrets), cap);
17844
17864
  agentId: this.options.agentId,
17845
17865
  roomId: input.roomId,
17846
17866
  requestId: turn?.requestId ?? `grant:${grant.grantId}`,
17867
+ generationId: turn?.generationId,
17847
17868
  activity: [
17848
17869
  {
17849
17870
  kind: "tool",
@@ -17898,7 +17919,10 @@ function scriptCandidates(cwd, scratch, argv) {
17898
17919
  const argument = interpreterScriptArgument(argv);
17899
17920
  if (!argument)
17900
17921
  return [];
17901
- const paths = [resolve10(cwd, argument.path), ...scratch ? [resolve10(scratch, argument.path)] : []];
17922
+ const paths = [
17923
+ resolve10(cwd, argument.path),
17924
+ ...scratch ? [resolve10(scratch, argument.path)] : []
17925
+ ];
17902
17926
  return [...new Set(paths)];
17903
17927
  }
17904
17928
  function roomSandboxCommand(policy, cwd, argv) {
@@ -17977,42 +18001,167 @@ var GrantRunnerServer = class {
17977
18001
  }
17978
18002
  };
17979
18003
 
18004
+ // apps/body/dist/server-command-intake.js
18005
+ import { randomUUID as randomUUID2 } from "node:crypto";
18006
+ import { mkdir as mkdir3, writeFile as writeFile4 } from "node:fs/promises";
18007
+ import { tmpdir as tmpdir2 } from "node:os";
18008
+ import { dirname as dirname5, join as join3 } from "node:path";
18009
+ var CommandExecutionContext = class {
18010
+ generationId = randomUUID2();
18011
+ path;
18012
+ current;
18013
+ constructor(root) {
18014
+ this.path = join3(root ?? tmpdir2(), `beeline-command-${this.generationId}.json`);
18015
+ }
18016
+ async enter(command) {
18017
+ this.current = command;
18018
+ await mkdir3(dirname5(this.path), { recursive: true });
18019
+ await writeFile4(this.path, JSON.stringify({
18020
+ roomId: command.roomId,
18021
+ requestId: command.turnRequestId,
18022
+ generationId: this.generationId
18023
+ }), { mode: 384 });
18024
+ }
18025
+ async leave() {
18026
+ this.current = void 0;
18027
+ await writeFile4(this.path, "{}", { mode: 384 });
18028
+ }
18029
+ bind(api) {
18030
+ return new Proxy(api, {
18031
+ get: (target, key) => {
18032
+ if (key === "execute")
18033
+ return (name, input) => {
18034
+ const turn = this.current;
18035
+ return target.execute(name, {
18036
+ ...input,
18037
+ ...turn && (input.roomId === turn.roomId || input.cornerId === turn.roomId) ? {
18038
+ generationId: this.generationId,
18039
+ requestId: input.requestId ?? input.turnId ?? turn.turnRequestId
18040
+ } : {}
18041
+ });
18042
+ };
18043
+ const value = Reflect.get(target, key);
18044
+ return typeof value === "function" ? value.bind(target) : value;
18045
+ }
18046
+ });
18047
+ }
18048
+ };
18049
+ function validateServerCommand(command, roomId, agentId) {
18050
+ if (!isAgentCommand(command) || command.roomId !== roomId || command.agentId !== agentId)
18051
+ throw new Error("invalid server command");
18052
+ }
18053
+ async function runServerCommandIntake(options) {
18054
+ const { api, roomId, agentId, context, signal } = options;
18055
+ const first = await api.execute("getAgentCommands", { roomId });
18056
+ if (first?.commandProtocol !== 1)
18057
+ throw new Error("server command protocol 1 is required; refusing intake");
18058
+ let busy;
18059
+ let wake;
18060
+ const pending = new Map(first.commands.map((command) => [command.id, command]));
18061
+ const claimed = /* @__PURE__ */ new Set();
18062
+ const notify2 = (commands = []) => {
18063
+ for (const command of commands)
18064
+ if (!claimed.has(command.id))
18065
+ pending.set(command.id, command);
18066
+ wake?.(false);
18067
+ };
18068
+ options.onWake?.(notify2);
18069
+ const off = api.liveSubscribe?.(roomId, void 0, void 0, (connected) => {
18070
+ if (!connected)
18071
+ wake?.(true);
18072
+ }, options.presence, notify2);
18073
+ try {
18074
+ while (!signal?.aborted) {
18075
+ if (options.closed && await options.closed())
18076
+ return;
18077
+ for (const command of [...pending.values()]) {
18078
+ validateServerCommand(command, roomId, agentId);
18079
+ if (busy && command.action !== "stop")
18080
+ continue;
18081
+ pending.delete(command.id);
18082
+ try {
18083
+ await api.execute("claimAgentCommand", {
18084
+ roomId,
18085
+ commandId: command.id,
18086
+ generationId: context.generationId
18087
+ });
18088
+ } catch (error) {
18089
+ options.onError?.(error);
18090
+ continue;
18091
+ }
18092
+ claimed.add(command.id);
18093
+ if (command.action === "stop") {
18094
+ options.stop(command.turnRequestId);
18095
+ await api.execute("acknowledgeAgentCommand", {
18096
+ roomId,
18097
+ commandId: command.id,
18098
+ generationId: context.generationId
18099
+ });
18100
+ } else {
18101
+ await context.enter(command);
18102
+ busy = options.run(command).catch((error) => {
18103
+ claimed.delete(command.id);
18104
+ options.onError?.(error);
18105
+ }).finally(async () => {
18106
+ await context.leave();
18107
+ busy = void 0;
18108
+ notify2();
18109
+ });
18110
+ }
18111
+ }
18112
+ options.onPoll?.();
18113
+ const reconcile = await new Promise((resolve30) => {
18114
+ const done = (needed) => {
18115
+ if (timer)
18116
+ clearTimeout(timer);
18117
+ signal?.removeEventListener("abort", aborted);
18118
+ wake = void 0;
18119
+ resolve30(needed);
18120
+ };
18121
+ const aborted = () => done(false);
18122
+ wake = done;
18123
+ const timer = setTimeout(() => done(true), options.pollMs ?? 1e3);
18124
+ signal?.addEventListener("abort", aborted, { once: true });
18125
+ if (pending.size && !busy)
18126
+ done(false);
18127
+ });
18128
+ if (signal?.aborted)
18129
+ break;
18130
+ if (reconcile) {
18131
+ const page = await api.execute("getAgentCommands", { roomId });
18132
+ if (page.commandProtocol !== 1)
18133
+ throw new Error("server command protocol changed; refusing intake");
18134
+ notify2(page.commands);
18135
+ }
18136
+ }
18137
+ } finally {
18138
+ off?.();
18139
+ options.onWake?.(void 0);
18140
+ if (context.current)
18141
+ options.stop(context.current.turnRequestId);
18142
+ await busy;
18143
+ }
18144
+ }
18145
+
17980
18146
  // apps/body/dist/monolith-corner-turn.js
17981
18147
  import { execFile as execFile4 } from "node:child_process";
17982
- import { mkdir as mkdir9 } from "node:fs/promises";
17983
- import { homedir as homedir6 } from "node:os";
17984
- import { join as join5 } from "node:path";
18148
+ import { createHash as createHash4 } from "node:crypto";
18149
+ import { mkdir as mkdir11 } from "node:fs/promises";
18150
+ import { homedir as homedir7 } from "node:os";
18151
+ import { join as join7 } from "node:path";
17985
18152
  import { promisify as promisify3 } from "node:util";
17986
18153
 
17987
18154
  // apps/body/dist/agent-home.js
17988
18155
  import { existsSync as existsSync3, readFileSync as readFileSync4 } from "node:fs";
17989
- import { createHash as createHash3, randomUUID as randomUUID2 } from "node:crypto";
17990
- import { chmod as chmod2, copyFile, lstat, mkdir as mkdir4, readFile as readFile6, readdir as readdir2, realpath, rename as rename2, rm as rm2, symlink, unlink, writeFile as writeFile5 } from "node:fs/promises";
18156
+ import { createHash as createHash3, randomUUID as randomUUID3 } from "node:crypto";
18157
+ import { chmod as chmod2, copyFile, lstat, mkdir as mkdir5, readFile as readFile6, readdir as readdir2, realpath, rename as rename2, rm as rm2, symlink, unlink, writeFile as writeFile6 } from "node:fs/promises";
17991
18158
  import { homedir as homedir5 } from "node:os";
17992
- import { basename as basename3, dirname as dirname5, join as join3, relative as relative2, resolve as resolve13, sep } from "node:path";
18159
+ import { basename as basename3, dirname as dirname6, join as join4, relative as relative2, resolve as resolve13, sep } from "node:path";
17993
18160
 
17994
18161
  // apps/body/dist/beeline-skill.js
17995
18162
  import { readFileSync as readFileSync3 } from "node:fs";
17996
18163
  import { resolve as resolve11 } from "node:path";
17997
18164
 
17998
- // packages/api-contract/dist/system-events.js
17999
- var SERVER_EVENT_KINDS = [
18000
- "joined",
18001
- "schedule-ran",
18002
- "corner-opened",
18003
- "check-passed",
18004
- "check-failed",
18005
- "merged",
18006
- "grant-decided"
18007
- ];
18008
- function isServerEventKind(value) {
18009
- return SERVER_EVENT_KINDS.includes(value);
18010
- }
18011
- var RESUME_KINDS = ["grant-decided"];
18012
- function isResumeKind(value) {
18013
- return RESUME_KINDS.includes(value);
18014
- }
18015
-
18016
18165
  // packages/api-contract/dist/agent-pairing-code.js
18017
18166
  var CURRENT_AGENT_PAIRING_CODE = /^[0-9A-F]{8}-[0-9A-F]{8}$/;
18018
18167
  var LEGACY_AGENT_PAIRING_CODE = /^BUZZ-(?:[0-9A-F]{8}-[0-9A-F]{8}|[A-HJ-NP-Z2-9]{4}-[A-HJ-NP-Z2-9]{4})$/;
@@ -18110,7 +18259,7 @@ var SQUIRE_GOVERNED_TOOLS = [
18110
18259
  var SQUIRE_GOVERNED_TOOL_SET = new Set(SQUIRE_GOVERNED_TOOLS);
18111
18260
 
18112
18261
  // apps/body/dist/openrouter-routing.js
18113
- import { mkdir as mkdir3, readFile as readFile5, writeFile as writeFile4 } from "node:fs/promises";
18262
+ import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile5 } from "node:fs/promises";
18114
18263
  import { resolve as resolve12 } from "node:path";
18115
18264
  var OPENROUTER_ENDPOINTS_BASE_URL = "https://openrouter.ai/api/v1/models";
18116
18265
  var OPENROUTER_COMPLETIONS_URL = "https://openrouter.ai/api/v1/chat/completions";
@@ -18240,8 +18389,8 @@ async function readCache(cacheDir, model) {
18240
18389
  }
18241
18390
  }
18242
18391
  async function writeCache(cacheDir, value) {
18243
- await mkdir3(cacheDir, { recursive: true, mode: 448 });
18244
- await writeFile4(cachePath(cacheDir, value.model), `${JSON.stringify(value, null, 2)}
18392
+ await mkdir4(cacheDir, { recursive: true, mode: 448 });
18393
+ await writeFile5(cachePath(cacheDir, value.model), `${JSON.stringify(value, null, 2)}
18245
18394
  `, {
18246
18395
  mode: 384
18247
18396
  });
@@ -18275,8 +18424,8 @@ async function readProbeCache(cacheDir, model) {
18275
18424
  }
18276
18425
  }
18277
18426
  async function writeProbeCache(cacheDir, value) {
18278
- await mkdir3(cacheDir, { recursive: true, mode: 448 });
18279
- await writeFile4(probeCachePath(cacheDir, value.model), `${JSON.stringify(value, null, 2)}
18427
+ await mkdir4(cacheDir, { recursive: true, mode: 448 });
18428
+ await writeFile5(probeCachePath(cacheDir, value.model), `${JSON.stringify(value, null, 2)}
18280
18429
  `, {
18281
18430
  mode: 384
18282
18431
  });
@@ -18732,14 +18881,14 @@ async function prepareRoomAgentHome(input) {
18732
18881
  const root = resolve13(input.root);
18733
18882
  const operatorHome = input.operatorHome ?? homedir5();
18734
18883
  try {
18735
- await mkdir4(root, { recursive: true, mode: 448 });
18884
+ await mkdir5(root, { recursive: true, mode: 448 });
18736
18885
  const rootStats = await lstat(root);
18737
18886
  if (!rootStats.isDirectory() || rootStats.isSymbolicLink()) {
18738
18887
  throw new AgentHomeSecurityError(`agent home root is not an ordinary directory: ${root}`);
18739
18888
  }
18740
18889
  for (const subdir of HOME_SUBDIRS) {
18741
18890
  const path = resolve13(root, subdir);
18742
- await mkdir4(path, { recursive: true, mode: 448 });
18891
+ await mkdir5(path, { recursive: true, mode: 448 });
18743
18892
  await assertRealContainedDirectory(path, root);
18744
18893
  }
18745
18894
  } catch (error) {
@@ -18791,7 +18940,7 @@ async function provisionAgentSkillsAndMcp(root, operatorHome, skillReleaseId, fa
18791
18940
  }
18792
18941
  try {
18793
18942
  const gooseConfigDir = resolve13(root, "goose", "config");
18794
- await mkdir4(gooseConfigDir, { recursive: true, mode: 448 });
18943
+ await mkdir5(gooseConfigDir, { recursive: true, mode: 448 });
18795
18944
  for (const name of GOOSE_SHARED_CONFIG_FILES) {
18796
18945
  const source = resolve13(operatorHome, ".config", "goose", name);
18797
18946
  const target = resolve13(gooseConfigDir, name);
@@ -18903,18 +19052,18 @@ function filteredHarnessMcpToml(source) {
18903
19052
  return extractTomlSections(source, ["mcp_servers"], excluded);
18904
19053
  }
18905
19054
  async function provisionManagedSkillsDir(target, managedSkills, sharedSkills, optionalShares) {
18906
- const parent = dirname5(target);
18907
- await assertRealContainedDirectory(parent, dirname5(parent));
19055
+ const parent = dirname6(target);
19056
+ await assertRealContainedDirectory(parent, dirname6(parent));
18908
19057
  const plan = await planManagedSkills(managedSkills, sharedSkills, optionalShares);
18909
19058
  if (await materializedSkillManifest(target) === plan.manifest)
18910
19059
  return;
18911
- const staged = resolve13(parent, `.skills.${process.pid}.${randomUUID2()}.tmp`);
18912
- await mkdir4(staged, { mode: 448 });
19060
+ const staged = resolve13(parent, `.skills.${process.pid}.${randomUUID3()}.tmp`);
19061
+ await mkdir5(staged, { mode: 448 });
18913
19062
  try {
18914
19063
  for (const entry of plan.entries) {
18915
19064
  if (entry.kind === "managed") {
18916
19065
  const skillDir = resolve13(staged, entry.name);
18917
- await mkdir4(skillDir, { recursive: true });
19066
+ await mkdir5(skillDir, { recursive: true });
18918
19067
  await writeIsolatedHarnessFile(resolve13(skillDir, "SKILL.md"), entry.content);
18919
19068
  } else {
18920
19069
  await copySafeSkillTree(entry.source, resolve13(staged, entry.name), entry.source);
@@ -18944,8 +19093,8 @@ async function planManagedSkills(managedSkills, sharedSkills, optionalShares) {
18944
19093
  try {
18945
19094
  const tree = [];
18946
19095
  await walkSafeSkillTree(shared.source, shared.source, {
18947
- directory: async (rel) => void tree.push(`d ${join3(shared.name, rel)}`),
18948
- file: async (rel, realPath) => void tree.push(`f ${join3(shared.name, rel)} ${sha2563(await readFile6(realPath))}`)
19096
+ directory: async (rel) => void tree.push(`d ${join4(shared.name, rel)}`),
19097
+ file: async (rel, realPath) => void tree.push(`f ${join4(shared.name, rel)} ${sha2563(await readFile6(realPath))}`)
18949
19098
  });
18950
19099
  entries.push({ kind: "shared", name: shared.name, source: shared.source });
18951
19100
  lines.push(...tree);
@@ -18965,7 +19114,7 @@ async function materializedSkillManifest(target) {
18965
19114
  const visit = async (directory, prefix) => {
18966
19115
  for (const entry of await readdir2(directory)) {
18967
19116
  const path = resolve13(directory, entry);
18968
- const rel = prefix ? join3(prefix, entry) : entry;
19117
+ const rel = prefix ? join4(prefix, entry) : entry;
18969
19118
  const entryStats = await lstat(path);
18970
19119
  if (entryStats.isSymbolicLink())
18971
19120
  return false;
@@ -19133,7 +19282,7 @@ async function walkSafeSkillTree(source, sourceRoot, visitor, rel = "") {
19133
19282
  for (const entry of await readdir2(resolvedSource)) {
19134
19283
  if (entry === "." || entry === "..")
19135
19284
  throw new Error("invalid shared skill entry");
19136
- await walkSafeSkillTree(resolve13(source, entry), sourceRoot, visitor, rel ? join3(rel, entry) : entry);
19285
+ await walkSafeSkillTree(resolve13(source, entry), sourceRoot, visitor, rel ? join4(rel, entry) : entry);
19137
19286
  }
19138
19287
  return;
19139
19288
  }
@@ -19145,7 +19294,7 @@ async function walkSafeSkillTree(source, sourceRoot, visitor, rel = "") {
19145
19294
  async function copySafeSkillTree(source, target, sourceRoot) {
19146
19295
  await walkSafeSkillTree(source, sourceRoot, {
19147
19296
  directory: async (rel) => {
19148
- await mkdir4(resolve13(target, rel), { mode: 448 });
19297
+ await mkdir5(resolve13(target, rel), { mode: 448 });
19149
19298
  },
19150
19299
  file: async (rel, realPath) => {
19151
19300
  const destination = resolve13(target, rel);
@@ -19155,14 +19304,14 @@ async function copySafeSkillTree(source, target, sourceRoot) {
19155
19304
  });
19156
19305
  }
19157
19306
  async function writeIsolatedHarnessFile(path, content) {
19158
- const parent = dirname5(path);
19307
+ const parent = dirname6(path);
19159
19308
  const parentStats = await lstat(parent);
19160
19309
  if (!parentStats.isDirectory() || parentStats.isSymbolicLink()) {
19161
19310
  throw new Error(`isolated harness parent is not a real directory: ${parent}`);
19162
19311
  }
19163
- const temporary = resolve13(parent, `.${basename3(path)}.${process.pid}.${randomUUID2()}.tmp`);
19312
+ const temporary = resolve13(parent, `.${basename3(path)}.${process.pid}.${randomUUID3()}.tmp`);
19164
19313
  try {
19165
- await writeFile5(temporary, content, { mode: 384, flag: "wx" });
19314
+ await writeFile6(temporary, content, { mode: 384, flag: "wx" });
19166
19315
  await chmod2(temporary, 384);
19167
19316
  await rename2(temporary, path);
19168
19317
  } finally {
@@ -19205,8 +19354,8 @@ function harnessStateDirsFromEnv(env) {
19205
19354
  }
19206
19355
 
19207
19356
  // apps/body/dist/attachment-delivery.js
19208
- import { mkdir as mkdir5, writeFile as writeFile6 } from "node:fs/promises";
19209
- import { basename as basename4, extname, join as join4 } from "node:path";
19357
+ import { mkdir as mkdir6, writeFile as writeFile7 } from "node:fs/promises";
19358
+ import { basename as basename4, extname, join as join5 } from "node:path";
19210
19359
  var MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024;
19211
19360
  var MEDIA_TTL_HOURS = 24;
19212
19361
  var EXPIRED_REASON = `expired: attachments are kept for ${MEDIA_TTL_HOURS} hours and these bytes are past that window`;
@@ -19226,7 +19375,7 @@ async function deliverAttachments(attachments, dir, fetchImpl = fetch) {
19226
19375
  if (!attachments.length)
19227
19376
  return [];
19228
19377
  const taken = /* @__PURE__ */ new Set();
19229
- await mkdir5(dir, { recursive: true });
19378
+ await mkdir6(dir, { recursive: true });
19230
19379
  return Promise.all(attachments.map(async (attachment, index) => {
19231
19380
  const tooLarge = (bytes) => ({
19232
19381
  attachment,
@@ -19250,8 +19399,8 @@ async function deliverAttachments(attachments, dir, fetchImpl = fetch) {
19250
19399
  const bytes = Buffer.from(await response.arrayBuffer());
19251
19400
  if (bytes.length > MAX_ATTACHMENT_BYTES)
19252
19401
  return tooLarge(bytes.length);
19253
- const path = join4(dir, safeFileName(attachment, index, taken));
19254
- await writeFile6(path, bytes);
19402
+ const path = join5(dir, safeFileName(attachment, index, taken));
19403
+ await writeFile7(path, bytes);
19255
19404
  const mimeType = attachment.mimeType ?? response.headers.get("content-type") ?? "";
19256
19405
  if (!mimeType.startsWith("image/"))
19257
19406
  return { attachment, path };
@@ -19433,6 +19582,11 @@ function isCornerStatusRestatement(reply, systemLines) {
19433
19582
  return statusWords(text2).every((word) => !word || admitted.has(word));
19434
19583
  }
19435
19584
 
19585
+ // apps/body/dist/turn-stop.js
19586
+ var TurnStoppedError = class extends Error {
19587
+ name = "TurnStoppedError";
19588
+ };
19589
+
19436
19590
  // apps/body/dist/turn-stream.js
19437
19591
  function durableReplyText(agentText) {
19438
19592
  return sanitizeAgentReply(agentText);
@@ -19491,7 +19645,8 @@ var AgentTurnStream = class {
19491
19645
  /**
19492
19646
  * Everything the delta hook has seen this turn: every assistant run joined,
19493
19647
  * which is a LONGER string than `PromptResult.agentText` whenever the turn
19494
- * spoke before a tool call. Nothing durable is derived from it.
19648
+ * spoke before a tool call. This final-reply lane derives nothing durable
19649
+ * from it; a corner records its current normalized run independently.
19495
19650
  */
19496
19651
  get streamedText() {
19497
19652
  return this.latest;
@@ -19514,17 +19669,18 @@ var AgentTurnStream = class {
19514
19669
  * An empty reply settles through the turn receipt instead, and the lane is
19515
19670
  * retracted either way.
19516
19671
  */
19517
- async settle(reply, fields = {}) {
19672
+ async settle(reply, fields = {}, onReplyPosted) {
19518
19673
  this.close();
19519
19674
  const { api, agentId, roomId, requestId } = this.options;
19520
19675
  if (reply) {
19521
- await api.execute("postRoomMessage", {
19676
+ const posted = await api.execute("postRoomMessage", {
19522
19677
  roomId,
19523
19678
  requestId,
19524
19679
  text: reply,
19525
19680
  presentation: "message",
19526
19681
  ...fields
19527
19682
  });
19683
+ onReplyPosted?.(posted);
19528
19684
  }
19529
19685
  await this.inFlight;
19530
19686
  await api.execute("retractAgentLiveOutput", {
@@ -19602,7 +19758,7 @@ function sessionConfigFingerprint(input) {
19602
19758
  }
19603
19759
 
19604
19760
  // apps/body/dist/pi-mcp-bridge.js
19605
- import { mkdir as mkdir6 } from "node:fs/promises";
19761
+ import { mkdir as mkdir7 } from "node:fs/promises";
19606
19762
  import { resolve as resolve14 } from "node:path";
19607
19763
  var PI_MCP_BRIDGE_FILENAME = "beeline-mcp-bridge.js";
19608
19764
  function harnessMountsSessionMcpServers(agentCommand) {
@@ -19626,7 +19782,7 @@ async function installPiMcpBridge(input) {
19626
19782
  const directory = resolve14(input.piHome, "extensions");
19627
19783
  const path = resolve14(directory, PI_MCP_BRIDGE_FILENAME);
19628
19784
  try {
19629
- await mkdir6(directory, { recursive: true, mode: 448 });
19785
+ await mkdir7(directory, { recursive: true, mode: 448 });
19630
19786
  await writeIsolatedHarnessFile(path, piMcpBridgeSource(input.servers));
19631
19787
  return path;
19632
19788
  } catch (error) {
@@ -20031,6 +20187,7 @@ function beelineAgentMcpServer(config, api, context) {
20031
20187
  args: [...config.readonlyMcpArgs ?? []],
20032
20188
  env: [
20033
20189
  { name: "BEELINE_MCP_SURFACE", value: "agent" },
20190
+ ...context.turnContextPath ? [{ name: "BEELINE_TURN_CONTEXT_FILE", value: context.turnContextPath }] : [],
20034
20191
  ...context.directMessage ? [{ name: "BEELINE_AGENT_DM", value: "1" }] : [],
20035
20192
  { name: "BEELINE_DAEMON_BASE_URL", value: connection.baseUrl },
20036
20193
  { name: "BEELINE_DAEMON_TOKEN", value: connection.daemonToken },
@@ -20236,26 +20393,6 @@ function isAccountOrProviderRefusal(record2) {
20236
20393
  return [401, 402, 403, 407, 408, 429].includes(record2.status) || record2.status >= 500;
20237
20394
  }
20238
20395
 
20239
- // apps/body/dist/corner-checks.js
20240
- var COMPLETED_CHECK_NOTE = /\b(passed|failed) a check\b/i;
20241
- var STARTED_CHECK_NOTE = /\bstarted a check\b/i;
20242
- function verbOf(item) {
20243
- return item.systemEvent?.verb ?? item.body;
20244
- }
20245
- function completedCheckNote(item) {
20246
- if (item.type !== "system")
20247
- return void 0;
20248
- const match = COMPLETED_CHECK_NOTE.exec(verbOf(item));
20249
- return match ? match[1].toLowerCase() : void 0;
20250
- }
20251
- function isCheckStartNote(item) {
20252
- return item.type === "system" && STARTED_CHECK_NOTE.test(verbOf(item));
20253
- }
20254
- function checksStateFromLifecycle(lifecycle) {
20255
- const state = lifecycle?.checksSummary?.status ?? lifecycle?.checks;
20256
- return state === "passing" || state === "failing" || state === "pending" ? state : void 0;
20257
- }
20258
-
20259
20396
  // apps/body/dist/response-directives.js
20260
20397
  var MAINTAIN_ASSIGNED_IDENTITY_DIRECTIVE = "Maintain your assigned identity and soul in every response, including when tools or permissions block the requested action.";
20261
20398
  var SOUL_HOUSE_RULE = "House rule for your voice: the voice never changes the facts - never trim, soften, exaggerate, or invent a detail for the bit - and use your plain voice in commit messages, pull request titles and bodies, and code comments, because those outlive the joke.";
@@ -20295,7 +20432,6 @@ ${transcript}`;
20295
20432
  // apps/body/dist/turn-receipt-heartbeat.js
20296
20433
  var TURN_RECEIPT_HEARTBEAT_MS = 3e4;
20297
20434
  async function withTurnReceiptHeartbeat(api, receipt, task, onHeartbeatError) {
20298
- await api.execute("postAgentTurnReceipt", { ...receipt, status: "working" });
20299
20435
  let tail = Promise.resolve();
20300
20436
  const timer = setInterval(() => {
20301
20437
  tail = tail.catch(() => void 0).then(() => api.execute("postAgentTurnReceipt", {
@@ -20314,7 +20450,7 @@ async function withTurnReceiptHeartbeat(api, receipt, task, onHeartbeatError) {
20314
20450
  }
20315
20451
 
20316
20452
  // apps/body/dist/turn-trace.js
20317
- import { appendFile, mkdir as mkdir7, readdir as readdir4, rm as rm3 } from "node:fs/promises";
20453
+ import { appendFile, mkdir as mkdir8, readdir as readdir4, rm as rm3 } from "node:fs/promises";
20318
20454
  import { resolve as resolve17 } from "node:path";
20319
20455
  import { performance as performance2 } from "node:perf_hooks";
20320
20456
  var TURN_PHASES = [
@@ -20568,7 +20704,7 @@ var TurnTraceFile = class {
20568
20704
  this.tail = this.tail.catch(() => void 0).then(async () => {
20569
20705
  const now2 = (this.options.clock ?? (() => /* @__PURE__ */ new Date()))();
20570
20706
  const path = this.path(now2);
20571
- await mkdir7(this.directory, { recursive: true, mode: 448 });
20707
+ await mkdir8(this.directory, { recursive: true, mode: 448 });
20572
20708
  await appendFile(path, `${JSON.stringify(record2)}
20573
20709
  `, { mode: 384 });
20574
20710
  await this.prune(now2);
@@ -20591,11 +20727,11 @@ var TurnTraceFile = class {
20591
20727
  };
20592
20728
 
20593
20729
  // apps/body/dist/corner-github-auth.js
20594
- import { chmod as chmod3, mkdir as mkdir8, writeFile as writeFile7 } from "node:fs/promises";
20730
+ import { chmod as chmod3, mkdir as mkdir9, writeFile as writeFile8 } from "node:fs/promises";
20595
20731
  import { delimiter as delimiter2, resolve as resolve18 } from "node:path";
20596
20732
  async function installCornerGitHubWrappers(input) {
20597
20733
  const bin = resolve18(input.root, "beeline-github-bin");
20598
- await mkdir8(bin, { recursive: true, mode: 448 });
20734
+ await mkdir9(bin, { recursive: true, mode: 448 });
20599
20735
  const common = {
20600
20736
  node: process.execPath,
20601
20737
  cli: input.cliEntrypoint,
@@ -20637,149 +20773,118 @@ if (result.stderr) process.stderr.write(result.stderr);
20637
20773
  if (result.error) throw result.error;
20638
20774
  process.exit(result.status ?? 1);
20639
20775
  `;
20640
- await writeFile7(path, source, { mode: 448 });
20776
+ await writeFile8(path, source, { mode: 448 });
20641
20777
  await chmod3(path, 448);
20642
20778
  }
20643
20779
 
20644
- // apps/body/dist/monolith-corner-turn.js
20645
- var execFileAsync3 = promisify3(execFile4);
20646
- var TOOL_ARGUMENT_MAX_BYTES = 1200;
20647
- var TOOL_OUTPUT_MAX_BYTES = 3200;
20648
- var TOOL_PATH_LIMIT = 12;
20649
- function oneLine(value) {
20650
- return value.replace(/\s+/g, " ").trim();
20780
+ // apps/body/dist/monolith-room-turn.js
20781
+ import { mkdir as mkdir10 } from "node:fs/promises";
20782
+ import { homedir as homedir6 } from "node:os";
20783
+ import { join as join6 } from "node:path";
20784
+
20785
+ // packages/api-contract/dist/scheduled-prompts.js
20786
+ var SCHEDULE_SCHEDULER_NAME = "Beeline Scheduler";
20787
+ var SCHEDULE_RAN_VERB = "ran a schedule for";
20788
+
20789
+ // apps/body/dist/monolith-room-turn.js
20790
+ function isRoomMcpPermissionRequest(request, mountedServers = ROOM_MOUNTED_MCP_SERVERS) {
20791
+ if (isSquireMcpPermissionRequest(request))
20792
+ return false;
20793
+ return isMountedMcpToolPermissionRequest(request, mountedServers);
20651
20794
  }
20652
- function serialized(value) {
20653
- if (typeof value === "string")
20654
- return value;
20655
- try {
20656
- return JSON.stringify(value) ?? "";
20657
- } catch {
20658
- return "";
20659
- }
20795
+ function isScheduledPrompt(item, agentId) {
20796
+ if (item.type !== "system" || !item.mentionIds.includes(agentId))
20797
+ return false;
20798
+ if (item.systemEvent?.kind)
20799
+ return item.systemEvent.kind === "schedule-ran";
20800
+ return item.systemEvent?.verb === SCHEDULE_RAN_VERB;
20660
20801
  }
20661
- function record(value) {
20662
- return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
20802
+ function inboxItemAuthorName(item, agentId, names) {
20803
+ if (isScheduledPrompt(item, agentId))
20804
+ return SCHEDULE_SCHEDULER_NAME;
20805
+ const subject = item.systemEvent?.subject;
20806
+ if (item.type === "system" && subject?.name)
20807
+ return subject.name;
20808
+ return names.get(item.authorId) ?? item.authorId.slice(0, 12);
20663
20809
  }
20664
- function clampBytes(value, maxBytes) {
20665
- const clean4 = value.trim();
20666
- if (Buffer.byteLength(clean4) <= maxBytes)
20667
- return clean4;
20668
- const suffix = "\n\u2026[truncated]";
20669
- const allowed = maxBytes - Buffer.byteLength(suffix);
20670
- return `${Buffer.from(clean4).subarray(0, Math.max(0, allowed)).toString("utf8")}${suffix}`;
20810
+ function inboxItemPromptBody(item, agentId) {
20811
+ return isScheduledPrompt(item, agentId) ? item.systemEvent?.consequence ?? item.body : item.body;
20671
20812
  }
20672
- function outputExcerpt(value) {
20673
- const redacted = redactToolDetail(serialized(value));
20674
- if (!redacted.trim())
20675
- return void 0;
20676
- if (/\b(?:git[- ]credential|credential[- ]helper)\b/i.test(redacted)) {
20677
- return "Credential-helper output omitted.";
20813
+ function pendingGrantToolCall(call) {
20814
+ if (!/(?:^|[._:/-])request_grant$/i.test(call.title ?? ""))
20815
+ return false;
20816
+ return /pending, card posted/i.test(typeof call.content === "string" ? call.content : JSON.stringify(call.content ?? ""));
20817
+ }
20818
+ function roomMentionDirectory(roster, selfId) {
20819
+ const rows = [];
20820
+ for (const member of roster.members) {
20821
+ if (member.identityId === selfId)
20822
+ continue;
20823
+ const handle = member.handle?.trim().replace(/^@/, "");
20824
+ const name = member.name?.trim() ?? "";
20825
+ const alias = handle || name;
20826
+ if (!alias)
20827
+ continue;
20828
+ const kind = member.kind === "agent" ? "agent" : "person";
20829
+ rows.push(`- @${alias}${name && name !== alias ? ` \u2014 ${name}` : ""} (${kind})`);
20678
20830
  }
20679
- const lines = redacted.split(/\r?\n/).map((line) => line.trimEnd());
20680
- if (lines.length <= 8)
20681
- return clampBytes(lines.join("\n"), TOOL_OUTPUT_MAX_BYTES);
20682
- return clampBytes([...lines.slice(0, 4), "\u2026[output omitted]\u2026", ...lines.slice(-4)].join("\n"), TOOL_OUTPUT_MAX_BYTES);
20831
+ if (!rows.length)
20832
+ return "";
20833
+ return [
20834
+ "Room members, and the exact spelling that tags each one:",
20835
+ "An exact agent tag assigns that agent work; use it only when you are asking that agent to act.",
20836
+ ...rows,
20837
+ "Write a tag exactly as spelled here. An @name spelled any other way is plain text: it reaches nobody, and nobody is told it was meant for them. Never invent a handle, shorten one, or copy an @name out of the conversation \u2014 old messages carry spellings that no longer exist."
20838
+ ].join("\n");
20683
20839
  }
20684
- function filePaths(value, worktreePath) {
20685
- const paths = /* @__PURE__ */ new Set();
20686
- const visit = (candidate, key) => {
20687
- if (paths.size >= TOOL_PATH_LIMIT || candidate === null || candidate === void 0)
20688
- return;
20689
- if (typeof candidate === "string") {
20690
- if (key && /(?:^|_)(?:path|file|filename|target)$/i.test(key)) {
20691
- const path = candidate.startsWith(`${worktreePath}/`) ? candidate.slice(worktreePath.length + 1) : candidate;
20692
- if (path && path.length <= 512)
20693
- paths.add(redactToolDetail(path));
20694
- }
20695
- return;
20696
- }
20697
- if (Array.isArray(candidate)) {
20698
- candidate.forEach((entry) => visit(entry));
20699
- return;
20700
- }
20701
- const object = record(candidate);
20702
- if (object)
20703
- Object.entries(object).forEach(([entryKey, entry]) => visit(entry, entryKey));
20704
- };
20705
- visit(value);
20706
- return [...paths];
20840
+ function escapeRegExp(value) {
20841
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
20707
20842
  }
20708
- function resultStatus(call) {
20709
- const content = record(call.content) ?? (typeof call.content === "string" ? (() => {
20710
- try {
20711
- return record(JSON.parse(call.content));
20712
- } catch {
20713
- return void 0;
20714
- }
20715
- })() : void 0);
20716
- const exitCode = content?.exitCode ?? content?.exit_code ?? content?.code;
20717
- if (typeof exitCode === "number" && Number.isFinite(exitCode))
20718
- return `exit ${exitCode}`;
20719
- if (content?.ok === true || content?.success === true)
20720
- return "ok";
20721
- if (content?.ok === false || content?.success === false)
20722
- return "error";
20723
- return /(?:failed|error|denied)/i.test(call.status ?? "") ? "error" : "ok";
20724
- }
20725
- function toolArguments(call) {
20726
- const raw = record(call.rawInput);
20727
- const command = typeof call.rawInput === "string" ? call.rawInput : typeof raw?.command === "string" ? raw.command : typeof raw?.cmd === "string" ? raw.cmd : void 0;
20728
- if (command)
20729
- return { command: clampBytes(redactToolDetail(command), TOOL_ARGUMENT_MAX_BYTES) };
20730
- const input = serialized(call.rawInput);
20731
- return input ? { input: clampBytes(redactToolDetail(input), TOOL_ARGUMENT_MAX_BYTES) } : {};
20732
- }
20733
- function toolCallKey(call, index) {
20734
- return call.id ?? `tool-${index}`;
20735
- }
20736
- function toolCallSettled(call) {
20737
- return /^(?:completed|complete|failed|error|succeeded|success|passed|done)$/i.test(call.status ?? "");
20738
- }
20739
- function isSuccessfulCommit(call) {
20740
- if (/failed|error|denied/i.test(call.status ?? ""))
20741
- return false;
20742
- return /\bgit\s+commit\b|\bcommit(?:ted)?\s+(?:changes|files?)\b/i.test(`${call.title ?? ""} ${serialized(call.rawInput)}`);
20743
- }
20744
- async function cornerToolActivity(call, worktreePath, requestedBy) {
20745
- const operation = oneLine(call.kind ?? "") || "tool";
20746
- let title = oneLine(redactToolDetail(call.title ?? "")) || `${operation} tool`;
20747
- if (isSuccessfulCommit(call)) {
20748
- try {
20749
- const shown = await execFileAsync3("git", ["-C", worktreePath, "show", "--format=%s", "--name-only", "--no-renames", "HEAD"], { maxBuffer: 1024 * 1024 });
20750
- const lines = shown.stdout.split(/\r?\n/);
20751
- const subject = oneLine(lines.shift() ?? "commit");
20752
- const files = new Set(lines.map(oneLine).filter(Boolean));
20753
- title = `committed ${files.size} files: ${subject}`;
20754
- } catch {
20843
+ function agentReplyMentionIds(text2, roster, authorId) {
20844
+ const aliases = /* @__PURE__ */ new Map();
20845
+ for (const member of roster.members) {
20846
+ if (member.identityId === authorId)
20847
+ continue;
20848
+ const rawAliases = member.kind === "agent" ? [member.handle] : [member.name, member.handle, member.soul?.name];
20849
+ for (const raw of rawAliases) {
20850
+ const display = raw?.trim().replace(/^@/, "");
20851
+ if (!display)
20852
+ continue;
20853
+ const key = display.toLocaleLowerCase();
20854
+ const entry = aliases.get(key) ?? { display, ids: /* @__PURE__ */ new Set() };
20855
+ entry.ids.add(member.identityId);
20856
+ aliases.set(key, entry);
20755
20857
  }
20756
20858
  }
20757
- const paths = filePaths([call.rawInput, call.content, call.locations], worktreePath);
20758
- const argumentsSummary = toolArguments(call);
20759
- const output = outputExcerpt(call.content);
20760
- return {
20761
- kind: "tool",
20762
- title: title.slice(0, 240),
20763
- operation: operation.slice(0, 80),
20764
- status: resultStatus(call),
20765
- ...argumentsSummary,
20766
- ...output ? { output } : {},
20767
- ...requestedBy ? { requestedBy } : {},
20768
- ...paths.length ? { files: paths.map((path) => ({ path })) } : {}
20769
- };
20770
- }
20771
- var CORNER_CLOSE_POLL_BASE_MS = 12e3;
20772
- function cornerClosePollMs(random = Math.random) {
20773
- return CORNER_CLOSE_POLL_BASE_MS + Math.floor(random() * 3e3);
20859
+ for (const member of roster.members) {
20860
+ if (member.identityId === authorId || member.kind === "agent" || !member.handle)
20861
+ continue;
20862
+ const handle = member.handle.trim().replace(/^@/, "").toLocaleLowerCase();
20863
+ const canonical = aliases.get(handle);
20864
+ const legacy = `a_${handle}`;
20865
+ if (canonical && !aliases.has(legacy))
20866
+ aliases.set(legacy, { ...canonical, display: legacy });
20867
+ }
20868
+ const mentioned = [];
20869
+ for (const { display, ids } of [...aliases.values()].sort((left, right) => right.display.length - left.display.length)) {
20870
+ if (ids.size !== 1)
20871
+ continue;
20872
+ const pattern = new RegExp(`(^|[\\s([{])@${escapeRegExp(display)}(?=$|[\\s.,!?;:)\\]}])`, "iu");
20873
+ if (!pattern.test(text2))
20874
+ continue;
20875
+ const identityId = [...ids][0];
20876
+ if (!mentioned.includes(identityId))
20877
+ mentioned.push(identityId);
20878
+ }
20879
+ return mentioned;
20774
20880
  }
20775
- var MonolithCornerTurnLoop = class {
20881
+ var MonolithRoomTurnLoop = class {
20776
20882
  options;
20883
+ commandContext;
20777
20884
  agent;
20778
- reconciliationRequested = true;
20779
20885
  wakeIntake;
20780
20886
  /** Called by the daemon's one slow workspace reconciliation sweep. */
20781
20887
  requestReconciliation() {
20782
- this.reconciliationRequested = true;
20783
20888
  this.wakeIntake?.();
20784
20889
  this.wakeIntake = void 0;
20785
20890
  }
@@ -20787,67 +20892,110 @@ var MonolithCornerTurnLoop = class {
20787
20892
  sessionId;
20788
20893
  /** The configuration the live session baked in; a change invalidates it. */
20789
20894
  sessionFingerprint;
20790
- /** What this exact ACP session has already been prompted with (`warm-transcript.ts`). */
20791
- warmTranscript = new WarmTranscript();
20792
20895
  /** The live session's environment, read back for pi's own turn record. */
20793
20896
  agentEnv = {};
20794
20897
  /** OpenRouter providers this activation pinned, in order (C92). */
20795
20898
  pinnedProviders = [];
20796
- /** Whether the pinned model takes images; `undefined` when the pin did not say. */
20797
- modelTakesImages;
20798
20899
  /** The one provider re-pinned after an empty completion, until the session ends. */
20799
20900
  pinnedProviderOverride;
20800
- turnIdentityInstructions = "";
20801
20901
  busy = false;
20802
- forcedStop = false;
20803
- activityTail = Promise.resolve();
20902
+ turnInstructionPrefix = "";
20903
+ activeTurn;
20904
+ queuedTurns = [];
20804
20905
  /** Session scratch directory attachments are downloaded into (`TMPDIR/beeline-attachments`). */
20805
20906
  attachmentDir;
20806
- /** The session's TMPDIR, where a granted command's script argument may also live. */
20907
+ /** Whether the pinned model takes images; `undefined` when the pin did not say. */
20908
+ modelTakesImages;
20909
+ /** The session's TMPDIR: writable to a granted command in a Room, as it is to the harness (C94). */
20807
20910
  sessionScratchDir;
20808
- /** The turn in flight and who asked for it, for ledger rows and the grant runner. */
20809
- currentTurn;
20911
+ /** The `agent-home.ts` overlay this session writes into; a Room grant keeps it. */
20912
+ sessionStateDirs = [];
20913
+ /** What this exact ACP session has already been prompted with (`warm-transcript.ts`). */
20914
+ warmTranscript = new WarmTranscript();
20915
+ /** Local copies already delivered this session, by message id, so transcript renders reuse them. */
20916
+ deliveredAttachments = /* @__PURE__ */ new Map();
20917
+ /** Names from the latest roster read, for ledger bylines the runner writes. */
20918
+ memberNames = /* @__PURE__ */ new Map();
20919
+ /** The request id of the turn that paused on a grant card, until its decision arrives. */
20920
+ pausedOnGrantRequestId;
20810
20921
  /** Operator-local turn traces; built once when the daemon configured a directory. */
20811
20922
  turnTraceSink;
20812
- memberNames = /* @__PURE__ */ new Map();
20813
- /** Agent identities in this Workspace, so a mention can be told from a human's. */
20814
- agentMembers = /* @__PURE__ */ new Set();
20815
- /** The member agent that answered in this corner last (`carriesCorner`). */
20816
- carrier;
20817
- /** The last server check state that started a turn; the same state never starts another. */
20818
- lastChecksState;
20819
20923
  constructor(options) {
20820
20924
  this.options = options;
20821
20925
  this.agent = runtimeIdentity(options.runtime.agent);
20822
- options.grantRunner?.register(options.cornerId, {
20926
+ this.commandContext = new CommandExecutionContext(options.config.agentHomeRoot);
20927
+ this.options = { ...options, api: this.commandContext.bind(options.api) };
20928
+ options.grantRunner?.register(options.roomId, {
20823
20929
  workspaceId: options.workspaceId,
20824
- cwd: options.worktreePath,
20825
- // A corner is the surface with `run-host-command`: its worktree becomes a
20826
- // branch and a pull request, and host work belongs here, next to the
20827
- // transcript that explains it. A granted command runs unwrapped (C94).
20828
- writePolicy: () => ({
20829
- surface: "corner",
20830
- ...this.sessionScratchDir ? { scratch: this.sessionScratchDir } : {}
20831
- }),
20832
- turn: () => this.currentTurn
20930
+ cwd: options.cwd,
20931
+ // A top-level Room keeps its read-only promise for grants too: the runner
20932
+ // wraps the command in this Room's own mount table (C94).
20933
+ writePolicy: () => this.grantWritePolicy(),
20934
+ turn: () => {
20935
+ const turn = this.currentTurnForRunner();
20936
+ return turn ? { ...turn, generationId: this.commandContext.generationId } : void 0;
20937
+ }
20833
20938
  });
20834
20939
  }
20835
20940
  isBusy() {
20836
20941
  return this.busy;
20837
20942
  }
20838
- currentPrincipalCanDrive(_workspaceId, _principalId) {
20839
- return Promise.resolve(true);
20943
+ /** The turn a `request_grant` paused, if any (cleared when its decision resumes it). */
20944
+ pausedGrantRequestId() {
20945
+ return this.pausedOnGrantRequestId;
20840
20946
  }
20841
- refreshPersonaForSoulUpdate() {
20842
- return this.options.scheduler.suspend(this.options.cornerId);
20947
+ /**
20948
+ * What the grant runner may write here: the session's own scratch and home
20949
+ * overlay and nothing else, enforced by the same read-only mount table the
20950
+ * harness runs under. With no usable bwrap there is no way to keep that
20951
+ * promise, so the policy carries no path and the runner refuses the run
20952
+ * rather than widening the boundary.
20953
+ */
20954
+ grantWritePolicy() {
20955
+ return {
20956
+ surface: "room",
20957
+ ...this.options.config.bwrapPath ? { bwrapPath: this.options.config.bwrapPath } : {},
20958
+ ...this.sessionScratchDir ? { scratch: this.sessionScratchDir } : {},
20959
+ ...this.sessionStateDirs.length ? { harnessStateDirs: this.sessionStateDirs } : {},
20960
+ maskPaths: credentialMaskPaths(this.options.config.sandboxMaskPaths, this.options.config.operatorHome ?? homedir6())
20961
+ };
20962
+ }
20963
+ /**
20964
+ * One turn's stopwatch. It is created for every turn — measuring is cheap —
20965
+ * and only WRITES when the daemon configured a runtime directory to write
20966
+ * into, so a standalone or test Body stays silent.
20967
+ */
20968
+ beginTurnTrace(requestId) {
20969
+ const directory = this.options.config.turnTraceDir;
20970
+ if (directory)
20971
+ this.turnTraceSink ??= new TurnTraceFile(directory);
20972
+ return new TurnTrace({
20973
+ surface: "room",
20974
+ agentId: this.agent.publicKey,
20975
+ roomId: this.options.roomId,
20976
+ requestId,
20977
+ ...this.turnTraceSink ? { sink: this.turnTraceSink } : {}
20978
+ });
20979
+ }
20980
+ currentTurnForRunner() {
20981
+ const active = this.activeTurn;
20982
+ if (!active)
20983
+ return void 0;
20984
+ return { requestId: active.item.id, requester: this.requesterOf(active.item.authorId) };
20985
+ }
20986
+ requesterOf(authorId) {
20987
+ const name = this.memberNames.get(authorId);
20988
+ return { pubkey: authorId, ...name ? { name } : {} };
20989
+ }
20990
+ async refreshPersonaForSoulUpdate() {
20991
+ await this.options.scheduler.suspend(this.options.roomId);
20843
20992
  }
20844
20993
  async prepareForForcedUpdateRestart() {
20845
- this.forcedStop = true;
20846
20994
  }
20847
20995
  async forceRecoverRoom() {
20848
20996
  if (this.client && this.sessionId)
20849
20997
  this.client.sessionCancel(this.sessionId);
20850
- await this.options.scheduler.forceSuspend(this.options.cornerId);
20998
+ await this.options.scheduler.forceSuspend(this.options.roomId);
20851
20999
  }
20852
21000
  async roster() {
20853
21001
  const roster = await this.options.api.execute("getWorkspaceRoster", {
@@ -20855,11 +21003,36 @@ var MonolithCornerTurnLoop = class {
20855
21003
  workspaceId: this.options.workspaceId
20856
21004
  });
20857
21005
  this.memberNames = new Map(roster.members.map((member) => [member.identityId, member.name]));
20858
- this.agentMembers = new Set(roster.members.filter((member) => member.kind === "agent").map((member) => member.identityId));
20859
21006
  return roster;
20860
21007
  }
20861
21008
  /**
20862
- * Drop this corner's live harness process. The next activation starts cold.
21009
+ * Whether a picture actually reaches the model this session. Both halves
21010
+ * have to hold (C87): the harness must advertise `promptCapabilities.image`,
21011
+ * AND — when the pin knows the model's modalities — the model must take
21012
+ * images. `undefined` modalities mean the question was never settled, and
21013
+ * the harness answer stands alone as before.
21014
+ */
21015
+ acceptsImages() {
21016
+ if (!(this.client?.canPromptWithImages() ?? false))
21017
+ return false;
21018
+ return this.modelTakesImages ?? true;
21019
+ }
21020
+ /** Download a message's attachments into the session scratch directory once. */
21021
+ async deliver(item) {
21022
+ if (!item.attachments.length || !this.attachmentDir)
21023
+ return [];
21024
+ const cached = this.deliveredAttachments.get(item.id);
21025
+ if (cached)
21026
+ return cached;
21027
+ const delivered = await deliverAttachments(item.attachments, join6(this.attachmentDir, item.id.replace(/[^\w-]/g, "_")), this.options.fetchImpl);
21028
+ this.deliveredAttachments.set(item.id, withoutImageData(delivered));
21029
+ return delivered;
21030
+ }
21031
+ repositoryState() {
21032
+ return this.options.api.execute("getRoomRepositoryState", { roomId: this.options.roomId });
21033
+ }
21034
+ /**
21035
+ * Drop this Room's live harness process. The next activation starts cold.
20863
21036
  * A rotation is a fact about one live session, so the pin goes with it.
20864
21037
  */
20865
21038
  async discardSession() {
@@ -20871,8 +21044,14 @@ var MonolithCornerTurnLoop = class {
20871
21044
  if (client?.isAlive)
20872
21045
  await client.stop();
20873
21046
  }
20874
- /** See `MonolithRoomTurnLoop.sessionIsCurrent`: retention never keeps a
20875
- * session whose persona or model pin the operator has since changed. */
21047
+ /**
21048
+ * Whether the retained session still matches the agent's server-side
21049
+ * configuration. Retention (C104) is a saving only while what it keeps is
21050
+ * still current, and a session's persona and model pin are fixed when it
21051
+ * opens: they cannot be corrected in place, so a changed one has to cost a
21052
+ * respawn. The check is one round trip — the roster half of which the turn
21053
+ * was going to fetch anyway — against a cold spawn measured in seconds.
21054
+ */
20876
21055
  async sessionIsCurrent() {
20877
21056
  return await this.currentSessionFingerprint() === this.sessionFingerprint;
20878
21057
  }
@@ -20880,7 +21059,7 @@ var MonolithCornerTurnLoop = class {
20880
21059
  const [configuration, roster] = await Promise.all([
20881
21060
  this.options.api.execute("getAgentConfiguration", {
20882
21061
  agentId: this.agent.publicKey,
20883
- roomId: this.options.cornerId
21062
+ roomId: this.options.roomId
20884
21063
  }),
20885
21064
  this.roster()
20886
21065
  ]);
@@ -20896,12 +21075,13 @@ var MonolithCornerTurnLoop = class {
20896
21075
  if (this.client?.isAlive && this.sessionId)
20897
21076
  return this.sessionId;
20898
21077
  trace?.noteActivation("cold");
20899
- const [configuration, roster] = await Promise.all([
21078
+ const [configuration, roster, repositoryState] = await Promise.all([
20900
21079
  this.options.api.execute("getAgentConfiguration", {
20901
21080
  agentId: this.agent.publicKey,
20902
- roomId: this.options.cornerId
21081
+ roomId: this.options.roomId
20903
21082
  }),
20904
- this.roster()
21083
+ this.roster(),
21084
+ this.repositoryState()
20905
21085
  ]);
20906
21086
  const self = roster.members.find((member) => member.identityId === this.agent.publicKey);
20907
21087
  const fingerprint = sessionConfigFingerprint({
@@ -20910,7 +21090,8 @@ var MonolithCornerTurnLoop = class {
20910
21090
  soul: configuration.soul ?? self?.soul,
20911
21091
  agentName: self?.name ?? this.agent.name
20912
21092
  });
20913
- await mkdir9(this.options.worktreePath, { recursive: true });
21093
+ const directMessage = Array.isArray(repositoryState.directParticipants) && repositoryState.directParticipants.length === 2;
21094
+ await mkdir10(this.options.cwd, { recursive: true });
20914
21095
  const selection = configuration.model || configuration.effort ? { model: configuration.model, effort: configuration.effort } : this.options.config.modelSelection;
20915
21096
  const homeOverlay = this.options.config.agentHomeRoot ? await prepareRoomAgentHome({
20916
21097
  root: this.options.config.agentHomeRoot,
@@ -20927,26 +21108,7 @@ var MonolithCornerTurnLoop = class {
20927
21108
  })
20928
21109
  }) : {};
20929
21110
  const command = this.options.config.agentCommand ?? this.options.config.agentBinary;
20930
- const repository = this.options.repository;
20931
- let githubEnv = repository ? { GH_TOKEN: repository.githubToken, GITHUB_TOKEN: repository.githubToken } : {};
20932
- if (repository && this.options.config.runtimeConfigPath && this.options.config.agentHomeRoot) {
20933
- const gitBinary = (await execFileAsync3("which", ["git"])).stdout.trim();
20934
- const ghBinary = await execFileAsync3("which", ["gh"]).then((result) => result.stdout.trim()).catch(() => void 0);
20935
- githubEnv = await installCornerGitHubWrappers({
20936
- root: this.options.config.agentHomeRoot,
20937
- runtimeConfigPath: this.options.config.runtimeConfigPath,
20938
- roomId: this.options.parentRoomId,
20939
- cliEntrypoint: process.argv[1],
20940
- gitBinary,
20941
- ...ghBinary ? { ghBinary } : {},
20942
- inheritedPath: this.options.config.agentEnv.PATH ?? process.env.PATH
20943
- });
20944
- }
20945
- const agentEnv = {
20946
- ...this.options.config.agentEnv,
20947
- ...homeOverlay,
20948
- ...githubEnv
20949
- };
21111
+ const agentEnv = { ...this.options.config.agentEnv, ...homeOverlay };
20950
21112
  this.agentEnv = agentEnv;
20951
21113
  const agentArgs = agentArgsWithModelSelection({
20952
21114
  kind: this.options.config.agentKind,
@@ -20955,21 +21117,19 @@ var MonolithCornerTurnLoop = class {
20955
21117
  }, selection);
20956
21118
  const operatorHome = this.options.config.operatorHome ?? homedir6();
20957
21119
  const { stateDirs, tmpDir } = harnessStateDirsFromEnv(agentEnv);
20958
- this.attachmentDir = tmpDir ? join5(tmpDir, "beeline-attachments") : void 0;
21120
+ this.attachmentDir = tmpDir ? join6(tmpDir, "beeline-attachments") : void 0;
20959
21121
  this.sessionScratchDir = tmpDir;
21122
+ this.sessionStateDirs = stateDirs;
20960
21123
  const homeStateDirs = harnessHomeStateDirs(command, agentEnv.HOME ?? operatorHome);
20961
- await Promise.all(homeStateDirs.map((dir) => mkdir9(dir, { recursive: true })));
21124
+ await Promise.all(homeStateDirs.map((dir) => mkdir10(dir, { recursive: true })));
20962
21125
  const attachScratchRoot = this.options.config.agentHomeRoot ?? tmpDir;
20963
21126
  if (attachScratchRoot)
20964
- await mkdir9(attachScratchRoot, { recursive: true });
21127
+ await mkdir10(attachScratchRoot, { recursive: true });
20965
21128
  const spawnCommand = wrapAgentCommand({
20966
21129
  bwrapPath: this.options.config.bwrapPath,
20967
21130
  spec: {
20968
- mode: "edit",
20969
- cwd: this.options.worktreePath,
20970
- worktreePath: this.options.worktreePath,
20971
- ...repository ? { gitCommonDir: repository.gitCommonDir } : {},
20972
- protectedPaths: [this.options.runtime.supervisorRoot],
21131
+ mode: "readonly",
21132
+ cwd: this.options.cwd,
20973
21133
  harnessStateDirs: stateDirs,
20974
21134
  harnessHomeStateDirs: homeStateDirs,
20975
21135
  ...tmpDir ? { tmpDir } : {},
@@ -20979,81 +21139,64 @@ var MonolithCornerTurnLoop = class {
20979
21139
  command,
20980
21140
  args: agentArgs
20981
21141
  });
21142
+ const servers = [
21143
+ readOnlyMcpServer(this.options.config, this.options.cwd),
21144
+ beelineAgentMcpServer(this.options.config, this.options.api, {
21145
+ roomId: this.options.roomId,
21146
+ workspaceId: this.options.workspaceId,
21147
+ attachRoot: this.options.cwd,
21148
+ // The whole per-session overlay, not an enumerated subset: the agent
21149
+ // never picks where a harness writes a file it generates (grok's own
21150
+ // images dir, say), so anything inside the overlay it could possibly
21151
+ // have written must be attachable, whatever subdirectory that is.
21152
+ attachScratchRoot,
21153
+ turnContextPath: this.commandContext.path,
21154
+ directMessage,
21155
+ ...this.options.grantRunnerEndpoint ? { grantRunner: this.options.grantRunnerEndpoint } : {}
21156
+ })
21157
+ ];
21158
+ await installPiMcpBridge({
21159
+ agentCommand: command,
21160
+ piHome: agentEnv.PI_CODING_AGENT_DIR,
21161
+ servers
21162
+ });
21163
+ const mountedServers = servers.map((server) => server.name);
20982
21164
  const clientOptions = {
20983
21165
  agentCommand: spawnCommand.command,
20984
21166
  agentArgs: spawnCommand.args,
20985
21167
  agentEnv,
20986
- agentCwd: this.options.worktreePath,
21168
+ agentCwd: this.options.cwd,
20987
21169
  agentLabel: command,
20988
- autoApprovePermissions: true,
20989
- permissionHandler: () => Promise.resolve("allow")
21170
+ // `bwrapPath` is set only when `detectBwrapSandbox` passed its self-test
21171
+ // (`config.ts`), which is exactly when `wrapAgentCommand` above wraps.
21172
+ osSandbox: Boolean(this.options.config.bwrapPath),
21173
+ autoApprovePermissions: false,
21174
+ permissionAllowlist: (request) => isRoomMcpPermissionRequest(request, mountedServers)
20990
21175
  };
20991
21176
  this.client = (this.options.createAcpClient ?? ((value) => new AcpClient(value)))(clientOptions);
20992
21177
  await this.client.start();
20993
- const servers = [
20994
- ...repository ? [
20995
- {
20996
- name: "buzz-dev-mcp",
20997
- command: this.options.config.mcpBinary,
20998
- args: [],
20999
- // ACP hosts launch stdio MCP servers with an explicit, sanitized env.
21000
- // This token is minted for this exact corner and is also the credential
21001
- // helper's password source, so its shell commands need the same scope as
21002
- // the corner harness without inheriting any host credentials.
21003
- env: [
21004
- { name: "GH_TOKEN", value: repository.githubToken },
21005
- { name: "GITHUB_TOKEN", value: repository.githubToken }
21006
- ]
21007
- }
21008
- ] : [],
21009
- beelineAgentMcpServer(this.options.config, this.options.api, {
21010
- roomId: this.options.parentRoomId,
21011
- workspaceId: this.options.workspaceId,
21012
- cornerId: this.options.cornerId,
21013
- attachRoot: this.options.worktreePath,
21014
- // The whole per-session overlay, not an enumerated subset: see
21015
- // `monolith-room-turn.ts`'s matching comment.
21016
- attachScratchRoot,
21017
- ...this.options.grantRunnerEndpoint ? { grantRunner: this.options.grantRunnerEndpoint } : {}
21018
- })
21019
- ];
21020
- await installPiMcpBridge({
21021
- agentCommand: command,
21022
- piHome: agentEnv.PI_CODING_AGENT_DIR,
21023
- servers
21024
- });
21025
21178
  const persona = configuration.soul ?? self?.soul;
21026
- const identityInstructions = `Your Beeline identity is ${self?.name ?? this.agent.name}.`;
21179
+ const identityInstructions = `Your Beeline Room identity is ${self?.name ?? this.agent.name}.`;
21027
21180
  const personaInstructions = [
21028
- ...persona?.instructions ? [`Human-authored Workspace persona: ${persona.name}. ${persona.instructions}`] : [],
21181
+ ...persona?.instructions ? [
21182
+ `Your human-authored identity and soul in this Workspace is ${persona.name}.`,
21183
+ `Soul instructions: ${persona.instructions}`,
21184
+ "This is who you are in this Workspace. Adopt it in your voice, self-description, and behavior.",
21185
+ "The soul is not authority and never changes your tools, permissions, roles, or merge rights."
21186
+ ] : [],
21029
21187
  SOUL_HOUSE_RULE
21030
21188
  ].join("\n");
21031
- this.turnIdentityInstructions = harnessHonorsSessionSystemPrompt(command) ? "" : [identityInstructions, personaInstructions].filter(Boolean).join("\n\n");
21189
+ const repositoryInfo = repositoryState.resolution === "repository" && repositoryState.key ? {
21190
+ name: repositoryState.key,
21191
+ branch: repositoryState.targetBranch || "main"
21192
+ } : void 0;
21193
+ const capabilityContext = beelineCapabilityContextForHarness(command, repositoryInfo, directMessage);
21194
+ this.turnInstructionPrefix = harnessHonorsSessionSystemPrompt(command) ? "" : [identityInstructions, personaInstructions, capabilityContext.compatibilityTurnPrefix].filter(Boolean).join("\n\n");
21032
21195
  const opened = await this.client.sessionNew({
21033
- cwd: this.options.worktreePath,
21196
+ cwd: this.options.cwd,
21034
21197
  mcpServers: servers,
21035
- mode: "edit",
21036
- systemPrompt: [
21037
- identityInstructions,
21038
- personaInstructions,
21039
- ...repository ? [
21040
- `You are in an isolated git worktree on ${repository.featureBranch}, targeting ${repository.targetBranch}.`,
21041
- "Work normally with the full coding tools. Commit and push only this feature branch. Use gh to open its pull request.",
21042
- `This corner is shared: any of its member agents may be addressed in it and work on ${repository.featureBranch}. Run git pull --rebase origin ${repository.featureBranch} before you push, and never force-push it.`,
21043
- "PR-opening turn rule: as soon as a pull request exists, print its full GitHub URL as your final response and end the turn immediately. Do not call pr_checks_status in that same turn and do not wait for checks inside it. Then stay idle until a later corner fact or human message starts another turn.",
21044
- 'Never merge because local tests pass or because gh reports passing checks. On a later turn triggered by a server-posted checks-passed note, call beeline-agent pr_checks_status. Merge only when it returns checks="passed", held=false, and approvalPending=false.',
21045
- "Merge the PR yourself only after the checks-passed event shows every check green; if any check failed or is still running, say exactly which and stop - never merge red.",
21046
- "If any human in this corner says hold or do not merge, do not merge until a later human explicitly resumes it.",
21047
- "Do not tag the user when a corner turn finishes: the server posts the merge summary card and its push already cover completion. Tag a human only mid-turn, and only when you need a decision or input.",
21048
- 'GitHub check and merge notes are server lines already in the corner: never restate them (no "checks passed", "CI is green", "PR ready for review"). On a checks turn, say nothing unless you act - a merge or a pushed fix - and then one short line about that.',
21049
- "A human approval in the app asks the server to merge. When approval is pending, wait for the server close request instead of racing it with gh. If checks passed, no hold exists, and no approval is pending, merge the pull request yourself with gh.",
21050
- "Never push directly to the target branch. Never merge a different pull request."
21051
- ] : [
21052
- "This is a chat-only corner with no repository or GitHub workflow.",
21053
- "Work in this corner's writable workspace. Use write_scratch_file or ordinary tools to create files, then attach_file to send them back to the corner.",
21054
- "Do not initialize a repository, create a branch, push, open a pull request, or wait for GitHub checks."
21055
- ]
21056
- ].filter(Boolean).join("\n\n")
21198
+ mode: "readonly",
21199
+ systemPrompt: [identityInstructions, personaInstructions, capabilityContext.sessionPrompt].filter(Boolean).join("\n\n")
21057
21200
  });
21058
21201
  this.sessionId = opened.sessionId;
21059
21202
  this.sessionFingerprint = fingerprint;
@@ -21063,7 +21206,12 @@ var MonolithCornerTurnLoop = class {
21063
21206
  }
21064
21207
  return opened.sessionId;
21065
21208
  }
21066
- /** The scheduler seam: `queue-wait` closes when a slot buys a session. */
21209
+ /**
21210
+ * The scheduler seam, and the only place that can see the boundary between
21211
+ * waiting for a slot and spawning a harness: `queue-wait` closes the instant
21212
+ * `activate()` is called, and `cold` vs `warm` is decided by whether this
21213
+ * Room already holds a live ACP client.
21214
+ */
21067
21215
  lifecycle(trace) {
21068
21216
  return {
21069
21217
  activate: async () => {
@@ -21082,12 +21230,6 @@ var MonolithCornerTurnLoop = class {
21082
21230
  suspend: () => this.discardSession()
21083
21231
  };
21084
21232
  }
21085
- /** A picture reaches the model only if the harness AND the model take one (C87). */
21086
- acceptsImages() {
21087
- if (!(this.client?.canPromptWithImages() ?? false))
21088
- return false;
21089
- return this.modelTakesImages ?? true;
21090
- }
21091
21233
  /** The pinned providers a failure reason should name for this session. */
21092
21234
  servingProviders() {
21093
21235
  return this.pinnedProviderOverride ? [this.pinnedProviderOverride] : this.pinnedProviders;
@@ -21106,7 +21248,8 @@ var MonolithCornerTurnLoop = class {
21106
21248
  /**
21107
21249
  * Re-pin the session to the next provider in the OpenRouter order and open a
21108
21250
  * fresh session on it, so the retry of an empty completion is served — and
21109
- * named — by exactly one provider (C92).
21251
+ * named — by exactly one provider. Undefined when the pin has nowhere left
21252
+ * to go.
21110
21253
  */
21111
21254
  async repinNextProvider(trace, reason) {
21112
21255
  const next = nextPinnedProvider(this.pinnedProviders, this.pinnedProviderOverride);
@@ -21123,523 +21266,418 @@ var MonolithCornerTurnLoop = class {
21123
21266
  await (trace ? trace.measure("activation", () => this.activate(trace)) : this.activate());
21124
21267
  return next;
21125
21268
  }
21126
- /** One turn's stopwatch; writes only when the daemon configured a trace directory. */
21127
- beginTurnTrace(requestId) {
21128
- const directory = this.options.config.turnTraceDir;
21129
- if (directory)
21130
- this.turnTraceSink ??= new TurnTraceFile(directory);
21131
- return new TurnTrace({
21132
- surface: "corner",
21133
- agentId: this.agent.publicKey,
21134
- roomId: this.options.cornerId,
21135
- requestId,
21136
- ...this.turnTraceSink ? { sink: this.turnTraceSink } : {}
21269
+ startPrompt(item) {
21270
+ const active = {
21271
+ item,
21272
+ steers: [],
21273
+ steerTail: Promise.resolve(),
21274
+ resumeRequested: false,
21275
+ cancelled: false,
21276
+ phase: "prompting",
21277
+ promise: Promise.resolve()
21278
+ };
21279
+ this.activeTurn = active;
21280
+ active.promise = this.prompt(active).catch((error) => {
21281
+ this.options.health.failure(1e3);
21282
+ console.error(`[thin-core] monolith Room ${this.options.roomId} turn failed:`, error);
21283
+ }).finally(() => {
21284
+ if (this.activeTurn === active) {
21285
+ this.activeTurn = void 0;
21286
+ this.wakeIntake?.();
21287
+ this.wakeIntake = void 0;
21288
+ }
21137
21289
  });
21138
21290
  }
21139
- async prompt(requestId, trigger, attachments = [], requestedById, restates) {
21140
- const { api, cornerId } = this.options;
21141
- const spoken = (text2) => restates && isCornerStatusRestatement(text2, restates) ? "" : text2;
21142
- const requester = requestedById ? {
21143
- pubkey: requestedById,
21144
- ...this.memberNames.get(requestedById) ? { name: this.memberNames.get(requestedById) } : {}
21145
- } : void 0;
21146
- this.currentTurn = { requestId, ...requester ? { requester } : {} };
21147
- this.carrier = this.agent.publicKey;
21148
- const trace = this.beginTurnTrace(requestId);
21291
+ /**
21292
+ * Obey a stop the requester already made a fact.
21293
+ *
21294
+ * A stop names ONE request id and touches only the turn that answers it. The
21295
+ * turn in flight is cancelled at the harness and marked so its own run
21296
+ * publishes nothing; a turn still queued is simply dropped, since starting
21297
+ * work the person has already withdrawn is worse than never starting it.
21298
+ * A stop for neither is ignored — it belongs to a turn that ended between
21299
+ * the press and the delivery, and the server's own receipt already said so.
21300
+ */
21301
+ stopTurn(requestId) {
21302
+ for (let index = this.queuedTurns.length - 1; index >= 0; index -= 1)
21303
+ if (this.queuedTurns[index].id === requestId)
21304
+ this.queuedTurns.splice(index, 1);
21305
+ const active = this.activeTurn;
21306
+ if (!active || active.item.id !== requestId)
21307
+ return;
21308
+ active.cancelled = true;
21309
+ if (this.client && this.sessionId)
21310
+ this.client.sessionCancel(this.sessionId);
21311
+ }
21312
+ async prompt(active) {
21313
+ const { item } = active;
21314
+ const api = this.options.api;
21315
+ this.busy = true;
21316
+ const trace = this.beginTurnTrace(item.id);
21149
21317
  try {
21318
+ if (!this.memberNames.has(item.authorId))
21319
+ await this.roster().catch(() => void 0);
21150
21320
  await withTurnReceiptHeartbeat(api, {
21151
21321
  agentId: this.agent.publicKey,
21152
- roomId: cornerId,
21153
- requestId,
21154
- generationId: `${this.agent.publicKey}:${cornerId}`
21155
- }, () => {
21322
+ roomId: this.options.roomId,
21323
+ requestId: item.id,
21324
+ generationId: this.commandContext.generationId
21325
+ }, async () => {
21326
+ await api.execute("postAgentActivity", {
21327
+ agentId: this.agent.publicKey,
21328
+ roomId: this.options.roomId,
21329
+ requestId: item.id,
21330
+ activity: [
21331
+ {
21332
+ kind: "thinking",
21333
+ title: "Working",
21334
+ status: "in_progress",
21335
+ requestedBy: this.requesterOf(item.authorId)
21336
+ }
21337
+ ]
21338
+ });
21156
21339
  trace.noteScheduler("queue", this.options.scheduler.snapshot());
21157
21340
  trace.start("queue-wait");
21158
- return this.options.scheduler.run(cornerId, this.lifecycle(trace), async () => {
21341
+ await this.options.scheduler.run(this.options.roomId, this.lifecycle(trace), async () => {
21159
21342
  trace.end("queue-wait");
21160
21343
  trace.noteScheduler("admission", this.options.scheduler.snapshot());
21161
- if (this.forcedStop)
21162
- throw new Error("corner turn stopped for daemon handoff");
21163
- this.busy = true;
21164
- await this.syncBranch();
21165
21344
  const [conversation, roster, delivered] = await trace.measure("context-fetch", () => Promise.all([
21166
- api.execute("getRoomConversation", { roomId: cornerId, limit: 200 }),
21345
+ api.execute("getRoomConversation", { roomId: this.options.roomId, limit: 200 }),
21167
21346
  this.roster(),
21168
- this.attachmentDir && attachments.length ? deliverAttachments(attachments, join5(this.attachmentDir, requestId.replace(/[^\w-]/g, "_")), this.options.fetchImpl) : Promise.resolve([])
21347
+ this.deliver(item)
21169
21348
  ]));
21170
21349
  const names = new Map(roster.members.map((member) => [member.identityId, member.name]));
21171
- const requestedBy = requester && !requester.name && names.get(requester.pubkey) ? { ...requester, name: names.get(requester.pubkey) } : requester;
21172
- if (requestedBy)
21173
- this.currentTurn = { requestId, requester: requestedBy };
21174
- const transcriptRows = conversation.items.slice(-120).map((message) => ({
21350
+ const transcriptRows = conversation.items.filter((message) => message.type === "message" && message.id !== item.id && !active.steers.some((steerItem) => steerItem.id === message.id)).slice(-80).map((message) => ({
21175
21351
  id: message.id,
21176
- line: `${names.get(message.authorId) ?? "Beeline"} [${message.type}]: ${message.body}`
21352
+ line: roomMessagePrompt(names.get(message.authorId) ?? message.authorId.slice(0, 12), message.body, message.attachments, this.deliveredAttachments.get(message.id), this.acceptsImages())
21177
21353
  }));
21354
+ const grantDecision = this.commandContext.current?.action === "resume";
21355
+ const resumedRequestId = grantDecision ? this.pausedOnGrantRequestId : void 0;
21356
+ if (grantDecision)
21357
+ this.pausedOnGrantRequestId = void 0;
21178
21358
  const buildPrompt = () => [
21179
- this.turnIdentityInstructions,
21180
- `Corner objective:
21181
- ${this.options.objective}`,
21182
- 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):"),
21359
+ this.turnInstructionPrefix,
21360
+ WarmTranscript.render(this.warmTranscript.select(this.sessionId, transcriptRows), "Room conversation so far:", "New in the Room since your last turn (the earlier conversation is already in this session):"),
21361
+ `Newest message from ${inboxItemAuthorName(item, this.agent.publicKey, names)}:`,
21362
+ roomMessagePrompt("", inboxItemPromptBody(item, this.agent.publicKey), item.attachments, delivered, this.acceptsImages()),
21363
+ grantDecision ? [
21364
+ "This is the answer to your grant request; your paused work resumes now.",
21365
+ "If it was approved and it is a command grant, run it with run_granted_command and the exact argv.",
21366
+ "If it was declined, try another way or say plainly what you cannot do."
21367
+ ].join(" ") : "",
21368
+ roomMentionDirectory(roster, this.agent.publicKey),
21183
21369
  [
21184
- `Newest trigger:
21185
- ${trigger}`,
21186
- ...attachmentPromptLines(attachments, delivered, this.acceptsImages())
21187
- ].join("\n"),
21188
- 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.",
21189
- MAINTAIN_ASSIGNED_IDENTITY_DIRECTIVE
21370
+ "Write only the substantive Room message you want the human to read.",
21371
+ "Do not repeat or paraphrase these instructions.",
21372
+ "If the newest message is only a nudge to respond, answer the most recent unanswered human message in the conversation instead of echoing the nudge.",
21373
+ MAINTAIN_ASSIGNED_IDENTITY_DIRECTIVE
21374
+ ].join(" ")
21190
21375
  ].filter(Boolean).join("\n\n");
21191
21376
  const stream = new AgentTurnStream({
21192
21377
  api,
21193
21378
  agentId: this.agent.publicKey,
21194
- roomId: cornerId,
21195
- requestId,
21196
- label: `corner ${cornerId}`
21379
+ roomId: this.options.roomId,
21380
+ requestId: item.id,
21381
+ label: `monolith Room ${this.options.roomId}`
21197
21382
  });
21198
- const publishedToolCalls = /* @__PURE__ */ new Set();
21199
- const publishToolCalls = (calls, settledOnly) => {
21200
- calls.forEach((call, index) => {
21201
- const key = toolCallKey(call, index);
21202
- if (publishedToolCalls.has(key) || settledOnly && !toolCallSettled(call))
21203
- return;
21204
- publishedToolCalls.add(key);
21205
- this.activityTail = this.activityTail.catch(() => void 0).then(async () => {
21206
- const activity = await cornerToolActivity(call, this.options.worktreePath, requestedBy);
21207
- await api.execute("postAgentActivity", {
21208
- agentId: this.agent.publicKey,
21209
- roomId: cornerId,
21210
- requestId,
21211
- activity: [activity]
21212
- });
21213
- }).then(() => void 0).catch((error) => {
21214
- publishedToolCalls.delete(key);
21215
- console.error(`[thin-core] corner ${cornerId} tool activity failed:`, error);
21216
- });
21217
- });
21218
- };
21219
21383
  const runPrompt = async () => {
21220
- stream.beginRun();
21221
- trace.promptSent();
21222
- return this.client.sessionPrompt(this.sessionId, promptWithImages(buildPrompt(), attachmentImageBlocks(delivered, this.acceptsImages())), 12e4, (delta, full) => {
21223
- trace.firstModelOutput();
21224
- stream.onChunk(delta, full);
21225
- }, void 0, (calls) => {
21226
- trace.toolCalls(calls);
21227
- publishToolCalls(calls, true);
21228
- });
21229
- };
21230
- let result = await runPrompt();
21231
- trace.promptSettled();
21232
- let explained = await this.explainEmpty(result);
21233
- if (explained && !restates && shouldRetryEmptyTurn(explained)) {
21234
- const silent = this.servingProviders();
21235
- const next = await this.repinNextProvider(trace, explained.reason);
21236
- if (next) {
21237
- console.warn(`[thin-core] corner ${cornerId} turn ${requestId}: ${turnFailureReasonWithProvider(explained.reason, silent)}; retrying on ${next}`);
21238
- result = await runPrompt();
21239
- trace.promptSettled();
21240
- explained = await this.explainEmpty(result);
21241
- }
21242
- }
21243
- await this.activityTail;
21244
- publishToolCalls(result.toolCalls, false);
21245
- for (const call of result.toolCalls) {
21246
- const failure = toolCallFailureLine(call);
21247
- if (failure)
21248
- console.warn(`[thin-core] corner ${cornerId} ${failure}`);
21249
- }
21250
- await this.activityTail;
21251
- stream.close();
21252
- let reply = durableReplyText(result.agentText);
21384
+ let nextPrompt = promptWithImages(buildPrompt(), attachmentImageBlocks(delivered, this.acceptsImages()));
21385
+ let result2;
21386
+ for (; ; ) {
21387
+ let promptError;
21388
+ try {
21389
+ stream.beginRun();
21390
+ trace.promptSent();
21391
+ result2 = await this.client.sessionPrompt(this.sessionId, nextPrompt, 12e4, (delta, full) => {
21392
+ trace.firstModelOutput();
21393
+ stream.onChunk(delta, full);
21394
+ }, void 0, (calls) => trace.toolCalls(calls));
21395
+ } catch (error) {
21396
+ promptError = error;
21397
+ }
21398
+ const settledSteerTail = active.steerTail;
21399
+ await settledSteerTail;
21400
+ if (settledSteerTail !== active.steerTail)
21401
+ continue;
21402
+ if (!active.resumeRequested) {
21403
+ if (promptError)
21404
+ throw promptError;
21405
+ break;
21406
+ }
21407
+ active.resumeRequested = false;
21408
+ nextPrompt = [
21409
+ "The previous run was cancelled because its harness could not accept every live steer.",
21410
+ "Resume the same turn. Keep the original request and everything that happened before it was cancelled.",
21411
+ "Human messages that arrived after the original request, in transcript order:",
21412
+ ...active.steers.map((steerItem) => roomMessagePrompt(steerItem.authorId.slice(0, 12), steerItem.body, steerItem.attachments, this.deliveredAttachments.get(steerItem.id), this.acceptsImages())),
21413
+ "Continue now and answer the updated request without erasing the earlier context."
21414
+ ].join("\n\n");
21415
+ }
21416
+ return result2;
21417
+ };
21418
+ let result = await runPrompt();
21419
+ trace.promptSettled();
21420
+ let explained = await this.explainEmpty(result);
21421
+ if (explained && shouldRetryEmptyTurn(explained)) {
21422
+ const silent = this.servingProviders();
21423
+ const next = await this.repinNextProvider(trace, explained.reason);
21424
+ if (next) {
21425
+ console.warn(`[thin-core] monolith Room ${this.options.roomId} turn ${item.id}: ${turnFailureReasonWithProvider(explained.reason, silent)}; retrying on ${next}`);
21426
+ result = await runPrompt();
21427
+ trace.promptSettled();
21428
+ explained = await this.explainEmpty(result);
21429
+ }
21430
+ }
21431
+ if (active.cancelled) {
21432
+ stream.close();
21433
+ throw new TurnStoppedError("turn stopped by the requester");
21434
+ }
21435
+ active.phase = "finishing";
21436
+ if (result.toolCalls.some((call) => pendingGrantToolCall(call))) {
21437
+ this.pausedOnGrantRequestId = item.id;
21438
+ console.log(`[thin-core] monolith Room ${this.options.roomId} turn ${item.id} paused on a grant card`);
21439
+ } else if (resumedRequestId) {
21440
+ console.log(`[thin-core] monolith Room ${this.options.roomId} turn ${resumedRequestId} resumed by grant decision ${item.id}`);
21441
+ }
21442
+ const openCornerCall = result.toolCalls.find((call) => /(?:^|[._:/-])open_corner$/i.test(call.title ?? ""));
21443
+ if (openCornerCall) {
21444
+ console.log(`[thin-core] monolith Room ${this.options.roomId} tool call: ${openCornerCall.title} (${openCornerCall.status ?? "no status"})`);
21445
+ if (!isFailedToolCall(openCornerCall))
21446
+ this.options.onCornerOpened?.();
21447
+ }
21448
+ for (const call of result?.toolCalls ?? []) {
21449
+ const failure = toolCallFailureLine(call);
21450
+ if (failure) {
21451
+ console.warn(`[thin-core] monolith Room ${this.options.roomId} ${failure}`);
21452
+ }
21453
+ }
21454
+ stream.close();
21455
+ let reply = durableReplyText(result.agentText);
21253
21456
  if (!reply && explained) {
21254
21457
  reply = explained.recoveredText ? durableReplyText(explained.recoveredText) : "";
21255
- if (!reply && !(restates && !isAccountOrProviderRefusal(explained.record))) {
21458
+ if (!reply) {
21256
21459
  throw new Error(turnFailureReasonWithProvider(explained.reason, this.servingProviders()));
21257
21460
  }
21258
- console.warn(`[thin-core] corner ${cornerId} turn ${requestId}: ${explained.reason}`);
21461
+ console.warn(`[thin-core] monolith Room ${this.options.roomId} turn ${item.id}: ${explained.reason}`);
21259
21462
  }
21260
- await trace.measure("publish", () => stream.settle(spoken(reply)));
21261
- }, { priority: "interactive", roomKey: cornerId });
21262
- }, (error) => console.error(`[thin-core] corner ${cornerId} receipt heartbeat failed:`, error));
21463
+ if (openCornerCall && !isFailedToolCall(openCornerCall)) {
21464
+ reply = stripCornerOpenEcho(reply);
21465
+ }
21466
+ const mentionIds = reply ? agentReplyMentionIds(reply, roster, this.agent.publicKey) : [];
21467
+ await trace.measure("publish", () => stream.settle(reply, reply ? {
21468
+ triggerMessageId: item.id,
21469
+ mentionIds
21470
+ } : {}));
21471
+ }, { priority: "interactive", roomKey: this.options.roomId });
21472
+ }, (error) => console.error(`[thin-core] monolith Room ${this.options.roomId} receipt heartbeat failed:`, error));
21263
21473
  await api.execute("postAgentTurnReceipt", {
21264
21474
  agentId: this.agent.publicKey,
21265
- roomId: cornerId,
21266
- requestId,
21475
+ roomId: this.options.roomId,
21476
+ requestId: item.id,
21267
21477
  status: "complete",
21268
- generationId: `${this.agent.publicKey}:${cornerId}`
21478
+ generationId: this.commandContext.generationId
21269
21479
  });
21270
21480
  await trace.finish("complete");
21271
21481
  } catch (error) {
21482
+ if (error instanceof TurnStoppedError || active.cancelled) {
21483
+ console.log(`[thin-core] monolith Room ${this.options.roomId} turn ${item.id} stopped by the requester`);
21484
+ await trace.finish("cancelled");
21485
+ return;
21486
+ }
21272
21487
  const reason = distillTurnFailureReason(error);
21273
21488
  await api.execute("postAgentTurnReceipt", {
21274
21489
  agentId: this.agent.publicKey,
21275
- roomId: cornerId,
21276
- requestId,
21490
+ roomId: this.options.roomId,
21491
+ requestId: item.id,
21277
21492
  status: "failed",
21278
- generationId: `${this.agent.publicKey}:${cornerId}`,
21493
+ generationId: this.commandContext.generationId,
21279
21494
  reason
21280
21495
  });
21281
21496
  await trace.finish("failed", reason);
21282
21497
  throw error;
21283
21498
  } finally {
21284
21499
  this.busy = false;
21285
- this.currentTurn = void 0;
21286
21500
  }
21287
21501
  }
21288
- /** Whether this agent opened the corner. A corner with no recorded opener
21289
- * behaves exactly as it did before members could carry it. */
21290
- isOpener() {
21291
- return !this.options.openedBy || this.options.openedBy === this.agent.publicKey;
21292
- }
21293
- /**
21294
- * Whether this agent is the one carrying the corner right now.
21295
- *
21296
- * Every member agent polls the corner, but its lifecycle — a server check
21297
- * note, a close request answered with work — is ONE fact and must start ONE
21298
- * turn, not one per member (the "one check turn per changed server state"
21299
- * rule). The carrier is whoever answered in the corner last, which is the
21300
- * opener until a human hands the work to someone else.
21301
- */
21302
- carriesCorner() {
21303
- return (this.carrier ?? this.options.openedBy ?? this.agent.publicKey) === this.agent.publicKey;
21304
- }
21305
- /** Notes an agent's durable message as the corner changing hands. */
21306
- noteCarrier(authorId) {
21307
- if (this.agentMembers.has(authorId))
21308
- this.carrier = authorId;
21309
- }
21310
- /**
21311
- * Whether a human message in this corner is addressed to THIS agent.
21312
- *
21313
- * A corner now runs like a Room — every member agent polls it — so an
21314
- * unrouted message would start one turn per member on one branch. A mention
21315
- * routes: the mentioned agent answers and nobody else. A message that names
21316
- * no agent at all keeps the old behaviour and falls to the opener, which is
21317
- * every single-agent corner ever opened.
21318
- */
21319
- async addressesThisAgent(item) {
21320
- if (item.mentionIds.includes(this.agent.publicKey))
21321
- return true;
21322
- if (!item.mentionIds.length)
21323
- return this.carriesCorner();
21324
- await this.roster().catch(() => void 0);
21325
- return item.mentionIds.some((id) => this.agentMembers.has(id)) ? false : this.carriesCorner();
21326
- }
21327
- /**
21328
- * Bring this worktree onto the corner's branch as GitHub currently has it,
21329
- * before any work is done on top of it.
21330
- *
21331
- * The branch is the shared artifact: another member agent may have pushed to
21332
- * it since this helper last looked, and a first touch of a corner this
21333
- * helper did not open starts from whatever `room-runtime.ts` restored. A
21334
- * divergence this cannot rebase away is raised, not pushed over — the turn
21335
- * fails with that sentence and the server inscribes it in the corner.
21336
- */
21337
- async syncBranch() {
21338
- const repository = this.options.repository;
21339
- if (!repository)
21340
- return;
21341
- const token = await this.options.api.execute("getRoomGitHubToken", { roomId: this.options.parentRoomId }).then((granted) => granted.token).catch(() => repository.githubToken);
21342
- await syncCornerBranch({
21343
- worktreePath: this.options.worktreePath,
21344
- featureBranch: repository.featureBranch,
21345
- env: { ...process.env, GH_TOKEN: token, GITHUB_TOKEN: token, GIT_TERMINAL_PROMPT: "0" }
21346
- });
21347
- }
21348
- /** The server's check state for this head, or the notes' own verdict when the server carries none. */
21349
- async checksState(notes) {
21350
- try {
21351
- const restore = await this.options.api.execute("getCornerRestoreState", {
21352
- cornerId: this.options.cornerId
21353
- });
21354
- const fromServer = checksStateFromLifecycle(restore.lifecycle);
21355
- if (fromServer)
21356
- return fromServer;
21357
- } catch (error) {
21358
- console.error(`[thin-core] corner ${this.options.cornerId} check state read failed:`, error);
21359
- }
21360
- return notes.some((note) => completedCheckNote(note) === "failed") ? "failing" : "passing";
21361
- }
21362
21502
  async run() {
21363
- const { api, cornerId, signal } = this.options;
21364
- const activation = await api.execute("getRoomInbox", {
21365
- roomId: cornerId,
21366
- startAtLatest: true
21367
- });
21368
- let cursor3 = activation.cursor;
21369
- const processedInboxIds = /* @__PURE__ */ new Set();
21370
- const pushedInbox = [];
21371
- let pendingPushedCursor;
21372
- let liveConnected = false;
21373
- const rewindSupported = Array.isArray(activation.rewindIds);
21374
- for (const id of activation.rewindIds ?? [])
21375
- processedInboxIds.add(id);
21376
- const stopLive = api.liveSubscribe?.(cornerId, cursor3, (items, pushedCursor) => {
21377
- pushedInbox.push(...items);
21378
- pendingPushedCursor = laterInboxCursor(pendingPushedCursor, pushedCursor);
21379
- this.wakeIntake?.();
21380
- this.wakeIntake = void 0;
21381
- }, (connected, capabilities) => {
21382
- liveConnected = connected && capabilities?.pushIntake === true;
21383
- this.wakeIntake?.();
21384
- this.wakeIntake = void 0;
21385
- }, {
21386
- ...this.options.config.daemonReleaseVersion ? { releaseVersion: this.options.config.daemonReleaseVersion } : {},
21387
- ...this.options.config.daemonSourceSha ? { sourceSha: this.options.config.daemonSourceSha } : {},
21388
- available: !this.options.config.modelUnavailable
21389
- });
21390
- const history = await api.execute("getRoomConversation", { roomId: cornerId, limit: 200 });
21391
- await this.roster().catch(() => void 0);
21392
- for (const item of history.items)
21393
- if (item.type === "message")
21394
- this.noteCarrier(item.authorId);
21395
- const durableAgentReplies = history.items.filter((item) => item.type === "message" && item.authorId === this.agent.publicKey && item.body.trim() !== this.options.objective.trim());
21396
- if (durableAgentReplies.length === 0 && this.isOpener()) {
21397
- await this.prompt(history.items.find((item) => item.requestId)?.requestId ?? cornerId.replaceAll("-", ""), this.options.objective);
21398
- }
21503
+ const { api, roomId, signal } = this.options;
21399
21504
  try {
21400
- let pollWithoutWait = false;
21401
- while (!signal?.aborted) {
21402
- try {
21403
- const pollNow = pushedInbox.length > 0 || !liveConnected || this.reconciliationRequested;
21404
- const inbox = !pollNow ? { items: [], cursor: void 0, closeRequested: false } : await api.execute("getCornerCloseRequests", {
21405
- cornerId,
21406
- ...cursor3 ? { after: cursor3 } : {},
21407
- ...rewindSupported ? { rewind: true } : {}
21408
- });
21409
- if (pollNow) {
21410
- this.reconciliationRequested = false;
21411
- }
21412
- if (inbox.closeRequested) {
21413
- await this.options.onCloseRequested();
21414
- return;
21415
- }
21416
- const checkNotes = [];
21417
- const delivered = [...pushedInbox.splice(0), ...inbox.items];
21418
- for (const item of delivered) {
21419
- if (processedInboxIds.has(item.id))
21420
- continue;
21421
- processedInboxIds.add(item.id);
21422
- while (processedInboxIds.size > 1e4)
21423
- processedInboxIds.delete(processedInboxIds.values().next().value);
21424
- if (item.type === "message") {
21425
- this.noteCarrier(item.authorId);
21426
- if (item.authorId === this.agent.publicKey)
21427
- continue;
21428
- if (!await this.addressesThisAgent(item))
21429
- continue;
21430
- const authority = await api.execute("getRoomAuthority", {
21431
- roomId: cornerId,
21432
- principalId: item.authorId
21433
- });
21434
- if (!authority.member || authority.principalKind !== "human")
21435
- continue;
21436
- await this.prompt(item.id, item.body, item.attachments, item.authorId);
21437
- pollWithoutWait = true;
21438
- continue;
21439
- }
21440
- const grantDecision = item.type === "system" && item.mentionIds.includes(this.agent.publicKey) && parseGrantDecisionLine(item.body) !== void 0;
21441
- if (grantDecision) {
21442
- await this.prompt(item.id, `${item.body}
21443
- This answers your grant request; resume the paused work. If approved and it is a command grant, run it with run_granted_command and the exact argv; if declined, try another way or say what you cannot do.`, [], item.authorId);
21444
- pollWithoutWait = true;
21445
- continue;
21446
- }
21447
- if (isCheckStartNote(item)) {
21448
- this.lastChecksState = void 0;
21449
- continue;
21450
- }
21451
- if (completedCheckNote(item))
21452
- checkNotes.push(item);
21453
- }
21454
- if (checkNotes.length && this.carriesCorner()) {
21455
- const state = await this.checksState(checkNotes);
21456
- if (state && state !== "pending" && state !== this.lastChecksState) {
21457
- this.lastChecksState = state;
21458
- const lines = checkNotes.map((note) => note.body);
21459
- await this.prompt(checkNotes[checkNotes.length - 1].id, lines.join("\n"), [], void 0, lines);
21460
- pollWithoutWait = true;
21461
- }
21462
- }
21463
- cursor3 = laterInboxCursor(cursor3, laterInboxCursor(inbox.cursor, pendingPushedCursor));
21464
- pendingPushedCursor = void 0;
21465
- api.updateLiveCursor?.(cornerId, cursor3);
21466
- if (pollNow)
21467
- this.options.onPoll();
21468
- await Promise.race([
21469
- wait(pollWithoutWait ? 0 : liveConnected ? 2147483647 : this.options.pollMs ?? cornerClosePollMs(), signal),
21470
- pushedInbox.length ? Promise.resolve() : new Promise((resolve30) => {
21471
- this.wakeIntake = resolve30;
21472
- })
21473
- ]);
21474
- pollWithoutWait = false;
21475
- } catch (error) {
21476
- if (signal?.aborted)
21477
- break;
21478
- this.options.onFailure(1e3);
21479
- console.error(`[thin-core] corner ${cornerId} turn loop failed:`, error);
21480
- await wait(1e3, signal);
21505
+ await runServerCommandIntake({
21506
+ api,
21507
+ roomId,
21508
+ agentId: this.agent.publicKey,
21509
+ context: this.commandContext,
21510
+ signal,
21511
+ pollMs: this.options.pollMs,
21512
+ presence: {
21513
+ ...this.options.config.daemonReleaseVersion ? { releaseVersion: this.options.config.daemonReleaseVersion } : {},
21514
+ ...this.options.config.daemonSourceSha ? { sourceSha: this.options.config.daemonSourceSha } : {},
21515
+ available: !this.options.config.modelUnavailable
21516
+ },
21517
+ onWake: (wake) => {
21518
+ this.wakeIntake = wake;
21519
+ },
21520
+ onPoll: () => this.options.health.poll(),
21521
+ onError: (error) => console.error("[thin-core] Room command failed", error),
21522
+ stop: (requestId) => this.stopTurn(requestId),
21523
+ run: async (command) => {
21524
+ const item = {
21525
+ ...command.source,
21526
+ id: command.turnRequestId,
21527
+ body: command.action === "resume" ? `Resume the paused turn. The server supplied this grant decision: ${command.source.body}` : command.source.body
21528
+ };
21529
+ this.startPrompt(item);
21530
+ await this.activeTurn?.promise;
21481
21531
  }
21482
- }
21532
+ });
21483
21533
  } finally {
21484
- stopLive?.();
21485
- this.wakeIntake = void 0;
21486
- this.options.grantRunner?.unregister(cornerId);
21487
- await this.options.scheduler.suspend(cornerId);
21534
+ this.options.grantRunner?.unregister(roomId);
21535
+ await this.options.scheduler.suspend(roomId);
21488
21536
  }
21489
21537
  }
21490
21538
  };
21491
- async function wait(ms, signal) {
21492
- if (signal?.aborted)
21493
- return;
21494
- await new Promise((resolveWait) => {
21495
- const done = () => {
21496
- clearTimeout(timer);
21497
- signal?.removeEventListener("abort", done);
21498
- resolveWait();
21499
- };
21500
- const timer = setTimeout(done, ms);
21501
- signal?.addEventListener("abort", done, { once: true });
21502
- });
21539
+ function roomMessagePrompt(author, body, attachments, delivered, harnessAcceptsImages = true) {
21540
+ const message = body.trim() || "(shared attachments)";
21541
+ const rendered = author ? `${author}: ${message}` : message;
21542
+ return [rendered, ...attachmentPromptLines(attachments, delivered, harnessAcceptsImages)].join("\n");
21503
21543
  }
21504
21544
 
21505
- // apps/body/dist/monolith-room-turn.js
21506
- import { mkdir as mkdir10 } from "node:fs/promises";
21507
- import { homedir as homedir7 } from "node:os";
21508
- import { join as join6 } from "node:path";
21509
-
21510
- // packages/api-contract/dist/scheduled-prompts.js
21511
- var SCHEDULE_SCHEDULER_NAME = "Beeline Scheduler";
21512
- var SCHEDULE_RAN_VERB = "ran a schedule for";
21513
-
21514
- // apps/body/dist/monolith-room-turn.js
21515
- function isRoomMcpPermissionRequest(request, mountedServers = ROOM_MOUNTED_MCP_SERVERS) {
21516
- if (isSquireMcpPermissionRequest(request))
21517
- return false;
21518
- return isMountedMcpToolPermissionRequest(request, mountedServers);
21519
- }
21520
- function roomPrincipalMayAddressAgent(authority, humanPermitted) {
21521
- if (!authority.member)
21522
- return false;
21523
- if (authority.principalKind === "agent")
21524
- return true;
21525
- if (authority.principalKind !== "human")
21526
- return false;
21527
- return authority.mayAddressAgent ?? humanPermitted;
21545
+ // apps/body/dist/monolith-corner-turn.js
21546
+ var execFileAsync3 = promisify3(execFile4);
21547
+ var TOOL_ARGUMENT_MAX_BYTES = 1200;
21548
+ var TOOL_OUTPUT_MAX_BYTES = 3200;
21549
+ var TOOL_PATH_LIMIT = 12;
21550
+ function oneLine(value) {
21551
+ return value.replace(/\s+/g, " ").trim();
21528
21552
  }
21529
- function isScheduledPrompt(item, agentId) {
21530
- if (item.type !== "system" || !item.mentionIds.includes(agentId))
21531
- return false;
21532
- if (item.systemEvent?.kind)
21533
- return item.systemEvent.kind === "schedule-ran";
21534
- return item.systemEvent?.verb === SCHEDULE_RAN_VERB;
21553
+ function serialized(value) {
21554
+ if (typeof value === "string")
21555
+ return value;
21556
+ try {
21557
+ return JSON.stringify(value) ?? "";
21558
+ } catch {
21559
+ return "";
21560
+ }
21535
21561
  }
21536
- function inboxItemAuthorName(item, agentId, names) {
21537
- if (isScheduledPrompt(item, agentId))
21538
- return SCHEDULE_SCHEDULER_NAME;
21539
- const subject = item.systemEvent?.subject;
21540
- if (item.type === "system" && subject?.name)
21541
- return subject.name;
21542
- return names.get(item.authorId) ?? item.authorId.slice(0, 12);
21562
+ function record(value) {
21563
+ return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
21543
21564
  }
21544
- function inboxItemPromptBody(item, agentId) {
21545
- return isScheduledPrompt(item, agentId) ? item.systemEvent?.consequence ?? item.body : item.body;
21565
+ function clampBytes(value, maxBytes) {
21566
+ const clean4 = value.trim();
21567
+ if (Buffer.byteLength(clean4) <= maxBytes)
21568
+ return clean4;
21569
+ const suffix = "\n\u2026[truncated]";
21570
+ const allowed = maxBytes - Buffer.byteLength(suffix);
21571
+ return `${Buffer.from(clean4).subarray(0, Math.max(0, allowed)).toString("utf8")}${suffix}`;
21546
21572
  }
21547
- function isSubscribedEvent(item, agentId) {
21548
- const kind = item.systemEvent?.kind;
21549
- return item.type === "system" && kind !== void 0 && !isResumeKind(kind) && item.mentionIds.includes(agentId);
21573
+ function outputExcerpt(value) {
21574
+ const redacted = redactToolDetail(serialized(value));
21575
+ if (!redacted.trim())
21576
+ return void 0;
21577
+ if (/\b(?:git[- ]credential|credential[- ]helper)\b/i.test(redacted)) {
21578
+ return "Credential-helper output omitted.";
21579
+ }
21580
+ const lines = redacted.split(/\r?\n/).map((line) => line.trimEnd());
21581
+ if (lines.length <= 8)
21582
+ return clampBytes(lines.join("\n"), TOOL_OUTPUT_MAX_BYTES);
21583
+ return clampBytes([...lines.slice(0, 4), "\u2026[output omitted]\u2026", ...lines.slice(-4)].join("\n"), TOOL_OUTPUT_MAX_BYTES);
21550
21584
  }
21551
- function inboxItemSkipsSenderPolicy(item, agentId) {
21552
- if (isGrantDecisionLine(item, agentId))
21553
- return true;
21554
- if (item.type !== "system" || !item.mentionIds.includes(agentId))
21555
- return false;
21556
- const kind = item.systemEvent?.kind;
21557
- return kind === void 0 ? isScheduledPrompt(item, agentId) : isServerEventKind(kind);
21585
+ function filePaths(value, worktreePath) {
21586
+ const paths = /* @__PURE__ */ new Set();
21587
+ const visit = (candidate, key) => {
21588
+ if (paths.size >= TOOL_PATH_LIMIT || candidate === null || candidate === void 0)
21589
+ return;
21590
+ if (typeof candidate === "string") {
21591
+ if (key && /(?:^|_)(?:path|file|filename|target)$/i.test(key)) {
21592
+ const path = candidate.startsWith(`${worktreePath}/`) ? candidate.slice(worktreePath.length + 1) : candidate;
21593
+ if (path && path.length <= 512)
21594
+ paths.add(redactToolDetail(path));
21595
+ }
21596
+ return;
21597
+ }
21598
+ if (Array.isArray(candidate)) {
21599
+ candidate.forEach((entry) => visit(entry));
21600
+ return;
21601
+ }
21602
+ const object = record(candidate);
21603
+ if (object)
21604
+ Object.entries(object).forEach(([entryKey, entry]) => visit(entry, entryKey));
21605
+ };
21606
+ visit(value);
21607
+ return [...paths];
21558
21608
  }
21559
- function isGrantDecisionLine(item, agentId) {
21560
- return item.type === "system" && item.mentionIds.includes(agentId) && parseGrantDecisionLine(item.body) !== void 0;
21609
+ function resultStatus(call) {
21610
+ const content = record(call.content) ?? (typeof call.content === "string" ? (() => {
21611
+ try {
21612
+ return record(JSON.parse(call.content));
21613
+ } catch {
21614
+ return void 0;
21615
+ }
21616
+ })() : void 0);
21617
+ const exitCode = content?.exitCode ?? content?.exit_code ?? content?.code;
21618
+ if (typeof exitCode === "number" && Number.isFinite(exitCode))
21619
+ return `exit ${exitCode}`;
21620
+ if (content?.ok === true || content?.success === true)
21621
+ return "ok";
21622
+ if (content?.ok === false || content?.success === false)
21623
+ return "error";
21624
+ return /(?:failed|error|denied)/i.test(call.status ?? "") ? "error" : "ok";
21561
21625
  }
21562
- function inboxItemTriggersTurn(item, agentId) {
21563
- if (item.authorId === agentId)
21564
- return false;
21565
- if (!item.mentionIds.includes(agentId))
21566
- return false;
21567
- return item.type === "message" || isSubscribedEvent(item, agentId) || isScheduledPrompt(item, agentId) || isGrantDecisionLine(item, agentId);
21626
+ function toolArguments(call) {
21627
+ const raw = record(call.rawInput);
21628
+ const command = typeof call.rawInput === "string" ? call.rawInput : typeof raw?.command === "string" ? raw.command : typeof raw?.cmd === "string" ? raw.cmd : void 0;
21629
+ if (command)
21630
+ return { command: clampBytes(redactToolDetail(command), TOOL_ARGUMENT_MAX_BYTES) };
21631
+ const input = serialized(call.rawInput);
21632
+ return input ? { input: clampBytes(redactToolDetail(input), TOOL_ARGUMENT_MAX_BYTES) } : {};
21568
21633
  }
21569
- function pendingGrantToolCall(call) {
21570
- if (!/(?:^|[._:/-])request_grant$/i.test(call.title ?? ""))
21571
- return false;
21572
- return /pending, card posted/i.test(typeof call.content === "string" ? call.content : JSON.stringify(call.content ?? ""));
21634
+ function toolCallKey(call, index) {
21635
+ return call.id ? `id-${createHash4("sha256").update(call.id).digest("hex")}` : `tool-${index}`;
21573
21636
  }
21574
- function roomMentionDirectory(roster, selfId) {
21575
- const rows = [];
21576
- for (const member of roster.members) {
21577
- if (member.identityId === selfId)
21578
- continue;
21579
- const handle = member.handle?.trim().replace(/^@/, "");
21580
- const name = member.name?.trim() ?? "";
21581
- const alias = handle || name;
21582
- if (!alias)
21583
- continue;
21584
- const kind = member.kind === "agent" ? "agent" : "person";
21585
- rows.push(`- @${alias}${name && name !== alias ? ` \u2014 ${name}` : ""} (${kind})`);
21586
- }
21587
- if (!rows.length)
21588
- return "";
21589
- return [
21590
- "Room members, and the exact spelling that tags each one:",
21591
- ...rows,
21592
- "Write a tag exactly as spelled here. An @name spelled any other way is plain text: it reaches nobody, and nobody is told it was meant for them. Never invent a handle, shorten one, or copy an @name out of the conversation \u2014 old messages carry spellings that no longer exist."
21593
- ].join("\n");
21637
+ function toolCallSettled(call) {
21638
+ if (call.resultReceived)
21639
+ return true;
21640
+ return /^(?:completed|complete|failed|error|succeeded|success|passed|done)$/i.test(call.status ?? "");
21594
21641
  }
21595
- function escapeRegExp(value) {
21596
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
21642
+ function isSuccessfulCommit(call) {
21643
+ if (/failed|error|denied/i.test(call.status ?? ""))
21644
+ return false;
21645
+ return /\bgit\s+commit\b|\bcommit(?:ted)?\s+(?:changes|files?)\b/i.test(`${call.title ?? ""} ${serialized(call.rawInput)}`);
21597
21646
  }
21598
- function agentReplyMentionIds(text2, roster, authorId) {
21599
- const aliases = /* @__PURE__ */ new Map();
21600
- for (const member of roster.members) {
21601
- if (member.identityId === authorId)
21602
- continue;
21603
- for (const raw of [member.name, member.handle, member.soul?.name]) {
21604
- const display = raw?.trim().replace(/^@/, "");
21605
- if (!display)
21606
- continue;
21607
- const key = display.toLocaleLowerCase();
21608
- const entry = aliases.get(key) ?? { display, ids: /* @__PURE__ */ new Set() };
21609
- entry.ids.add(member.identityId);
21610
- aliases.set(key, entry);
21647
+ async function cornerToolActivity(call, worktreePath, requestedBy) {
21648
+ const operation = oneLine(call.kind ?? "") || "tool";
21649
+ let title = oneLine(redactToolDetail(call.title ?? "")) || `${operation} tool`;
21650
+ if (isSuccessfulCommit(call)) {
21651
+ try {
21652
+ const shown = await execFileAsync3("git", ["-C", worktreePath, "show", "--format=%s", "--name-only", "--no-renames", "HEAD"], { maxBuffer: 1024 * 1024 });
21653
+ const lines = shown.stdout.split(/\r?\n/);
21654
+ const subject = oneLine(lines.shift() ?? "commit");
21655
+ const files = new Set(lines.map(oneLine).filter(Boolean));
21656
+ title = `committed ${files.size} files: ${subject}`;
21657
+ } catch {
21611
21658
  }
21612
21659
  }
21613
- for (const member of roster.members) {
21614
- if (member.identityId === authorId || !member.handle)
21615
- continue;
21616
- const handle = member.handle.trim().replace(/^@/, "").toLocaleLowerCase();
21617
- const canonical = aliases.get(handle);
21618
- const legacy = `a_${handle}`;
21619
- if (canonical && !aliases.has(legacy))
21620
- aliases.set(legacy, { ...canonical, display: legacy });
21621
- }
21622
- const mentioned = [];
21623
- for (const { display, ids } of [...aliases.values()].sort((left, right) => right.display.length - left.display.length)) {
21624
- if (ids.size !== 1)
21625
- continue;
21626
- const pattern = new RegExp(`(^|[\\s([{])@${escapeRegExp(display)}(?=$|[\\s.,!?;:)\\]}])`, "iu");
21627
- if (!pattern.test(text2))
21628
- continue;
21629
- const identityId = [...ids][0];
21630
- if (!mentioned.includes(identityId))
21631
- mentioned.push(identityId);
21632
- }
21633
- return mentioned;
21660
+ const paths = filePaths([call.rawInput, call.content, call.locations], worktreePath);
21661
+ const argumentsSummary = toolArguments(call);
21662
+ const output = outputExcerpt(call.content);
21663
+ return {
21664
+ kind: "tool",
21665
+ title: title.slice(0, 240),
21666
+ operation: operation.slice(0, 80),
21667
+ status: resultStatus(call),
21668
+ ...argumentsSummary,
21669
+ ...output ? { output } : {},
21670
+ ...requestedBy ? { requestedBy } : {},
21671
+ ...paths.length ? { files: paths.map((path) => ({ path })) } : {}
21672
+ };
21634
21673
  }
21635
- var MonolithRoomTurnLoop = class {
21674
+ var MonolithCornerTurnLoop = class {
21636
21675
  options;
21676
+ commandContext;
21637
21677
  agent;
21638
- reconciliationRequested = true;
21639
21678
  wakeIntake;
21640
21679
  /** Called by the daemon's one slow workspace reconciliation sweep. */
21641
21680
  requestReconciliation() {
21642
- this.reconciliationRequested = true;
21643
21681
  this.wakeIntake?.();
21644
21682
  this.wakeIntake = void 0;
21645
21683
  }
@@ -21647,108 +21685,88 @@ var MonolithRoomTurnLoop = class {
21647
21685
  sessionId;
21648
21686
  /** The configuration the live session baked in; a change invalidates it. */
21649
21687
  sessionFingerprint;
21688
+ /** What this exact ACP session has already been prompted with (`warm-transcript.ts`). */
21689
+ warmTranscript = new WarmTranscript();
21650
21690
  /** The live session's environment, read back for pi's own turn record. */
21651
21691
  agentEnv = {};
21652
21692
  /** OpenRouter providers this activation pinned, in order (C92). */
21653
21693
  pinnedProviders = [];
21694
+ /** Whether the pinned model takes images; `undefined` when the pin did not say. */
21695
+ modelTakesImages;
21654
21696
  /** The one provider re-pinned after an empty completion, until the session ends. */
21655
21697
  pinnedProviderOverride;
21698
+ turnIdentityInstructions = "";
21656
21699
  busy = false;
21657
- turnInstructionPrefix = "";
21658
- activeTurn;
21659
- queuedTurns = [];
21700
+ forcedStop = false;
21701
+ activityTail = Promise.resolve();
21660
21702
  /** Session scratch directory attachments are downloaded into (`TMPDIR/beeline-attachments`). */
21661
21703
  attachmentDir;
21662
- /** Whether the pinned model takes images; `undefined` when the pin did not say. */
21663
- modelTakesImages;
21664
- /** The session's TMPDIR: writable to a granted command in a Room, as it is to the harness (C94). */
21704
+ /** The session's TMPDIR, where a granted command's script argument may also live. */
21665
21705
  sessionScratchDir;
21666
- /** The `agent-home.ts` overlay this session writes into; a Room grant keeps it. */
21667
- sessionStateDirs = [];
21668
- /** What this exact ACP session has already been prompted with (`warm-transcript.ts`). */
21669
- warmTranscript = new WarmTranscript();
21670
- /** Local copies already delivered this session, by message id, so transcript renders reuse them. */
21671
- deliveredAttachments = /* @__PURE__ */ new Map();
21672
- /** Names from the latest roster read, for ledger bylines the runner writes. */
21673
- memberNames = /* @__PURE__ */ new Map();
21674
- /** The request id of the turn that paused on a grant card, until its decision arrives. */
21675
- pausedOnGrantRequestId;
21706
+ /** The turn in flight and who asked for it, for ledger rows and the grant runner. */
21707
+ currentTurn;
21676
21708
  /** Operator-local turn traces; built once when the daemon configured a directory. */
21677
21709
  turnTraceSink;
21710
+ memberNames = /* @__PURE__ */ new Map();
21711
+ /** Agent identities in this Workspace, so a mention can be told from a human's. */
21712
+ /** Per-sender continuity, shared in shape with top-level Room intake. */
21713
+ /** The member agent that owns corner-wide lifecycle facts such as checks. */
21714
+ /** The last server check state that started a turn; the same state never starts another. */
21715
+ /**
21716
+ * Request ids the requester has stopped. A corner's intake is blocked while
21717
+ * its turn runs, so a stop is recorded from the live-push callback and read
21718
+ * back here; ids that never matched a running turn are harmless and bounded
21719
+ * by the same replay window the inbox already de-duplicates over.
21720
+ */
21721
+ stoppedTurns = /* @__PURE__ */ new Set();
21678
21722
  constructor(options) {
21679
21723
  this.options = options;
21680
21724
  this.agent = runtimeIdentity(options.runtime.agent);
21681
- options.grantRunner?.register(options.roomId, {
21725
+ this.commandContext = new CommandExecutionContext(options.config.agentHomeRoot);
21726
+ this.options = { ...options, api: this.commandContext.bind(options.api) };
21727
+ options.grantRunner?.register(options.cornerId, {
21682
21728
  workspaceId: options.workspaceId,
21683
- cwd: options.cwd,
21684
- // A top-level Room keeps its read-only promise for grants too: the runner
21685
- // wraps the command in this Room's own mount table (C94).
21686
- writePolicy: () => this.grantWritePolicy(),
21687
- turn: () => this.currentTurnForRunner()
21729
+ cwd: options.worktreePath,
21730
+ // A corner is the surface with `run-host-command`: its worktree becomes a
21731
+ // branch and a pull request, and host work belongs here, next to the
21732
+ // transcript that explains it. A granted command runs unwrapped (C94).
21733
+ writePolicy: () => ({
21734
+ surface: "corner",
21735
+ ...this.sessionScratchDir ? { scratch: this.sessionScratchDir } : {}
21736
+ }),
21737
+ turn: () => this.currentTurn ? { ...this.currentTurn, generationId: this.commandContext.generationId } : void 0
21688
21738
  });
21689
21739
  }
21690
21740
  isBusy() {
21691
21741
  return this.busy;
21692
21742
  }
21693
- /** The turn a `request_grant` paused, if any (cleared when its decision resumes it). */
21694
- pausedGrantRequestId() {
21695
- return this.pausedOnGrantRequestId;
21696
- }
21697
- /**
21698
- * What the grant runner may write here: the session's own scratch and home
21699
- * overlay and nothing else, enforced by the same read-only mount table the
21700
- * harness runs under. With no usable bwrap there is no way to keep that
21701
- * promise, so the policy carries no path and the runner refuses the run
21702
- * rather than widening the boundary.
21703
- */
21704
- grantWritePolicy() {
21705
- return {
21706
- surface: "room",
21707
- ...this.options.config.bwrapPath ? { bwrapPath: this.options.config.bwrapPath } : {},
21708
- ...this.sessionScratchDir ? { scratch: this.sessionScratchDir } : {},
21709
- ...this.sessionStateDirs.length ? { harnessStateDirs: this.sessionStateDirs } : {},
21710
- maskPaths: credentialMaskPaths(this.options.config.sandboxMaskPaths, this.options.config.operatorHome ?? homedir7())
21711
- };
21712
- }
21713
- /**
21714
- * One turn's stopwatch. It is created for every turn — measuring is cheap —
21715
- * and only WRITES when the daemon configured a runtime directory to write
21716
- * into, so a standalone or test Body stays silent.
21717
- */
21718
- beginTurnTrace(requestId) {
21719
- const directory = this.options.config.turnTraceDir;
21720
- if (directory)
21721
- this.turnTraceSink ??= new TurnTraceFile(directory);
21722
- return new TurnTrace({
21723
- surface: "room",
21724
- agentId: this.agent.publicKey,
21725
- roomId: this.options.roomId,
21726
- requestId,
21727
- ...this.turnTraceSink ? { sink: this.turnTraceSink } : {}
21728
- });
21729
- }
21730
- currentTurnForRunner() {
21731
- const active = this.activeTurn;
21732
- if (!active)
21733
- return void 0;
21734
- return { requestId: active.item.id, requester: this.requesterOf(active.item.authorId) };
21735
- }
21736
- requesterOf(authorId) {
21737
- const name = this.memberNames.get(authorId);
21738
- return { pubkey: authorId, ...name ? { name } : {} };
21739
- }
21740
- currentPrincipalCanDrive(_workspaceId, principalId) {
21741
- return Promise.resolve(isSenderPermitted(this.options.config.accessPolicy ?? LEGACY_ACCESS_POLICY, principalId, this.options.config.accessOwnerPubkey, this.options.config.accessAllowlist));
21742
- }
21743
- async refreshPersonaForSoulUpdate() {
21744
- await this.options.scheduler.suspend(this.options.roomId);
21743
+ refreshPersonaForSoulUpdate() {
21744
+ return this.options.scheduler.suspend(this.options.cornerId);
21745
21745
  }
21746
21746
  async prepareForForcedUpdateRestart() {
21747
+ this.forcedStop = true;
21747
21748
  }
21748
21749
  async forceRecoverRoom() {
21749
21750
  if (this.client && this.sessionId)
21750
21751
  this.client.sessionCancel(this.sessionId);
21751
- await this.options.scheduler.forceSuspend(this.options.roomId);
21752
+ await this.options.scheduler.forceSuspend(this.options.cornerId);
21753
+ }
21754
+ /**
21755
+ * Obey a stop the requester already made a fact.
21756
+ *
21757
+ * The id is remembered whether or not it names the turn in flight: a stop for
21758
+ * work not yet started must still keep that work from starting, and one for a
21759
+ * turn that has already ended is simply never read again. Only the session
21760
+ * actually running the stopped turn is cancelled.
21761
+ */
21762
+ stopTurn(requestId) {
21763
+ this.stoppedTurns.add(requestId);
21764
+ while (this.stoppedTurns.size > 500)
21765
+ this.stoppedTurns.delete(this.stoppedTurns.values().next().value);
21766
+ if (this.currentTurn?.requestId !== requestId)
21767
+ return;
21768
+ if (this.client && this.sessionId)
21769
+ this.client.sessionCancel(this.sessionId);
21752
21770
  }
21753
21771
  async roster() {
21754
21772
  const roster = await this.options.api.execute("getWorkspaceRoster", {
@@ -21759,33 +21777,7 @@ var MonolithRoomTurnLoop = class {
21759
21777
  return roster;
21760
21778
  }
21761
21779
  /**
21762
- * Whether a picture actually reaches the model this session. Both halves
21763
- * have to hold (C87): the harness must advertise `promptCapabilities.image`,
21764
- * AND — when the pin knows the model's modalities — the model must take
21765
- * images. `undefined` modalities mean the question was never settled, and
21766
- * the harness answer stands alone as before.
21767
- */
21768
- acceptsImages() {
21769
- if (!(this.client?.canPromptWithImages() ?? false))
21770
- return false;
21771
- return this.modelTakesImages ?? true;
21772
- }
21773
- /** Download a message's attachments into the session scratch directory once. */
21774
- async deliver(item) {
21775
- if (!item.attachments.length || !this.attachmentDir)
21776
- return [];
21777
- const cached = this.deliveredAttachments.get(item.id);
21778
- if (cached)
21779
- return cached;
21780
- const delivered = await deliverAttachments(item.attachments, join6(this.attachmentDir, item.id.replace(/[^\w-]/g, "_")), this.options.fetchImpl);
21781
- this.deliveredAttachments.set(item.id, withoutImageData(delivered));
21782
- return delivered;
21783
- }
21784
- repositoryState() {
21785
- return this.options.api.execute("getRoomRepositoryState", { roomId: this.options.roomId });
21786
- }
21787
- /**
21788
- * Drop this Room's live harness process. The next activation starts cold.
21780
+ * Drop this corner's live harness process. The next activation starts cold.
21789
21781
  * A rotation is a fact about one live session, so the pin goes with it.
21790
21782
  */
21791
21783
  async discardSession() {
@@ -21797,14 +21789,8 @@ var MonolithRoomTurnLoop = class {
21797
21789
  if (client?.isAlive)
21798
21790
  await client.stop();
21799
21791
  }
21800
- /**
21801
- * Whether the retained session still matches the agent's server-side
21802
- * configuration. Retention (C104) is a saving only while what it keeps is
21803
- * still current, and a session's persona and model pin are fixed when it
21804
- * opens: they cannot be corrected in place, so a changed one has to cost a
21805
- * respawn. The check is one round trip — the roster half of which the turn
21806
- * was going to fetch anyway — against a cold spawn measured in seconds.
21807
- */
21792
+ /** See `MonolithRoomTurnLoop.sessionIsCurrent`: retention never keeps a
21793
+ * session whose persona or model pin the operator has since changed. */
21808
21794
  async sessionIsCurrent() {
21809
21795
  return await this.currentSessionFingerprint() === this.sessionFingerprint;
21810
21796
  }
@@ -21812,7 +21798,7 @@ var MonolithRoomTurnLoop = class {
21812
21798
  const [configuration, roster] = await Promise.all([
21813
21799
  this.options.api.execute("getAgentConfiguration", {
21814
21800
  agentId: this.agent.publicKey,
21815
- roomId: this.options.roomId
21801
+ roomId: this.options.cornerId
21816
21802
  }),
21817
21803
  this.roster()
21818
21804
  ]);
@@ -21828,13 +21814,12 @@ var MonolithRoomTurnLoop = class {
21828
21814
  if (this.client?.isAlive && this.sessionId)
21829
21815
  return this.sessionId;
21830
21816
  trace?.noteActivation("cold");
21831
- const [configuration, roster, repositoryState] = await Promise.all([
21817
+ const [configuration, roster] = await Promise.all([
21832
21818
  this.options.api.execute("getAgentConfiguration", {
21833
21819
  agentId: this.agent.publicKey,
21834
- roomId: this.options.roomId
21820
+ roomId: this.options.cornerId
21835
21821
  }),
21836
- this.roster(),
21837
- this.repositoryState()
21822
+ this.roster()
21838
21823
  ]);
21839
21824
  const self = roster.members.find((member) => member.identityId === this.agent.publicKey);
21840
21825
  const fingerprint = sessionConfigFingerprint({
@@ -21843,8 +21828,7 @@ var MonolithRoomTurnLoop = class {
21843
21828
  soul: configuration.soul ?? self?.soul,
21844
21829
  agentName: self?.name ?? this.agent.name
21845
21830
  });
21846
- const directMessage = Array.isArray(repositoryState.directParticipants) && repositoryState.directParticipants.length === 2;
21847
- await mkdir10(this.options.cwd, { recursive: true });
21831
+ await mkdir11(this.options.worktreePath, { recursive: true });
21848
21832
  const selection = configuration.model || configuration.effort ? { model: configuration.model, effort: configuration.effort } : this.options.config.modelSelection;
21849
21833
  const homeOverlay = this.options.config.agentHomeRoot ? await prepareRoomAgentHome({
21850
21834
  root: this.options.config.agentHomeRoot,
@@ -21861,7 +21845,26 @@ var MonolithRoomTurnLoop = class {
21861
21845
  })
21862
21846
  }) : {};
21863
21847
  const command = this.options.config.agentCommand ?? this.options.config.agentBinary;
21864
- const agentEnv = { ...this.options.config.agentEnv, ...homeOverlay };
21848
+ const repository = this.options.repository;
21849
+ let githubEnv = repository ? { GH_TOKEN: repository.githubToken, GITHUB_TOKEN: repository.githubToken } : {};
21850
+ if (repository && this.options.config.runtimeConfigPath && this.options.config.agentHomeRoot) {
21851
+ const gitBinary = (await execFileAsync3("which", ["git"])).stdout.trim();
21852
+ const ghBinary = await execFileAsync3("which", ["gh"]).then((result) => result.stdout.trim()).catch(() => void 0);
21853
+ githubEnv = await installCornerGitHubWrappers({
21854
+ root: this.options.config.agentHomeRoot,
21855
+ runtimeConfigPath: this.options.config.runtimeConfigPath,
21856
+ roomId: this.options.parentRoomId,
21857
+ cliEntrypoint: process.argv[1],
21858
+ gitBinary,
21859
+ ...ghBinary ? { ghBinary } : {},
21860
+ inheritedPath: this.options.config.agentEnv.PATH ?? process.env.PATH
21861
+ });
21862
+ }
21863
+ const agentEnv = {
21864
+ ...this.options.config.agentEnv,
21865
+ ...homeOverlay,
21866
+ ...githubEnv
21867
+ };
21865
21868
  this.agentEnv = agentEnv;
21866
21869
  const agentArgs = agentArgsWithModelSelection({
21867
21870
  kind: this.options.config.agentKind,
@@ -21870,19 +21873,21 @@ var MonolithRoomTurnLoop = class {
21870
21873
  }, selection);
21871
21874
  const operatorHome = this.options.config.operatorHome ?? homedir7();
21872
21875
  const { stateDirs, tmpDir } = harnessStateDirsFromEnv(agentEnv);
21873
- this.attachmentDir = tmpDir ? join6(tmpDir, "beeline-attachments") : void 0;
21876
+ this.attachmentDir = tmpDir ? join7(tmpDir, "beeline-attachments") : void 0;
21874
21877
  this.sessionScratchDir = tmpDir;
21875
- this.sessionStateDirs = stateDirs;
21876
21878
  const homeStateDirs = harnessHomeStateDirs(command, agentEnv.HOME ?? operatorHome);
21877
- await Promise.all(homeStateDirs.map((dir) => mkdir10(dir, { recursive: true })));
21879
+ await Promise.all(homeStateDirs.map((dir) => mkdir11(dir, { recursive: true })));
21878
21880
  const attachScratchRoot = this.options.config.agentHomeRoot ?? tmpDir;
21879
21881
  if (attachScratchRoot)
21880
- await mkdir10(attachScratchRoot, { recursive: true });
21882
+ await mkdir11(attachScratchRoot, { recursive: true });
21881
21883
  const spawnCommand = wrapAgentCommand({
21882
21884
  bwrapPath: this.options.config.bwrapPath,
21883
21885
  spec: {
21884
- mode: "readonly",
21885
- cwd: this.options.cwd,
21886
+ mode: "edit",
21887
+ cwd: this.options.worktreePath,
21888
+ worktreePath: this.options.worktreePath,
21889
+ ...repository ? { gitCommonDir: repository.gitCommonDir } : {},
21890
+ protectedPaths: [this.options.runtime.supervisorRoot],
21886
21891
  harnessStateDirs: stateDirs,
21887
21892
  harnessHomeStateDirs: homeStateDirs,
21888
21893
  ...tmpDir ? { tmpDir } : {},
@@ -21892,18 +21897,42 @@ var MonolithRoomTurnLoop = class {
21892
21897
  command,
21893
21898
  args: agentArgs
21894
21899
  });
21900
+ const clientOptions = {
21901
+ agentCommand: spawnCommand.command,
21902
+ agentArgs: spawnCommand.args,
21903
+ agentEnv,
21904
+ agentCwd: this.options.worktreePath,
21905
+ agentLabel: command,
21906
+ autoApprovePermissions: true,
21907
+ permissionHandler: () => Promise.resolve("allow")
21908
+ };
21909
+ this.client = (this.options.createAcpClient ?? ((value) => new AcpClient(value)))(clientOptions);
21910
+ await this.client.start();
21895
21911
  const servers = [
21896
- readOnlyMcpServer(this.options.config, this.options.cwd),
21912
+ ...repository ? [
21913
+ {
21914
+ name: "buzz-dev-mcp",
21915
+ command: this.options.config.mcpBinary,
21916
+ args: [],
21917
+ // ACP hosts launch stdio MCP servers with an explicit, sanitized env.
21918
+ // This token is minted for this exact corner and is also the credential
21919
+ // helper's password source, so its shell commands need the same scope as
21920
+ // the corner harness without inheriting any host credentials.
21921
+ env: [
21922
+ { name: "GH_TOKEN", value: repository.githubToken },
21923
+ { name: "GITHUB_TOKEN", value: repository.githubToken }
21924
+ ]
21925
+ }
21926
+ ] : [],
21897
21927
  beelineAgentMcpServer(this.options.config, this.options.api, {
21898
- roomId: this.options.roomId,
21928
+ roomId: this.options.parentRoomId,
21899
21929
  workspaceId: this.options.workspaceId,
21900
- attachRoot: this.options.cwd,
21901
- // The whole per-session overlay, not an enumerated subset: the agent
21902
- // never picks where a harness writes a file it generates (grok's own
21903
- // images dir, say), so anything inside the overlay it could possibly
21904
- // have written must be attachable, whatever subdirectory that is.
21930
+ cornerId: this.options.cornerId,
21931
+ attachRoot: this.options.worktreePath,
21932
+ // The whole per-session overlay, not an enumerated subset: see
21933
+ // `monolith-room-turn.ts`'s matching comment.
21905
21934
  attachScratchRoot,
21906
- directMessage,
21935
+ turnContextPath: this.commandContext.path,
21907
21936
  ...this.options.grantRunnerEndpoint ? { grantRunner: this.options.grantRunnerEndpoint } : {}
21908
21937
  })
21909
21938
  ];
@@ -21912,43 +21941,38 @@ var MonolithRoomTurnLoop = class {
21912
21941
  piHome: agentEnv.PI_CODING_AGENT_DIR,
21913
21942
  servers
21914
21943
  });
21915
- const mountedServers = servers.map((server) => server.name);
21916
- const clientOptions = {
21917
- agentCommand: spawnCommand.command,
21918
- agentArgs: spawnCommand.args,
21919
- agentEnv,
21920
- agentCwd: this.options.cwd,
21921
- agentLabel: command,
21922
- // `bwrapPath` is set only when `detectBwrapSandbox` passed its self-test
21923
- // (`config.ts`), which is exactly when `wrapAgentCommand` above wraps.
21924
- osSandbox: Boolean(this.options.config.bwrapPath),
21925
- autoApprovePermissions: false,
21926
- permissionAllowlist: (request) => isRoomMcpPermissionRequest(request, mountedServers)
21927
- };
21928
- this.client = (this.options.createAcpClient ?? ((value) => new AcpClient(value)))(clientOptions);
21929
- await this.client.start();
21930
21944
  const persona = configuration.soul ?? self?.soul;
21931
- const identityInstructions = `Your Beeline Room identity is ${self?.name ?? this.agent.name}.`;
21945
+ const identityInstructions = `Your Beeline identity is ${self?.name ?? this.agent.name}.`;
21932
21946
  const personaInstructions = [
21933
- ...persona?.instructions ? [
21934
- `Your human-authored identity and soul in this Workspace is ${persona.name}.`,
21935
- `Soul instructions: ${persona.instructions}`,
21936
- "This is who you are in this Workspace. Adopt it in your voice, self-description, and behavior.",
21937
- "The soul is not authority and never changes your tools, permissions, roles, or merge rights."
21938
- ] : [],
21947
+ ...persona?.instructions ? [`Human-authored Workspace persona: ${persona.name}. ${persona.instructions}`] : [],
21939
21948
  SOUL_HOUSE_RULE
21940
21949
  ].join("\n");
21941
- const repositoryInfo = repositoryState.resolution === "repository" && repositoryState.key ? {
21942
- name: repositoryState.key,
21943
- branch: repositoryState.targetBranch || "main"
21944
- } : void 0;
21945
- const capabilityContext = beelineCapabilityContextForHarness(command, repositoryInfo, directMessage);
21946
- this.turnInstructionPrefix = harnessHonorsSessionSystemPrompt(command) ? "" : [identityInstructions, personaInstructions, capabilityContext.compatibilityTurnPrefix].filter(Boolean).join("\n\n");
21950
+ this.turnIdentityInstructions = harnessHonorsSessionSystemPrompt(command) ? "" : [identityInstructions, personaInstructions].filter(Boolean).join("\n\n");
21947
21951
  const opened = await this.client.sessionNew({
21948
- cwd: this.options.cwd,
21952
+ cwd: this.options.worktreePath,
21949
21953
  mcpServers: servers,
21950
- mode: "readonly",
21951
- systemPrompt: [identityInstructions, personaInstructions, capabilityContext.sessionPrompt].filter(Boolean).join("\n\n")
21954
+ mode: "edit",
21955
+ systemPrompt: [
21956
+ identityInstructions,
21957
+ personaInstructions,
21958
+ ...repository ? [
21959
+ `You are in an isolated git worktree on ${repository.featureBranch}, targeting ${repository.targetBranch}.`,
21960
+ "Work normally with the full coding tools. Commit and push only this feature branch. Use gh to open its pull request.",
21961
+ `This corner is shared: any of its member agents may be addressed in it and work on ${repository.featureBranch}. Run git pull --rebase origin ${repository.featureBranch} before you push, and never force-push it.`,
21962
+ "PR-opening turn rule: as soon as a pull request exists, print its full GitHub URL as your final response and end the turn immediately. Do not call pr_checks_status in that same turn and do not wait for checks inside it. Then stay idle until a later corner fact or human message starts another turn.",
21963
+ 'Never merge because local tests pass or because gh reports passing checks. On a later turn triggered by a server-posted checks-passed note, call beeline-agent pr_checks_status. Merge only when it returns checks="passed", held=false, and approvalPending=false.',
21964
+ "Merge the PR yourself only after the checks-passed event shows every check green; if any check failed or is still running, say exactly which and stop - never merge red.",
21965
+ "If any human in this corner says hold or do not merge, do not merge until a later human explicitly resumes it.",
21966
+ "Do not tag the user when a corner turn finishes: the server posts the merge summary card and its push already cover completion. Tag a human only mid-turn, and only when you need a decision or input.",
21967
+ 'GitHub check and merge notes are server lines already in the corner: never restate them (no "checks passed", "CI is green", "PR ready for review"). On a checks turn, say nothing unless you act - a merge or a pushed fix - and then one short line about that.',
21968
+ "A human approval in the app asks the server to merge. When approval is pending, wait for the server close request instead of racing it with gh. If checks passed, no hold exists, and no approval is pending, merge the pull request yourself with gh.",
21969
+ "Never push directly to the target branch. Never merge a different pull request."
21970
+ ] : [
21971
+ "This is a chat-only corner with no repository or GitHub workflow.",
21972
+ "Work in this corner's writable workspace. Use write_scratch_file or ordinary tools to create files, then attach_file to send them back to the corner.",
21973
+ "Do not initialize a repository, create a branch, push, open a pull request, or wait for GitHub checks."
21974
+ ]
21975
+ ].filter(Boolean).join("\n\n")
21952
21976
  });
21953
21977
  this.sessionId = opened.sessionId;
21954
21978
  this.sessionFingerprint = fingerprint;
@@ -21958,12 +21982,7 @@ var MonolithRoomTurnLoop = class {
21958
21982
  }
21959
21983
  return opened.sessionId;
21960
21984
  }
21961
- /**
21962
- * The scheduler seam, and the only place that can see the boundary between
21963
- * waiting for a slot and spawning a harness: `queue-wait` closes the instant
21964
- * `activate()` is called, and `cold` vs `warm` is decided by whether this
21965
- * Room already holds a live ACP client.
21966
- */
21985
+ /** The scheduler seam: `queue-wait` closes when a slot buys a session. */
21967
21986
  lifecycle(trace) {
21968
21987
  return {
21969
21988
  activate: async () => {
@@ -21982,6 +22001,12 @@ var MonolithRoomTurnLoop = class {
21982
22001
  suspend: () => this.discardSession()
21983
22002
  };
21984
22003
  }
22004
+ /** A picture reaches the model only if the harness AND the model take one (C87). */
22005
+ acceptsImages() {
22006
+ if (!(this.client?.canPromptWithImages() ?? false))
22007
+ return false;
22008
+ return this.modelTakesImages ?? true;
22009
+ }
21985
22010
  /** The pinned providers a failure reason should name for this session. */
21986
22011
  servingProviders() {
21987
22012
  return this.pinnedProviderOverride ? [this.pinnedProviderOverride] : this.pinnedProviders;
@@ -22000,8 +22025,7 @@ var MonolithRoomTurnLoop = class {
22000
22025
  /**
22001
22026
  * Re-pin the session to the next provider in the OpenRouter order and open a
22002
22027
  * fresh session on it, so the retry of an empty completion is served — and
22003
- * named — by exactly one provider. Undefined when the pin has nowhere left
22004
- * to go.
22028
+ * named — by exactly one provider (C92).
22005
22029
  */
22006
22030
  async repinNextProvider(trace, reason) {
22007
22031
  const next = nextPinnedProvider(this.pinnedProviders, this.pinnedProviderOverride);
@@ -22018,343 +22042,310 @@ var MonolithRoomTurnLoop = class {
22018
22042
  await (trace ? trace.measure("activation", () => this.activate(trace)) : this.activate());
22019
22043
  return next;
22020
22044
  }
22021
- startPrompt(item) {
22022
- const active = {
22023
- item,
22024
- steers: [],
22025
- steerTail: Promise.resolve(),
22026
- resumeRequested: false,
22027
- phase: "prompting",
22028
- promise: Promise.resolve()
22029
- };
22030
- this.activeTurn = active;
22031
- active.promise = this.prompt(active).catch((error) => {
22032
- this.options.health.failure(1e3);
22033
- console.error(`[thin-core] monolith Room ${this.options.roomId} turn failed:`, error);
22034
- }).finally(() => {
22035
- if (this.activeTurn === active)
22036
- this.activeTurn = void 0;
22037
- });
22038
- }
22039
- steer(active, item) {
22040
- active.steers.push(item);
22041
- active.steerTail = active.steerTail.catch(() => void 0).then(async () => {
22042
- try {
22043
- const [roster, delivered] = await Promise.all([this.roster(), this.deliver(item)]);
22044
- const author = roster.members.find((member) => member.identityId === item.authorId)?.name ?? item.authorId.slice(0, 12);
22045
- await this.client.sessionSteer(this.sessionId, [
22046
- `Human steer received while the current turn is running from ${author}:`,
22047
- roomMessagePrompt("", item.body, item.attachments, delivered, this.acceptsImages()),
22048
- "Adjust the current work now. Keep the original request and earlier messages as context."
22049
- ].join("\n\n"));
22050
- } catch (error) {
22051
- active.resumeRequested = true;
22052
- this.client?.sessionCancel(this.sessionId);
22053
- console.warn(`[thin-core] monolith Room ${this.options.roomId} live steer unavailable; cancelling and resuming:`, error);
22054
- }
22045
+ /** One turn's stopwatch; writes only when the daemon configured a trace directory. */
22046
+ beginTurnTrace(requestId) {
22047
+ const directory = this.options.config.turnTraceDir;
22048
+ if (directory)
22049
+ this.turnTraceSink ??= new TurnTraceFile(directory);
22050
+ return new TurnTrace({
22051
+ surface: "corner",
22052
+ agentId: this.agent.publicKey,
22053
+ roomId: this.options.cornerId,
22054
+ requestId,
22055
+ ...this.turnTraceSink ? { sink: this.turnTraceSink } : {}
22055
22056
  });
22056
22057
  }
22057
- async prompt(active) {
22058
- const { item } = active;
22059
- const api = this.options.api;
22060
- this.busy = true;
22061
- const trace = this.beginTurnTrace(item.id);
22062
- try {
22063
- if (!this.memberNames.has(item.authorId))
22064
- await this.roster().catch(() => void 0);
22065
- await withTurnReceiptHeartbeat(api, {
22066
- agentId: this.agent.publicKey,
22067
- roomId: this.options.roomId,
22068
- requestId: item.id,
22069
- generationId: `${this.agent.publicKey}:${this.options.roomId}`
22070
- }, async () => {
22071
- await api.execute("postAgentActivity", {
22072
- agentId: this.agent.publicKey,
22073
- roomId: this.options.roomId,
22074
- requestId: item.id,
22075
- activity: [
22076
- {
22077
- kind: "thinking",
22078
- title: "Working",
22079
- status: "in_progress",
22080
- requestedBy: this.requesterOf(item.authorId)
22081
- }
22082
- ]
22083
- });
22058
+ async prompt(requestId, trigger, attachments = [], requestedById, restates) {
22059
+ const { api, cornerId } = this.options;
22060
+ if (this.stoppedTurns.has(requestId))
22061
+ return;
22062
+ const spoken = (text2) => restates && isCornerStatusRestatement(text2, restates) ? "" : text2;
22063
+ const requester = requestedById ? {
22064
+ pubkey: requestedById,
22065
+ ...this.memberNames.get(requestedById) ? { name: this.memberNames.get(requestedById) } : {}
22066
+ } : void 0;
22067
+ this.currentTurn = { requestId, ...requester ? { requester } : {} };
22068
+ const trace = this.beginTurnTrace(requestId);
22069
+ try {
22070
+ await withTurnReceiptHeartbeat(api, {
22071
+ agentId: this.agent.publicKey,
22072
+ roomId: cornerId,
22073
+ requestId,
22074
+ generationId: this.commandContext.generationId
22075
+ }, () => {
22084
22076
  trace.noteScheduler("queue", this.options.scheduler.snapshot());
22085
22077
  trace.start("queue-wait");
22086
- await this.options.scheduler.run(this.options.roomId, this.lifecycle(trace), async () => {
22078
+ return this.options.scheduler.run(cornerId, this.lifecycle(trace), async () => {
22087
22079
  trace.end("queue-wait");
22088
22080
  trace.noteScheduler("admission", this.options.scheduler.snapshot());
22081
+ if (this.forcedStop)
22082
+ throw new Error("corner turn stopped for daemon handoff");
22083
+ this.busy = true;
22084
+ await this.syncBranch();
22089
22085
  const [conversation, roster, delivered] = await trace.measure("context-fetch", () => Promise.all([
22090
- api.execute("getRoomConversation", { roomId: this.options.roomId, limit: 200 }),
22086
+ api.execute("getRoomConversation", { roomId: cornerId, limit: 200 }),
22091
22087
  this.roster(),
22092
- this.deliver(item)
22088
+ this.attachmentDir && attachments.length ? deliverAttachments(attachments, join7(this.attachmentDir, requestId.replace(/[^\w-]/g, "_")), this.options.fetchImpl) : Promise.resolve([])
22093
22089
  ]));
22094
22090
  const names = new Map(roster.members.map((member) => [member.identityId, member.name]));
22095
- const transcriptRows = conversation.items.filter((message) => message.type === "message" && message.id !== item.id && !active.steers.some((steerItem) => steerItem.id === message.id)).slice(-80).map((message) => ({
22091
+ const requestedBy = requester && !requester.name && names.get(requester.pubkey) ? { ...requester, name: names.get(requester.pubkey) } : requester;
22092
+ if (requestedBy)
22093
+ this.currentTurn = { requestId, requester: requestedBy };
22094
+ const transcriptRows = conversation.items.slice(-120).map((message) => ({
22096
22095
  id: message.id,
22097
- line: roomMessagePrompt(names.get(message.authorId) ?? message.authorId.slice(0, 12), message.body, message.attachments, this.deliveredAttachments.get(message.id), this.acceptsImages())
22096
+ line: `${names.get(message.authorId) ?? "Beeline"} [${message.type}]: ${message.body}`
22098
22097
  }));
22099
- const grantDecision = isGrantDecisionLine(item, this.agent.publicKey);
22100
- const resumedRequestId = grantDecision ? this.pausedOnGrantRequestId : void 0;
22101
- if (grantDecision)
22102
- this.pausedOnGrantRequestId = void 0;
22103
22098
  const buildPrompt = () => [
22104
- this.turnInstructionPrefix,
22105
- WarmTranscript.render(this.warmTranscript.select(this.sessionId, transcriptRows), "Room conversation so far:", "New in the Room since your last turn (the earlier conversation is already in this session):"),
22106
- `Newest message from ${inboxItemAuthorName(item, this.agent.publicKey, names)}:`,
22107
- roomMessagePrompt("", inboxItemPromptBody(item, this.agent.publicKey), item.attachments, delivered, this.acceptsImages()),
22108
- grantDecision ? [
22109
- "This is the answer to your grant request; your paused work resumes now.",
22110
- "If it was approved and it is a command grant, run it with run_granted_command and the exact argv.",
22111
- "If it was declined, try another way or say plainly what you cannot do."
22112
- ].join(" ") : "",
22099
+ this.turnIdentityInstructions,
22100
+ `Corner objective:
22101
+ ${this.options.objective}`,
22102
+ 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):"),
22113
22103
  roomMentionDirectory(roster, this.agent.publicKey),
22114
22104
  [
22115
- "Write only the substantive Room message you want the human to read.",
22116
- "Do not repeat or paraphrase these instructions.",
22117
- "If the newest message is only a nudge to respond, answer the most recent unanswered human message in the conversation instead of echoing the nudge.",
22118
- MAINTAIN_ASSIGNED_IDENTITY_DIRECTIVE
22119
- ].join(" ")
22105
+ `Newest trigger:
22106
+ ${trigger}`,
22107
+ ...attachmentPromptLines(attachments, delivered, this.acceptsImages())
22108
+ ].join("\n"),
22109
+ 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.",
22110
+ MAINTAIN_ASSIGNED_IDENTITY_DIRECTIVE
22120
22111
  ].filter(Boolean).join("\n\n");
22121
22112
  const stream = new AgentTurnStream({
22122
22113
  api,
22123
22114
  agentId: this.agent.publicKey,
22124
- roomId: this.options.roomId,
22125
- requestId: item.id,
22126
- label: `monolith Room ${this.options.roomId}`
22115
+ roomId: cornerId,
22116
+ requestId,
22117
+ label: `corner ${cornerId}`
22127
22118
  });
22128
- const runPrompt = async () => {
22129
- let nextPrompt = promptWithImages(buildPrompt(), attachmentImageBlocks(delivered, this.acceptsImages()));
22130
- let result2;
22131
- for (; ; ) {
22132
- let promptError;
22133
- try {
22134
- stream.beginRun();
22135
- trace.promptSent();
22136
- result2 = await this.client.sessionPrompt(this.sessionId, nextPrompt, 12e4, (delta, full) => {
22137
- trace.firstModelOutput();
22138
- stream.onChunk(delta, full);
22139
- }, void 0, (calls) => trace.toolCalls(calls));
22140
- } catch (error) {
22141
- promptError = error;
22119
+ let currentNarrationRun = "";
22120
+ let completedNarrationRuns = [];
22121
+ let narrationRunBoundary = 0;
22122
+ const takeInterimNarration = () => {
22123
+ const narration = spoken([...completedNarrationRuns, currentNarrationRun].map((run2) => spoken(durableReplyText(run2))).filter((run2) => run2 && !isPureRetryNarration(run2)).join("\n\n"));
22124
+ narrationRunBoundary += completedNarrationRuns.length;
22125
+ completedNarrationRuns = [];
22126
+ currentNarrationRun = "";
22127
+ return narration;
22128
+ };
22129
+ const publishedToolCalls = /* @__PURE__ */ new Set();
22130
+ const observedToolCalls = /* @__PURE__ */ new Set();
22131
+ const pendingToolNarrations = /* @__PURE__ */ new Map();
22132
+ const pendingToolActivities = /* @__PURE__ */ new Map();
22133
+ let lastNarratedToolCall;
22134
+ let activityAttempt = 0;
22135
+ const publishToolCalls = (calls, settledOnly) => {
22136
+ calls.forEach((call, index) => {
22137
+ const key = `${activityAttempt}:${toolCallKey(call, index)}`;
22138
+ if (settledOnly && !observedToolCalls.has(key)) {
22139
+ observedToolCalls.add(key);
22140
+ const narration2 = takeInterimNarration();
22141
+ pendingToolNarrations.set(key, narration2);
22142
+ if (narration2)
22143
+ lastNarratedToolCall = key;
22142
22144
  }
22143
- const settledSteerTail = active.steerTail;
22144
- await settledSteerTail;
22145
- if (settledSteerTail !== active.steerTail)
22146
- continue;
22147
- if (!active.resumeRequested) {
22148
- if (promptError)
22149
- throw promptError;
22150
- break;
22145
+ if (settledOnly || publishedToolCalls.has(key) || !toolCallSettled(call))
22146
+ return;
22147
+ publishedToolCalls.add(key);
22148
+ const narration = pendingToolNarrations.get(key) ?? "";
22149
+ this.activityTail = this.activityTail.catch(() => void 0).then(async () => {
22150
+ let activity = pendingToolActivities.get(key);
22151
+ if (!activity) {
22152
+ const toolActivity = await cornerToolActivity(call, this.options.worktreePath, requestedBy);
22153
+ activity = [
22154
+ ...narration ? [
22155
+ {
22156
+ kind: "output",
22157
+ title: "Update",
22158
+ text: narration,
22159
+ ...requestedBy ? { requestedBy } : {}
22160
+ }
22161
+ ] : [],
22162
+ toolActivity
22163
+ ];
22164
+ pendingToolActivities.set(key, activity);
22165
+ }
22166
+ await api.execute("postAgentActivity", {
22167
+ agentId: this.agent.publicKey,
22168
+ roomId: cornerId,
22169
+ requestId,
22170
+ cornerActivityKey: key,
22171
+ activity
22172
+ });
22173
+ pendingToolNarrations.delete(key);
22174
+ pendingToolActivities.delete(key);
22175
+ }).then(() => void 0).catch((error) => {
22176
+ publishedToolCalls.delete(key);
22177
+ console.error(`[thin-core] corner ${cornerId} tool activity failed:`, error);
22178
+ });
22179
+ });
22180
+ };
22181
+ const flushToolCalls = async (calls, finalReply) => {
22182
+ await this.activityTail;
22183
+ if (lastNarratedToolCall && pendingToolNarrations.get(lastNarratedToolCall) === finalReply)
22184
+ pendingToolNarrations.delete(lastNarratedToolCall);
22185
+ publishToolCalls(calls, false);
22186
+ await this.activityTail;
22187
+ publishToolCalls(calls, false);
22188
+ await this.activityTail;
22189
+ };
22190
+ const runPrompt = async () => {
22191
+ activityAttempt += 1;
22192
+ publishedToolCalls.clear();
22193
+ observedToolCalls.clear();
22194
+ pendingToolNarrations.clear();
22195
+ pendingToolActivities.clear();
22196
+ lastNarratedToolCall = void 0;
22197
+ stream.beginRun();
22198
+ completedNarrationRuns = [];
22199
+ narrationRunBoundary = 0;
22200
+ currentNarrationRun = "";
22201
+ trace.promptSent();
22202
+ return this.client.sessionPrompt(this.sessionId, promptWithImages(buildPrompt(), attachmentImageBlocks(delivered, this.acceptsImages())), 12e4, (delta, full, currentRun, runs) => {
22203
+ trace.firstModelOutput();
22204
+ if (runs) {
22205
+ completedNarrationRuns = runs.slice(narrationRunBoundary);
22206
+ currentNarrationRun = "";
22207
+ } else if (currentRun === void 0)
22208
+ currentNarrationRun += delta;
22209
+ else {
22210
+ if (currentNarrationRun && currentNarrationRun !== currentRun && !currentRun.startsWith(currentNarrationRun))
22211
+ completedNarrationRuns.push(currentNarrationRun);
22212
+ currentNarrationRun = currentRun;
22151
22213
  }
22152
- active.resumeRequested = false;
22153
- nextPrompt = [
22154
- "The previous run was cancelled because its harness could not accept every live steer.",
22155
- "Resume the same turn. Keep the original request and everything that happened before it was cancelled.",
22156
- "Human messages that arrived after the original request, in transcript order:",
22157
- ...active.steers.map((steerItem) => roomMessagePrompt(steerItem.authorId.slice(0, 12), steerItem.body, steerItem.attachments, this.deliveredAttachments.get(steerItem.id), this.acceptsImages())),
22158
- "Continue now and answer the updated request without erasing the earlier context."
22159
- ].join("\n\n");
22160
- }
22161
- return result2;
22214
+ stream.onChunk(delta, full);
22215
+ }, void 0, (calls) => {
22216
+ trace.toolCalls(calls);
22217
+ publishToolCalls(calls, true);
22218
+ });
22162
22219
  };
22163
22220
  let result = await runPrompt();
22164
22221
  trace.promptSettled();
22165
22222
  let explained = await this.explainEmpty(result);
22166
- if (explained && shouldRetryEmptyTurn(explained)) {
22223
+ if (explained && !restates && shouldRetryEmptyTurn(explained)) {
22224
+ await flushToolCalls(result.toolCalls, "");
22167
22225
  const silent = this.servingProviders();
22168
22226
  const next = await this.repinNextProvider(trace, explained.reason);
22169
22227
  if (next) {
22170
- console.warn(`[thin-core] monolith Room ${this.options.roomId} turn ${item.id}: ${turnFailureReasonWithProvider(explained.reason, silent)}; retrying on ${next}`);
22228
+ console.warn(`[thin-core] corner ${cornerId} turn ${requestId}: ${turnFailureReasonWithProvider(explained.reason, silent)}; retrying on ${next}`);
22171
22229
  result = await runPrompt();
22172
22230
  trace.promptSettled();
22173
22231
  explained = await this.explainEmpty(result);
22174
22232
  }
22175
22233
  }
22176
- active.phase = "finishing";
22177
- if (result.toolCalls.some((call) => pendingGrantToolCall(call))) {
22178
- this.pausedOnGrantRequestId = item.id;
22179
- console.log(`[thin-core] monolith Room ${this.options.roomId} turn ${item.id} paused on a grant card`);
22180
- } else if (resumedRequestId) {
22181
- console.log(`[thin-core] monolith Room ${this.options.roomId} turn ${resumedRequestId} resumed by grant decision ${item.id}`);
22182
- }
22183
- const openCornerCall = result.toolCalls.find((call) => /(?:^|[._:/-])open_corner$/i.test(call.title ?? ""));
22184
- if (openCornerCall) {
22185
- console.log(`[thin-core] monolith Room ${this.options.roomId} tool call: ${openCornerCall.title} (${openCornerCall.status ?? "no status"})`);
22186
- if (!isFailedToolCall(openCornerCall))
22187
- this.options.onCornerOpened?.();
22234
+ if (this.stoppedTurns.has(requestId)) {
22235
+ stream.close();
22236
+ throw new TurnStoppedError("turn stopped by the requester");
22188
22237
  }
22189
- for (const call of result?.toolCalls ?? []) {
22238
+ let reply = durableReplyText(result.agentText);
22239
+ if (!reply && explained?.recoveredText)
22240
+ reply = durableReplyText(explained.recoveredText);
22241
+ await flushToolCalls(result.toolCalls, reply);
22242
+ for (const call of result.toolCalls) {
22190
22243
  const failure = toolCallFailureLine(call);
22191
- if (failure) {
22192
- console.warn(`[thin-core] monolith Room ${this.options.roomId} ${failure}`);
22193
- }
22244
+ if (failure)
22245
+ console.warn(`[thin-core] corner ${cornerId} ${failure}`);
22194
22246
  }
22195
22247
  stream.close();
22196
- let reply = durableReplyText(result.agentText);
22197
22248
  if (!reply && explained) {
22198
- reply = explained.recoveredText ? durableReplyText(explained.recoveredText) : "";
22199
- if (!reply) {
22249
+ if (!reply && !(restates && !isAccountOrProviderRefusal(explained.record))) {
22200
22250
  throw new Error(turnFailureReasonWithProvider(explained.reason, this.servingProviders()));
22201
22251
  }
22202
- console.warn(`[thin-core] monolith Room ${this.options.roomId} turn ${item.id}: ${explained.reason}`);
22203
- }
22204
- if (openCornerCall && !isFailedToolCall(openCornerCall)) {
22205
- reply = stripCornerOpenEcho(reply);
22252
+ console.warn(`[thin-core] corner ${cornerId} turn ${requestId}: ${explained.reason}`);
22206
22253
  }
22207
- await trace.measure("publish", () => stream.settle(reply, reply ? {
22208
- triggerMessageId: item.id,
22209
- mentionIds: agentReplyMentionIds(reply, roster, this.agent.publicKey)
22254
+ const durableReply = spoken(reply);
22255
+ const mentionIds = durableReply ? agentReplyMentionIds(durableReply, roster, this.agent.publicKey) : [];
22256
+ await trace.measure("publish", () => stream.settle(durableReply, durableReply ? {
22257
+ ...requestedById ? { triggerMessageId: requestId } : {},
22258
+ mentionIds
22210
22259
  } : {}));
22211
- }, { priority: "interactive", roomKey: this.options.roomId });
22212
- }, (error) => console.error(`[thin-core] monolith Room ${this.options.roomId} receipt heartbeat failed:`, error));
22260
+ }, { priority: "interactive", roomKey: cornerId });
22261
+ }, (error) => console.error(`[thin-core] corner ${cornerId} receipt heartbeat failed:`, error));
22213
22262
  await api.execute("postAgentTurnReceipt", {
22214
22263
  agentId: this.agent.publicKey,
22215
- roomId: this.options.roomId,
22216
- requestId: item.id,
22264
+ roomId: cornerId,
22265
+ requestId,
22217
22266
  status: "complete",
22218
- generationId: `${this.agent.publicKey}:${this.options.roomId}`
22267
+ generationId: this.commandContext.generationId
22219
22268
  });
22220
22269
  await trace.finish("complete");
22221
22270
  } catch (error) {
22271
+ if (error instanceof TurnStoppedError || this.stoppedTurns.has(requestId)) {
22272
+ console.log(`[thin-core] corner ${cornerId} turn ${requestId} stopped by the requester`);
22273
+ await trace.finish("cancelled");
22274
+ return;
22275
+ }
22222
22276
  const reason = distillTurnFailureReason(error);
22223
22277
  await api.execute("postAgentTurnReceipt", {
22224
22278
  agentId: this.agent.publicKey,
22225
- roomId: this.options.roomId,
22226
- requestId: item.id,
22279
+ roomId: cornerId,
22280
+ requestId,
22227
22281
  status: "failed",
22228
- generationId: `${this.agent.publicKey}:${this.options.roomId}`,
22282
+ generationId: this.commandContext.generationId,
22229
22283
  reason
22230
22284
  });
22231
22285
  await trace.finish("failed", reason);
22232
22286
  throw error;
22233
22287
  } finally {
22234
22288
  this.busy = false;
22289
+ this.currentTurn = void 0;
22235
22290
  }
22236
22291
  }
22292
+ /**
22293
+ * Bring this worktree onto the corner's branch as GitHub currently has it,
22294
+ * before any work is done on top of it.
22295
+ *
22296
+ * The branch is the shared artifact: another member agent may have pushed to
22297
+ * it since this helper last looked, and a first touch of a corner this
22298
+ * helper did not open starts from whatever `room-runtime.ts` restored. A
22299
+ * divergence this cannot rebase away is raised, not pushed over — the turn
22300
+ * fails with that sentence and the server inscribes it in the corner.
22301
+ */
22302
+ async syncBranch() {
22303
+ const repository = this.options.repository;
22304
+ if (!repository)
22305
+ return;
22306
+ const token = await this.options.api.execute("getRoomGitHubToken", { roomId: this.options.parentRoomId }).then((granted) => granted.token).catch(() => repository.githubToken);
22307
+ await syncCornerBranch({
22308
+ worktreePath: this.options.worktreePath,
22309
+ featureBranch: repository.featureBranch,
22310
+ env: { ...process.env, GH_TOKEN: token, GITHUB_TOKEN: token, GIT_TERMINAL_PROMPT: "0" }
22311
+ });
22312
+ }
22237
22313
  async run() {
22238
- const { api, roomId, signal } = this.options;
22239
- let cursor3;
22240
- const processedInboxIds = /* @__PURE__ */ new Set();
22241
- const pushedInbox = [];
22242
- let pendingPushedCursor;
22243
- let liveConnected = false;
22244
- let stopLive;
22314
+ const { api, cornerId, signal } = this.options;
22245
22315
  try {
22246
- const activation = await api.execute("getRoomInbox", { roomId, startAtLatest: true });
22247
- cursor3 = activation.cursor;
22248
- const rewindSupported = Array.isArray(activation.rewindIds);
22249
- for (const id of activation.rewindIds ?? [])
22250
- processedInboxIds.add(id);
22251
- stopLive = api.liveSubscribe?.(roomId, cursor3, (items, pushedCursor) => {
22252
- pushedInbox.push(...items);
22253
- pendingPushedCursor = laterInboxCursor(pendingPushedCursor, pushedCursor);
22254
- this.wakeIntake?.();
22255
- this.wakeIntake = void 0;
22256
- }, (connected, capabilities) => {
22257
- liveConnected = connected && capabilities?.pushIntake === true;
22258
- this.options.health.presence(connected && !this.options.config.modelUnavailable ? "online" : "offline");
22259
- this.wakeIntake?.();
22260
- this.wakeIntake = void 0;
22261
- }, {
22262
- ...this.options.config.daemonReleaseVersion ? { releaseVersion: this.options.config.daemonReleaseVersion } : {},
22263
- ...this.options.config.daemonSourceSha ? { sourceSha: this.options.config.daemonSourceSha } : {},
22264
- available: !this.options.config.modelUnavailable
22316
+ await runServerCommandIntake({
22317
+ api,
22318
+ roomId: cornerId,
22319
+ agentId: this.agent.publicKey,
22320
+ context: this.commandContext,
22321
+ signal,
22322
+ pollMs: this.options.pollMs,
22323
+ presence: {
22324
+ ...this.options.config.daemonReleaseVersion ? { releaseVersion: this.options.config.daemonReleaseVersion } : {},
22325
+ ...this.options.config.daemonSourceSha ? { sourceSha: this.options.config.daemonSourceSha } : {},
22326
+ available: !this.options.config.modelUnavailable
22327
+ },
22328
+ onWake: (wake) => {
22329
+ this.wakeIntake = wake;
22330
+ },
22331
+ onPoll: () => this.options.onPoll(),
22332
+ onError: (error) => console.error("[thin-core] corner command failed", error),
22333
+ stop: (requestId) => this.stopTurn(requestId),
22334
+ closed: async () => {
22335
+ const state = await api.execute("getCornerRestoreState", { cornerId });
22336
+ if (!state.closeRequested)
22337
+ return false;
22338
+ await this.options.onCloseRequested();
22339
+ return true;
22340
+ },
22341
+ run: (command) => this.prompt(command.turnRequestId, command.source.body, command.source.attachments, command.source.authorId, command.reason === "corner_check" ? [command.source.body] : void 0)
22265
22342
  });
22266
- while (!signal?.aborted) {
22267
- try {
22268
- if (!this.activeTurn && this.queuedTurns.length) {
22269
- this.startPrompt(this.queuedTurns.shift());
22270
- }
22271
- const pollNow = pushedInbox.length === 0 && (!liveConnected || this.reconciliationRequested);
22272
- const inbox = !pollNow ? { items: [], cursor: void 0 } : await api.execute("getRoomInbox", {
22273
- roomId,
22274
- ...cursor3 ? { after: cursor3 } : {},
22275
- ...rewindSupported ? { rewind: true } : {},
22276
- limit: 200
22277
- });
22278
- if (pollNow) {
22279
- this.reconciliationRequested = false;
22280
- }
22281
- const delivered = [...pushedInbox.splice(0), ...inbox.items];
22282
- for (const item of delivered) {
22283
- if (processedInboxIds.has(item.id))
22284
- continue;
22285
- processedInboxIds.add(item.id);
22286
- while (processedInboxIds.size > 1e4)
22287
- processedInboxIds.delete(processedInboxIds.values().next().value);
22288
- if (!inboxItemTriggersTurn(item, this.agent.publicKey))
22289
- continue;
22290
- if (!inboxItemSkipsSenderPolicy(item, this.agent.publicKey)) {
22291
- const authority = await api.execute("getRoomAuthority", {
22292
- roomId,
22293
- principalId: item.authorId
22294
- });
22295
- const humanPermitted = authority.principalKind === "human" ? await this.currentPrincipalCanDrive(this.options.workspaceId, item.authorId) : false;
22296
- if (!roomPrincipalMayAddressAgent(authority, humanPermitted))
22297
- continue;
22298
- }
22299
- const active = this.activeTurn;
22300
- if (!active)
22301
- this.startPrompt(item);
22302
- else if (active.phase === "prompting")
22303
- this.steer(active, item);
22304
- else
22305
- this.queuedTurns.push(item);
22306
- }
22307
- cursor3 = laterInboxCursor(cursor3, laterInboxCursor(inbox.cursor, pendingPushedCursor));
22308
- pendingPushedCursor = void 0;
22309
- api.updateLiveCursor?.(roomId, cursor3);
22310
- if (pollNow)
22311
- this.options.health.poll();
22312
- if (!pushedInbox.length) {
22313
- await Promise.race([
22314
- wait2(liveConnected ? 2147483647 : this.options.pollMs ?? 1e3, signal),
22315
- new Promise((resolve30) => {
22316
- this.wakeIntake = resolve30;
22317
- })
22318
- ]);
22319
- }
22320
- } catch (error) {
22321
- if (signal?.aborted)
22322
- break;
22323
- this.options.health.failure(1e3);
22324
- console.error(`[thin-core] monolith Room ${roomId} turn loop failed:`, error);
22325
- await wait2(1e3, signal);
22326
- }
22327
- }
22328
22343
  } finally {
22329
- stopLive?.();
22330
- this.wakeIntake = void 0;
22331
- this.options.grantRunner?.unregister(roomId);
22332
- if (this.activeTurn?.phase === "prompting" && this.client && this.sessionId) {
22333
- this.client.sessionCancel(this.sessionId);
22334
- }
22335
- await this.activeTurn?.promise;
22336
- await this.options.scheduler.suspend(roomId);
22344
+ this.options.grantRunner?.unregister(cornerId);
22345
+ await this.options.scheduler.suspend(cornerId);
22337
22346
  }
22338
22347
  }
22339
22348
  };
22340
- function roomMessagePrompt(author, body, attachments, delivered, harnessAcceptsImages = true) {
22341
- const message = body.trim() || "(shared attachments)";
22342
- const rendered = author ? `${author}: ${message}` : message;
22343
- return [rendered, ...attachmentPromptLines(attachments, delivered, harnessAcceptsImages)].join("\n");
22344
- }
22345
- async function wait2(ms, signal) {
22346
- if (signal?.aborted)
22347
- return;
22348
- await new Promise((resolveWait) => {
22349
- const done = () => {
22350
- clearTimeout(timer);
22351
- signal?.removeEventListener("abort", done);
22352
- resolveWait();
22353
- };
22354
- const timer = setTimeout(done, ms);
22355
- signal?.addEventListener("abort", done, { once: true });
22356
- });
22357
- }
22358
22349
 
22359
22350
  // apps/body/dist/session-scheduler.js
22360
22351
  var DEFAULT_PER_ROOM_LIVE_SESSIONS = 10;
@@ -22771,11 +22762,11 @@ function shouldPostInitialCornerWorkingState(restore, isOpener = true) {
22771
22762
  }
22772
22763
  async function materializeCornerWorktree(input) {
22773
22764
  const remote = roomCheckoutRemote(input.remote);
22774
- const repositoryHash = createHash4("sha256").update(remote).digest("hex").slice(0, 24);
22765
+ const repositoryHash = createHash5("sha256").update(remote).digest("hex").slice(0, 24);
22775
22766
  const gitCommonDir = resolve19(input.supervisorRoot, "beeline", "repositories", `${repositoryHash}.git`);
22776
22767
  const path = resolve19(input.supervisorRoot, "beeline", "corners", input.cornerId);
22777
- await mkdir11(dirname6(gitCommonDir), { recursive: true, mode: 448 });
22778
- await mkdir11(dirname6(path), { recursive: true, mode: 448 });
22768
+ await mkdir12(dirname7(gitCommonDir), { recursive: true, mode: 448 });
22769
+ await mkdir12(dirname7(path), { recursive: true, mode: 448 });
22779
22770
  const authEnv = githubGitEnv(input.token);
22780
22771
  if (!existsSync4(resolve19(gitCommonDir, "HEAD"))) {
22781
22772
  await execFileAsync4("git", ["clone", "--bare", remote, gitCommonDir], {
@@ -22962,9 +22953,6 @@ var RoomRuntimeCoordinator = class {
22962
22953
  this.drainDeadlineAt = Math.min(this.drainDeadlineAt ?? Number.POSITIVE_INFINITY, deadlineAt);
22963
22954
  }
22964
22955
  }
22965
- async currentPrincipalCanDrive(roomId, workspaceId, principalId) {
22966
- return this.running.get(roomId)?.body.currentPrincipalCanDrive(workspaceId, principalId);
22967
- }
22968
22956
  async prepareForForcedUpdateRestart() {
22969
22957
  const rooms = [...this.running.values()];
22970
22958
  await Promise.allSettled(rooms.filter((room) => room.body.isBusy()).map((room) => room.body.prepareForForcedUpdateRestart()));
@@ -23049,7 +23037,7 @@ var RoomRuntimeCoordinator = class {
23049
23037
  return this.runtime.rooms.find((room) => room.channelId === roomId);
23050
23038
  }
23051
23039
  roomRoot(roomId) {
23052
- return this.roomRecord(roomId)?.root ?? resolve19(dirname6(this.configPath), "rooms", roomId);
23040
+ return this.roomRecord(roomId)?.root ?? resolve19(dirname7(this.configPath), "rooms", roomId);
23053
23041
  }
23054
23042
  roomAgentHomeRoot(workspaceRoot, required = false) {
23055
23043
  const flag = process.env.BUZZY_BODY_ROOM_HOME;
@@ -23072,9 +23060,9 @@ var RoomRuntimeCoordinator = class {
23072
23060
  ...this.baseConfig,
23073
23061
  workspaceRoot,
23074
23062
  agentPrivateRoot: resolve19(workspaceRoot, "agent-private"),
23075
- agentMemoryRoot: resolve19(dirname6(this.configPath), "memory"),
23076
- openRouterRoutingCacheDir: openRouterRoutingCacheDir(dirname6(this.configPath)),
23077
- turnTraceDir: turnTraceDirectory(dirname6(this.configPath)),
23063
+ agentMemoryRoot: resolve19(dirname7(this.configPath), "memory"),
23064
+ openRouterRoutingCacheDir: openRouterRoutingCacheDir(dirname7(this.configPath)),
23065
+ turnTraceDir: turnTraceDirectory(dirname7(this.configPath)),
23078
23066
  ...agentHomeRoot ? { agentHomeRoot } : {}
23079
23067
  };
23080
23068
  }
@@ -23140,9 +23128,9 @@ var RoomRuntimeCoordinator = class {
23140
23128
  return this.roomRoot(roomId);
23141
23129
  const remote = roomCheckoutRemote(repository.remote);
23142
23130
  const targetBranch = repository.targetBranch || "main";
23143
- const checkoutId = createHash4("sha256").update(`${remote}\0${targetBranch}`).digest("hex").slice(0, 24);
23131
+ const checkoutId = createHash5("sha256").update(`${remote}\0${targetBranch}`).digest("hex").slice(0, 24);
23144
23132
  const path = resolve19(this.runtime.supervisorRoot, "beeline", "room-checkouts", checkoutId);
23145
- await mkdir11(dirname6(path), { recursive: true, mode: 448 });
23133
+ await mkdir12(dirname7(path), { recursive: true, mode: 448 });
23146
23134
  const token = remote.startsWith("https://github.com/") ? await this.options.daemonApi.execute("getRoomGitHubToken", { roomId }) : void 0;
23147
23135
  const env = token ? githubGitEnv(token.token) : process.env;
23148
23136
  if (!existsSync4(resolve19(path, ".git"))) {
@@ -23208,7 +23196,7 @@ var RoomRuntimeCoordinator = class {
23208
23196
  }) : void 0;
23209
23197
  const workspacePath = worktree?.path ?? resolve19(this.roomRoot(corner.cornerId), "scratch");
23210
23198
  if (!worktree)
23211
- await mkdir11(workspacePath, { recursive: true, mode: 448 });
23199
+ await mkdir12(workspacePath, { recursive: true, mode: 448 });
23212
23200
  const isOpener = !corner.openedBy || corner.openedBy === this.agent.publicKey;
23213
23201
  if (worktree && shouldPostInitialCornerWorkingState(restore, isOpener)) {
23214
23202
  await this.options.daemonApi.execute("postCornerRemoteState", {
@@ -23507,9 +23495,9 @@ var ThinDaemonCore = class {
23507
23495
 
23508
23496
  // apps/body/dist/systemd.js
23509
23497
  import { execFile as execFile6 } from "node:child_process";
23510
- import { mkdir as mkdir12, readFile as readFile8, writeFile as writeFile8 } from "node:fs/promises";
23498
+ import { mkdir as mkdir13, readFile as readFile8, writeFile as writeFile9 } from "node:fs/promises";
23511
23499
  import { homedir as homedir8 } from "node:os";
23512
- import { dirname as dirname7, resolve as resolve20 } from "node:path";
23500
+ import { dirname as dirname8, resolve as resolve20 } from "node:path";
23513
23501
  import { setTimeout as sleep } from "node:timers/promises";
23514
23502
  import { promisify as promisify5 } from "node:util";
23515
23503
  var execFileAsync5 = promisify5(execFile6);
@@ -23580,8 +23568,8 @@ async function installAgentService(publicKey, options = {}) {
23580
23568
  const content = agentServiceUnit();
23581
23569
  const existing = await readFile8(path, "utf8").catch(() => "");
23582
23570
  if (existing !== content) {
23583
- await mkdir12(dirname7(path), { recursive: true, mode: 448 });
23584
- await writeFile8(path, content, { mode: 384 });
23571
+ await mkdir13(dirname8(path), { recursive: true, mode: 448 });
23572
+ await writeFile9(path, content, { mode: 384 });
23585
23573
  }
23586
23574
  const run2 = options.run ?? runSystemctl;
23587
23575
  await run2(["daemon-reload"]);
@@ -23661,7 +23649,7 @@ async function retireRemovedAgent(runtime, options = {}) {
23661
23649
  }
23662
23650
 
23663
23651
  // apps/body/dist/start-command.js
23664
- import { dirname as dirname8 } from "node:path";
23652
+ import { dirname as dirname9 } from "node:path";
23665
23653
  var import_picocolors = __toESM(require_picocolors(), 1);
23666
23654
  var DEFAULT_RESTART_DRAIN_TIMEOUT_MS = 30 * 6e4;
23667
23655
  var RESTART_WAIT_REPORT_INTERVAL_MS = 3e4;
@@ -23736,7 +23724,7 @@ async function runStartCommand(args, interactiveUi) {
23736
23724
  continue;
23737
23725
  }
23738
23726
  const spinnerHandle = spinner();
23739
- spinnerHandle.start(`Starting ${dirname8(path)}\u2026`);
23727
+ spinnerHandle.start(`Starting ${dirname9(path)}\u2026`);
23740
23728
  try {
23741
23729
  await startRuntime(path, spinnerHandle);
23742
23730
  spinnerHandle.stop(import_picocolors.default.green("Started."));
@@ -23752,9 +23740,9 @@ async function runStartCommand(args, interactiveUi) {
23752
23740
 
23753
23741
  // apps/body/dist/connect-command.js
23754
23742
  import { spawn as spawn5 } from "node:child_process";
23755
- import { createHash as createHash6 } from "node:crypto";
23756
- import { chmod as chmod5, mkdir as mkdir14, readFile as readFile10, unlink as unlink2, writeFile as writeFile10 } from "node:fs/promises";
23757
- import { dirname as dirname10, resolve as resolve22 } from "node:path";
23743
+ import { createHash as createHash7 } from "node:crypto";
23744
+ import { chmod as chmod5, mkdir as mkdir15, readFile as readFile10, unlink as unlink2, writeFile as writeFile11 } from "node:fs/promises";
23745
+ import { dirname as dirname11, resolve as resolve22 } from "node:path";
23758
23746
  import { stdin as stdin3, stdout as stdout4 } from "node:process";
23759
23747
 
23760
23748
  // apps/body/dist/clack-support.js
@@ -24328,7 +24316,7 @@ function requestConnectGrant(baseUrl, pairingCode, selection, fetchImpl) {
24328
24316
  const normalizedPairingCode = normalizeAgentPairingCode(pairingCode);
24329
24317
  if (!normalizedPairingCode)
24330
24318
  throw new Error("invalid pairing code");
24331
- const avatarSeed = createHash6("sha256").update(normalizedPairingCode.toUpperCase()).digest("hex").slice(0, 32);
24319
+ const avatarSeed = createHash7("sha256").update(normalizedPairingCode.toUpperCase()).digest("hex").slice(0, 32);
24332
24320
  return jsonRequest(`${baseUrl}/auth/agent/connect`, {
24333
24321
  pairing_code: normalizedPairingCode,
24334
24322
  harness: selection.harness,
@@ -24398,8 +24386,8 @@ function providerEnvironment(selection) {
24398
24386
  };
24399
24387
  }
24400
24388
  async function writePrivateJson(path, value) {
24401
- await mkdir14(dirname10(path), { recursive: true, mode: 448 });
24402
- await writeFile10(path, `${JSON.stringify(value, null, 2)}
24389
+ await mkdir15(dirname11(path), { recursive: true, mode: 448 });
24390
+ await writeFile11(path, `${JSON.stringify(value, null, 2)}
24403
24391
  `, { mode: 384 });
24404
24392
  await chmod5(path, 384);
24405
24393
  }
@@ -24408,9 +24396,9 @@ async function writeProviderEnv(selection, agentPubkey) {
24408
24396
  if (Object.keys(values).length === 0)
24409
24397
  return void 0;
24410
24398
  const path = resolve22(defaultSupervisorRoot(process.env), "beeline", "connect", `${agentPubkey}.env`);
24411
- await mkdir14(dirname10(path), { recursive: true, mode: 448 });
24399
+ await mkdir15(dirname11(path), { recursive: true, mode: 448 });
24412
24400
  const contents = Object.entries(values).map(([key, value]) => `${key}=${JSON.stringify(value)}`).join("\n");
24413
- await writeFile10(path, `${contents}
24401
+ await writeFile11(path, `${contents}
24414
24402
  `, { mode: 384 });
24415
24403
  await chmod5(path, 384);
24416
24404
  return path;
@@ -24566,7 +24554,7 @@ async function runConnectFinishCommand(path) {
24566
24554
  if (model) {
24567
24555
  const decision2 = await resolveOpenRouterRouting({
24568
24556
  model,
24569
- cacheDir: openRouterRoutingCacheDir(dirname10(connected.configPath)),
24557
+ cacheDir: openRouterRoutingCacheDir(dirname11(connected.configPath)),
24570
24558
  ...apiKey ? { apiKey } : {},
24571
24559
  probeTimeoutMs: 1e4
24572
24560
  });
@@ -24588,12 +24576,12 @@ init_self_update_manifest();
24588
24576
  // apps/body/dist/managed-update.js
24589
24577
  init_self_update();
24590
24578
  import { spawn as spawn6 } from "node:child_process";
24591
- import { mkdir as mkdir16, rm as rm6, stat as stat2, writeFile as writeFile12 } from "node:fs/promises";
24592
- import { dirname as dirname12, resolve as resolve24 } from "node:path";
24579
+ import { mkdir as mkdir17, rm as rm6, stat as stat2, writeFile as writeFile13 } from "node:fs/promises";
24580
+ import { dirname as dirname13, resolve as resolve24 } from "node:path";
24593
24581
 
24594
24582
  // apps/body/dist/update-rollback-alert.js
24595
- import { mkdir as mkdir15, readFile as readFile11, rename as rename4, unlink as unlink3, writeFile as writeFile11 } from "node:fs/promises";
24596
- import { dirname as dirname11, resolve as resolve23 } from "node:path";
24583
+ import { mkdir as mkdir16, readFile as readFile11, rename as rename4, unlink as unlink3, writeFile as writeFile12 } from "node:fs/promises";
24584
+ import { dirname as dirname12, resolve as resolve23 } from "node:path";
24597
24585
  var REPORT_INTERVAL_MS = 60 * 60 * 1e3;
24598
24586
  var lastLogged = /* @__PURE__ */ new Map();
24599
24587
  function updateRollbackAlertPath(runtimeDir) {
@@ -24602,8 +24590,8 @@ function updateRollbackAlertPath(runtimeDir) {
24602
24590
  async function writeAlert(runtimeDir, alert) {
24603
24591
  const path = updateRollbackAlertPath(runtimeDir);
24604
24592
  const staged = `${path}.${process.pid}.tmp`;
24605
- await mkdir15(dirname11(path), { recursive: true });
24606
- await writeFile11(staged, `${JSON.stringify(alert, null, 2)}
24593
+ await mkdir16(dirname12(path), { recursive: true });
24594
+ await writeFile12(staged, `${JSON.stringify(alert, null, 2)}
24607
24595
  `, { mode: 384 });
24608
24596
  await rename4(staged, path);
24609
24597
  }
@@ -24668,11 +24656,11 @@ async function withInstallLock(layout, work, options = {}) {
24668
24656
  const now2 = options.now ?? Date.now;
24669
24657
  const lock = resolve24(layout.releasesRoot, ".state", "install.lock");
24670
24658
  const deadline = now2() + (options.waitMs ?? 1e4);
24671
- await mkdir16(dirname12(lock), { recursive: true });
24659
+ await mkdir17(dirname13(lock), { recursive: true });
24672
24660
  for (; ; ) {
24673
24661
  try {
24674
- await mkdir16(lock);
24675
- await writeFile12(resolve24(lock, "owner"), `${process.pid}
24662
+ await mkdir17(lock);
24663
+ await writeFile13(resolve24(lock, "owner"), `${process.pid}
24676
24664
  ${now2()}
24677
24665
  `, "utf8");
24678
24666
  break;
@@ -25082,7 +25070,7 @@ async function proveLoadedReleaseReady(layout, runtimeDir, loadedRelease, option
25082
25070
  });
25083
25071
  if (!accepted)
25084
25072
  return false;
25085
- await writeFile12(resolve24(runtimeDir, "daemon-ready.json"), `${JSON.stringify({
25073
+ await writeFile13(resolve24(runtimeDir, "daemon-ready.json"), `${JSON.stringify({
25086
25074
  readyAt: (options.now ?? Date.now)(),
25087
25075
  loadedRelease,
25088
25076
  functionalProof: options.functionalProof
@@ -25310,8 +25298,8 @@ async function runUpdateCommand(args) {
25310
25298
  init_self_update();
25311
25299
 
25312
25300
  // apps/body/dist/daemon-failure.js
25313
- import { mkdir as mkdir17, readFile as readFile12, rename as rename5, rm as rm7, writeFile as writeFile13 } from "node:fs/promises";
25314
- import { dirname as dirname13, resolve as resolve25 } from "node:path";
25301
+ import { mkdir as mkdir18, readFile as readFile12, rename as rename5, rm as rm7, writeFile as writeFile14 } from "node:fs/promises";
25302
+ import { dirname as dirname14, resolve as resolve25 } from "node:path";
25315
25303
  var DAEMON_FAILURE_LIMIT = 3;
25316
25304
  var DAEMON_FAILURE_WINDOW_MS = 5 * 6e4;
25317
25305
  function daemonFailurePath(runtimeDir) {
@@ -25331,8 +25319,8 @@ async function readFailureRecord(runtimeDir) {
25331
25319
  async function writeFailureRecord(runtimeDir, record2) {
25332
25320
  const path = daemonFailurePath(runtimeDir);
25333
25321
  const staged = `${path}.${process.pid}.tmp`;
25334
- await mkdir17(dirname13(path), { recursive: true, mode: 448 });
25335
- await writeFile13(staged, `${JSON.stringify(record2, null, 2)}
25322
+ await mkdir18(dirname14(path), { recursive: true, mode: 448 });
25323
+ await writeFile14(staged, `${JSON.stringify(record2, null, 2)}
25336
25324
  `, { mode: 384 });
25337
25325
  await rename5(staged, path);
25338
25326
  }
@@ -25354,7 +25342,7 @@ async function clearDaemonStartFailures(runtimeDir) {
25354
25342
  }
25355
25343
 
25356
25344
  // apps/body/dist/update-functional-probe.js
25357
- import { mkdir as mkdir18, rm as rm8 } from "node:fs/promises";
25345
+ import { mkdir as mkdir19, rm as rm8 } from "node:fs/promises";
25358
25346
  import { homedir as homedir10 } from "node:os";
25359
25347
  import { resolve as resolve26 } from "node:path";
25360
25348
  var UPDATE_PROBE_SESSION_TIMEOUT_MS = 1e4;
@@ -25400,6 +25388,9 @@ async function probeOutcome(run2) {
25400
25388
  await run2();
25401
25389
  return { kind: "served" };
25402
25390
  } catch (error) {
25391
+ if (error instanceof UpdateFunctionalProbeError && error.reason === "sandbox-unavailable") {
25392
+ return { kind: "sandbox-unavailable", reason: error.message };
25393
+ }
25403
25394
  if (error instanceof UpdateFunctionalProbeError && error.providerRefusal) {
25404
25395
  return { kind: "refused", ...error.providerRefusal };
25405
25396
  }
@@ -25412,6 +25403,8 @@ function describeCurrentReleaseOutcome(outcome) {
25412
25403
  return "the current release answered";
25413
25404
  case "refused":
25414
25405
  return `the current release got a different refusal (${outcome.reason})`;
25406
+ case "sandbox-unavailable":
25407
+ return `the current release has the same unavailable sandbox (${outcome.reason})`;
25415
25408
  case "unavailable":
25416
25409
  return `the current release could not be compared (${outcome.reason})`;
25417
25410
  }
@@ -25423,13 +25416,33 @@ async function runUpdateFunctionalProbe(input) {
25423
25416
  throw new UpdateFunctionalProbeError("model-unavailable", input.config.modelUnavailable.detail);
25424
25417
  }
25425
25418
  if (input.sandboxRequired && !input.config.bwrapPath) {
25426
- throw new UpdateFunctionalProbeError("sandbox-unavailable", "the configured bubblewrap boundary did not pass its startup self-test");
25419
+ const detail = input.sandboxUnavailableDetail ?? "the configured bubblewrap boundary did not pass its startup self-test";
25420
+ if (!input.compareWithCurrentRelease) {
25421
+ throw new UpdateFunctionalProbeError("sandbox-unavailable", detail);
25422
+ }
25423
+ const current = await input.compareWithCurrentRelease({
25424
+ kind: "sandbox-unavailable",
25425
+ reason: detail
25426
+ });
25427
+ if (current.kind !== "sandbox-unavailable") {
25428
+ throw new UpdateFunctionalProbeError("sandbox-unavailable", `${detail}; ${describeCurrentReleaseOutcome(current)}`);
25429
+ }
25430
+ console.warn("[body] update probe: the sandbox is unavailable to this release and the current release alike; preserving fail-closed sandboxing and accepting the bundle as inconclusive");
25431
+ return {
25432
+ harness,
25433
+ sandboxed: false,
25434
+ sessionStarted: false,
25435
+ turnCompleted: false,
25436
+ nativeTools: [],
25437
+ modelAnswer: "unavailable",
25438
+ modelAnswerReason: `${detail} (the current release has the same host sandbox failure)`
25439
+ };
25427
25440
  }
25428
25441
  const root = input.probeRoot ?? resolve26(input.runtimeDir, "update-functional-probe");
25429
25442
  const cwd = resolve26(root, "checkout");
25430
25443
  const homeRoot = resolve26(root, "agent-home");
25431
25444
  await rm8(root, { recursive: true, force: true });
25432
- await mkdir18(cwd, { recursive: true, mode: 448 });
25445
+ await mkdir19(cwd, { recursive: true, mode: 448 });
25433
25446
  let client;
25434
25447
  try {
25435
25448
  const agentEnv = {
@@ -25459,7 +25472,7 @@ async function runUpdateFunctionalProbe(input) {
25459
25472
  const { stateDirs, tmpDir } = harnessStateDirsFromEnv(agentEnv);
25460
25473
  const operatorHome = input.config.operatorHome ?? homedir10();
25461
25474
  const homeStateDirs = harnessHomeStateDirs(command, agentEnv.HOME ?? operatorHome);
25462
- await Promise.all(homeStateDirs.map((dir) => mkdir18(dir, { recursive: true })));
25475
+ await Promise.all(homeStateDirs.map((dir) => mkdir19(dir, { recursive: true })));
25463
25476
  spawnCommand = wrapAgentCommand({
25464
25477
  bwrapPath: input.config.bwrapPath,
25465
25478
  spec: {
@@ -25609,7 +25622,7 @@ async function runUpdateFunctionalProbe(input) {
25609
25622
 
25610
25623
  // apps/body/dist/current-release-probe.js
25611
25624
  import { spawn as spawn7 } from "node:child_process";
25612
- import { dirname as dirname14, join as join8 } from "node:path";
25625
+ import { dirname as dirname15, join as join9 } from "node:path";
25613
25626
  init_self_update();
25614
25627
  var CURRENT_RELEASE_PROBE_TIMEOUT_MS = 12e4;
25615
25628
  var UPDATE_PROBE_COMMAND = "update-probe";
@@ -25631,6 +25644,9 @@ function parseReport(line) {
25631
25644
  if (report.probe === "failed" && typeof report.reason === "string") {
25632
25645
  return { probe: "failed", reason: report.reason };
25633
25646
  }
25647
+ if (report.probe === "sandbox-unavailable" && typeof report.reason === "string") {
25648
+ return { probe: "sandbox-unavailable", reason: report.reason };
25649
+ }
25634
25650
  return void 0;
25635
25651
  }
25636
25652
  function outcomeFromReport(report) {
@@ -25639,12 +25655,17 @@ function outcomeFromReport(report) {
25639
25655
  return { kind: "served" };
25640
25656
  case "refused":
25641
25657
  return { kind: "refused", status: report.status, reason: report.reason };
25658
+ case "sandbox-unavailable":
25659
+ return { kind: "sandbox-unavailable", reason: report.reason };
25642
25660
  case "failed":
25661
+ if (report.reason.startsWith("functional update probe failed (sandbox-unavailable):")) {
25662
+ return { kind: "sandbox-unavailable", reason: report.reason };
25663
+ }
25643
25664
  return { kind: "unavailable", reason: report.reason };
25644
25665
  }
25645
25666
  }
25646
25667
  async function probeReleaseInSubprocess(input) {
25647
- const bundleDir = join8(input.layout.releasesRoot, input.releaseId);
25668
+ const bundleDir = join9(input.layout.releasesRoot, input.releaseId);
25648
25669
  const entrypoint = await resolveBundleEntrypoint(bundleDir);
25649
25670
  if (!entrypoint) {
25650
25671
  return { kind: "unavailable", reason: `release ${input.releaseId} has no runnable CLI entrypoint` };
@@ -25707,7 +25728,7 @@ async function runUpdateProbeCommand(args, options = {}) {
25707
25728
  const runtime = await readRuntimeRecord(configPath);
25708
25729
  const agent = runtimeAgentCommand(runtime);
25709
25730
  const config = loadBodyConfig({
25710
- workspaceRoot: join8(dirname14(configPath), "workspace"),
25731
+ workspaceRoot: join9(dirname15(configPath), "workspace"),
25711
25732
  llmEnvFile: runtime.llmEnvFile,
25712
25733
  env: { ...env, BUZZ_AGENT_BIN: agent.command, BUZZ_DEV_MCP_BIN: runtime.mcpBinary },
25713
25734
  agent
@@ -25725,21 +25746,22 @@ async function runUpdateProbeCommand(args, options = {}) {
25725
25746
  }
25726
25747
  const layout = beelineInstallLayout(env);
25727
25748
  const releaseId = (layout && await activeReleaseId(layout).catch(() => void 0)) ?? "unknown";
25728
- const runtimeDir = dirname14(configPath);
25749
+ const runtimeDir = dirname15(configPath);
25729
25750
  const outcome = await probeOutcome(() => (options.probe ?? runUpdateFunctionalProbe)({
25730
25751
  config,
25731
25752
  runtimeDir,
25732
25753
  releaseId,
25733
25754
  sandboxRequired: runtime.sandbox !== "off",
25755
+ sandboxUnavailableDetail: sandbox.advisory,
25734
25756
  // The successor's probe still holds `<runtimeDir>/update-functional-probe`.
25735
- probeRoot: join8(runtimeDir, "current-release-probe")
25757
+ probeRoot: join9(runtimeDir, "current-release-probe")
25736
25758
  }));
25737
- const report = outcome.kind === "served" ? { probe: "served" } : outcome.kind === "refused" ? { probe: "refused", status: outcome.status, reason: outcome.reason } : { probe: "failed", reason: outcome.reason };
25759
+ 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 };
25738
25760
  write(JSON.stringify(report));
25739
25761
  }
25740
25762
 
25741
25763
  // apps/body/dist/release-status.js
25742
- import { readFile as readFile13, readdir as readdir5, rename as rename6, writeFile as writeFile14 } from "node:fs/promises";
25764
+ import { readFile as readFile13, readdir as readdir5, rename as rename6, writeFile as writeFile15 } from "node:fs/promises";
25743
25765
  import { resolve as resolve27 } from "node:path";
25744
25766
  var DAEMON_RELEASE_STATUS_FILE = "release-status.json";
25745
25767
  var RELEASE_VERSION = /^v\d+\.\d+\.\d+$/;
@@ -25759,7 +25781,7 @@ async function writeDaemonReleaseStatus(runtimeDir, agentPubkey, identity, optio
25759
25781
  };
25760
25782
  const target = resolve27(runtimeDir, DAEMON_RELEASE_STATUS_FILE);
25761
25783
  const temporary = `${target}.${status.pid}.tmp`;
25762
- await writeFile14(temporary, `${JSON.stringify(status, null, 2)}
25784
+ await writeFile15(temporary, `${JSON.stringify(status, null, 2)}
25763
25785
  `, { mode: 384 });
25764
25786
  await rename6(temporary, target);
25765
25787
  return status;
@@ -25893,7 +25915,7 @@ var DaemonExitError = class extends Error {
25893
25915
  };
25894
25916
  async function runStoredDaemon(pathOrPointer) {
25895
25917
  const configPath = await resolveRuntimeConfigPath(pathOrPointer);
25896
- daemonFailureRuntimeDir = dirname15(configPath);
25918
+ daemonFailureRuntimeDir = dirname16(configPath);
25897
25919
  const accessMigration = await migrateRuntimeRecordAccessPolicy(configPath);
25898
25920
  let runtime = accessMigration.runtime;
25899
25921
  if (!runtime.transport) {
@@ -25905,7 +25927,7 @@ async function runStoredDaemon(pathOrPointer) {
25905
25927
  runtime = activated.runtime;
25906
25928
  const daemonApi = activated.client;
25907
25929
  const agent = runtimeAgentCommand(runtime);
25908
- await writeFile15(resolve29(dirname15(configPath), "daemon.pid"), `${process.pid}
25930
+ await writeFile16(resolve29(dirname16(configPath), "daemon.pid"), `${process.pid}
25909
25931
  `, { mode: 384 });
25910
25932
  const env = {
25911
25933
  ...process.env,
@@ -25913,7 +25935,7 @@ async function runStoredDaemon(pathOrPointer) {
25913
25935
  BUZZ_DEV_MCP_BIN: runtime.mcpBinary
25914
25936
  };
25915
25937
  const config = loadBodyConfig({
25916
- workspaceRoot: resolve29(dirname15(configPath), "workspace"),
25938
+ workspaceRoot: resolve29(dirname16(configPath), "workspace"),
25917
25939
  llmEnvFile: runtime.llmEnvFile,
25918
25940
  env,
25919
25941
  agent
@@ -25948,7 +25970,7 @@ async function runStoredDaemon(pathOrPointer) {
25948
25970
  const stop = () => controller.abort();
25949
25971
  process.once("SIGINT", stop);
25950
25972
  process.once("SIGTERM", stop);
25951
- const runtimeDir = dirname15(configPath);
25973
+ const runtimeDir = dirname16(configPath);
25952
25974
  const layout = beelineInstallLayout(process.env);
25953
25975
  const notifier = new SystemdNotifier();
25954
25976
  let rollbackAlertDrain;
@@ -26030,6 +26052,7 @@ async function runStoredDaemon(pathOrPointer) {
26030
26052
  runtimeDir,
26031
26053
  releaseId: loadedRelease ?? "unknown",
26032
26054
  sandboxRequired: runtime.sandbox !== "off",
26055
+ sandboxUnavailableDetail: sandbox.advisory,
26033
26056
  ...currentReleaseId ? {
26034
26057
  compareWithCurrentRelease: async (appeal) => {
26035
26058
  console.warn(`[thin-core] successor probe got no answer from the provider (${appeal.reason}); probing the current release ${currentReleaseId} for the same outcome`);
@@ -26094,7 +26117,7 @@ async function runStoredDaemon(pathOrPointer) {
26094
26117
  } finally {
26095
26118
  clearInterval(scratchSweepTimer);
26096
26119
  await notifier.stopping(stoppingStatus).catch(() => void 0);
26097
- const pidPath = resolve29(dirname15(configPath), "daemon.pid");
26120
+ const pidPath = resolve29(dirname16(configPath), "daemon.pid");
26098
26121
  const recorded = Number((await readFile14(pidPath, "utf8").catch(() => "")).trim());
26099
26122
  if (recorded === process.pid) {
26100
26123
  await unlink5(pidPath).catch(() => void 0);
@@ -26154,10 +26177,7 @@ async function main() {
26154
26177
  await repairInstallForwarders(startupLayout).catch(() => void 0);
26155
26178
  }
26156
26179
  const interactiveUi = command !== "daemon" && Boolean(stdin4.isTTY && stdout5.isTTY);
26157
- const llmEnvFile = process.env.BUZZY_BODY_LLM_FILE;
26158
- const workspaceRoot = process.env.BUZZY_BODY_WORKSPACE ?? "./body-workspace";
26159
26180
  if (command === "--version" || command === "version") {
26160
- const config = loadBodyConfig({ workspaceRoot, llmEnvFile });
26161
26181
  console.log(import_picocolors3.default.bold("beeline 0.0.0"));
26162
26182
  const layout = beelineInstallLayout(process.env);
26163
26183
  if (layout) {
@@ -26165,9 +26185,6 @@ async function main() {
26165
26185
  const active = await activeReleaseId(layout);
26166
26186
  console.log(`${import_picocolors3.default.dim("installed bundle:")} ${describeIdentity(identity)}${active ? ` (release ${active})` : ""}`);
26167
26187
  }
26168
- console.log(`${import_picocolors3.default.dim("[body] agent binary:")} ${config.agentCommand ?? config.agentBinary}`);
26169
- console.log(`${import_picocolors3.default.dim("[body] mcp binary:")} ${config.mcpBinary}`);
26170
- console.log(`${import_picocolors3.default.dim("[body] read-only mcp:")} ${config.readonlyMcpCommand}`);
26171
26188
  return;
26172
26189
  }
26173
26190
  if (command === "daemon") {
@@ -26177,7 +26194,7 @@ async function main() {
26177
26194
  const agentPubkey = agentFlag >= 0 ? args[agentFlag + 1] : void 0;
26178
26195
  if (!configPath && agentPubkey) {
26179
26196
  const configs = await findAgentRuntimeConfigPaths(process.env, process.cwd());
26180
- configPath = configs.find((candidate) => dirname15(candidate).endsWith(agentPubkey));
26197
+ configPath = configs.find((candidate) => dirname16(candidate).endsWith(agentPubkey));
26181
26198
  }
26182
26199
  if (!configPath && agentPubkey) {
26183
26200
  throw new DaemonExitError(`unknown agent ${agentPubkey}: no durable runtime exists; refusing systemd restart loop`, UNKNOWN_AGENT_EXIT_STATUS);
@@ -26205,7 +26222,7 @@ async function main() {
26205
26222
  if (!agentPubkey)
26206
26223
  throw new Error("stop requires --agent <pubkey>");
26207
26224
  const configs = await findAgentRuntimeConfigPaths(process.env, process.cwd());
26208
- const configPath = configs.find((candidate) => dirname15(candidate).endsWith(agentPubkey));
26225
+ const configPath = configs.find((candidate) => dirname16(candidate).endsWith(agentPubkey));
26209
26226
  if (!configPath)
26210
26227
  throw new Error(`no stored runtime found for agent ${agentPubkey}`);
26211
26228
  const runtime = await readRuntimeRecord(configPath);