usebeeline 0.0.63 → 0.0.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/usebeeline.mjs +1543 -1178
  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,7 +3984,7 @@ __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
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";
3990
3990
  import { spawn as spawn4 } from "node:child_process";
@@ -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);
@@ -6912,8 +6912,13 @@ function agentMessageRuns(updates, agentLabel) {
6912
6912
  let current = "";
6913
6913
  let lastWasText = false;
6914
6914
  for (const u4 of updates) {
6915
+ const isToolCall = u4.update.sessionUpdate === "tool_call";
6915
6916
  const delta = normalizeStreamDelta(agentMessageChunkText(u4.update), agentLabel);
6916
6917
  if (!delta) {
6918
+ if (isToolCall && current) {
6919
+ runs.push(current);
6920
+ current = "";
6921
+ }
6917
6922
  lastWasText = false;
6918
6923
  continue;
6919
6924
  }
@@ -6934,9 +6939,6 @@ function agentMessageRuns(updates, agentLabel) {
6934
6939
  return true;
6935
6940
  });
6936
6941
  }
6937
- function joinAgentMessageChunks(updates, agentLabel) {
6938
- return agentMessageRuns(updates, agentLabel).join("\n\n");
6939
- }
6940
6942
  var RETRY_NARRATION_FRAGMENTS = [
6941
6943
  // `Retrying (attempt 2/3, waiting 4s)` — the pi/ox-alpha shape.
6942
6944
  /retrying\s*\((?:attempt|try)\s*\d+\s*\/\s*\d+(?:,\s*(?:waiting|backoff)\s*\d+(?:\.\d+)?s)?\)/gi,
@@ -7065,6 +7067,7 @@ function toolCallEntries(updates) {
7065
7067
  ...typeof update.title === "string" ? { title: update.title } : {},
7066
7068
  ...typeof update.kind === "string" ? { kind: update.kind } : {},
7067
7069
  ...typeof update.status === "string" ? { status: update.status } : {},
7070
+ ...sessionUpdate === "tool_result" ? { resultReceived: true } : {},
7068
7071
  ..."rawInput" in update ? { rawInput: update.rawInput } : {},
7069
7072
  ..."content" in update ? { content: update.content } : {},
7070
7073
  ..."rawOutput" in update ? { rawOutput: update.rawOutput } : {},
@@ -7327,8 +7330,10 @@ var AcpClient = class extends EventEmitter {
7327
7330
  onToolCalls?.(toolCallEntries(updates));
7328
7331
  if (onChunk) {
7329
7332
  const delta = agentMessageChunkText(u4.update);
7330
- if (delta)
7331
- onChunk(delta, joinAgentMessageChunks(updates, this.agentLabel));
7333
+ if (delta) {
7334
+ const runs = agentMessageRuns(updates, this.agentLabel);
7335
+ onChunk(delta, runs.join("\n\n"), runs.at(-1), runs);
7336
+ }
7332
7337
  }
7333
7338
  };
7334
7339
  this.on("session/update", onUpdate);
@@ -16919,6 +16924,18 @@ function laterInboxCursor(left, right) {
16919
16924
  return timeOrder > 0n ? left : right;
16920
16925
  return leftMatch[2] >= rightMatch[2] ? left : right;
16921
16926
  }
16927
+ function orderInboxItems(items) {
16928
+ return [...items].sort((left, right) => {
16929
+ const leftMatch = left.cursor?.match(/^(\d+),([0-9a-f]{64})$/);
16930
+ const rightMatch = right.cursor?.match(/^(\d+),([0-9a-f]{64})$/);
16931
+ if (!leftMatch || !rightMatch)
16932
+ return 0;
16933
+ const timeOrder = BigInt(leftMatch[1]) - BigInt(rightMatch[1]);
16934
+ if (timeOrder !== 0n)
16935
+ return timeOrder < 0n ? -1 : 1;
16936
+ return leftMatch[2] < rightMatch[2] ? -1 : leftMatch[2] > rightMatch[2] ? 1 : 0;
16937
+ });
16938
+ }
16922
16939
  function isAgentRemovedError(error) {
16923
16940
  return error instanceof DaemonApiError && error.status === 403 && error.code === AGENT_REMOVED_CODE;
16924
16941
  }
@@ -17134,7 +17151,7 @@ async function activateDaemonTransport(path, fetchImpl = fetch) {
17134
17151
 
17135
17152
  // apps/body/dist/room-runtime.js
17136
17153
  import { execFile as execFile5 } from "node:child_process";
17137
- import { createHash as createHash4 } from "node:crypto";
17154
+ import { createHash as createHash5 } from "node:crypto";
17138
17155
  import { existsSync as existsSync4, mkdirSync } from "node:fs";
17139
17156
  import { mkdir as mkdir11, rm as rm4 } from "node:fs/promises";
17140
17157
  import { dirname as dirname6, resolve as resolve19 } from "node:path";
@@ -17979,21 +17996,14 @@ var GrantRunnerServer = class {
17979
17996
 
17980
17997
  // apps/body/dist/monolith-corner-turn.js
17981
17998
  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";
17999
+ import { createHash as createHash4 } from "node:crypto";
18000
+ import { mkdir as mkdir10 } from "node:fs/promises";
18001
+ import { homedir as homedir7 } from "node:os";
18002
+ import { join as join6 } from "node:path";
17985
18003
  import { promisify as promisify3 } from "node:util";
17986
18004
 
17987
- // apps/body/dist/agent-home.js
17988
- 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";
17991
- 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";
17993
-
17994
- // apps/body/dist/beeline-skill.js
17995
- import { readFileSync as readFileSync3 } from "node:fs";
17996
- import { resolve as resolve11 } from "node:path";
18005
+ // packages/api-contract/dist/daemon-operations.js
18006
+ var AGENT_TO_AGENT_HOP_CAP = 3;
17997
18007
 
17998
18008
  // packages/api-contract/dist/system-events.js
17999
18009
  var SERVER_EVENT_KINDS = [
@@ -18003,7 +18013,8 @@ var SERVER_EVENT_KINDS = [
18003
18013
  "check-passed",
18004
18014
  "check-failed",
18005
18015
  "merged",
18006
- "grant-decided"
18016
+ "grant-decided",
18017
+ "turn-cancelled"
18007
18018
  ];
18008
18019
  function isServerEventKind(value) {
18009
18020
  return SERVER_EVENT_KINDS.includes(value);
@@ -18012,6 +18023,103 @@ var RESUME_KINDS = ["grant-decided"];
18012
18023
  function isResumeKind(value) {
18013
18024
  return RESUME_KINDS.includes(value);
18014
18025
  }
18026
+ var CONTROL_KINDS = ["turn-cancelled"];
18027
+ function isControlKind(value) {
18028
+ return CONTROL_KINDS.includes(value);
18029
+ }
18030
+
18031
+ // apps/body/dist/agent-response-rule.js
18032
+ var INBOX_DEDUPLICATION_LIMIT = 1e4;
18033
+ var CONTINUITY_WINDOW_LIMIT = 200;
18034
+ var AgentResponseRule = class {
18035
+ agentIds = /* @__PURE__ */ new Set();
18036
+ lastAgentBySender = /* @__PURE__ */ new Map();
18037
+ observedIds = /* @__PURE__ */ new Set();
18038
+ recentMessages = [];
18039
+ localReplySequence = 0;
18040
+ setAgents(agentIds) {
18041
+ this.agentIds = new Set(agentIds);
18042
+ this.rebuildLastAgentBySender();
18043
+ }
18044
+ observeAll(items) {
18045
+ for (const item of items)
18046
+ this.observe(item);
18047
+ }
18048
+ replaceHistory(items) {
18049
+ this.recentMessages.length = 0;
18050
+ this.lastAgentBySender.clear();
18051
+ for (const item of items)
18052
+ this.record(item);
18053
+ }
18054
+ observe(item) {
18055
+ if (this.observedIds.has(item.id))
18056
+ return;
18057
+ this.observedIds.add(item.id);
18058
+ while (this.observedIds.size > INBOX_DEDUPLICATION_LIMIT)
18059
+ this.observedIds.delete(this.observedIds.values().next().value);
18060
+ this.record(item);
18061
+ }
18062
+ record(item) {
18063
+ if (item.type !== "message")
18064
+ return;
18065
+ this.recentMessages.push(item);
18066
+ while (this.recentMessages.length > CONTINUITY_WINDOW_LIMIT)
18067
+ this.recentMessages.shift();
18068
+ this.rebuildLastAgentBySender();
18069
+ }
18070
+ rebuildLastAgentBySender() {
18071
+ this.lastAgentBySender.clear();
18072
+ for (const recent of this.recentMessages) {
18073
+ if (!recent.agentAuthor && !this.agentIds.has(recent.authorId))
18074
+ continue;
18075
+ const addressed = new Set(recent.mentionIds);
18076
+ if (recent.requestAuthorId)
18077
+ addressed.add(recent.requestAuthorId);
18078
+ if (recent.replyToAuthorId)
18079
+ addressed.add(recent.replyToAuthorId);
18080
+ for (const senderId of addressed) {
18081
+ if (senderId !== recent.authorId)
18082
+ this.lastAgentBySender.set(senderId, recent.authorId);
18083
+ }
18084
+ }
18085
+ }
18086
+ noteReply(agentId, senderIds) {
18087
+ this.record({
18088
+ id: `local-reply-${agentId}-${this.localReplySequence++}`,
18089
+ authorId: agentId,
18090
+ type: "message",
18091
+ mentionIds: [...senderIds],
18092
+ agentAuthor: true
18093
+ });
18094
+ }
18095
+ /** Whether trigger 2 applies. Explicit mention handling stays with each intake loop. */
18096
+ continues(item, agentId) {
18097
+ if (item.type !== "message" || item.authorId === agentId)
18098
+ return false;
18099
+ if (!this.agentIds.has(agentId))
18100
+ return false;
18101
+ if ((item.agentAuthor || this.agentIds.has(item.authorId)) && (item.agentHopCount ?? 0) >= AGENT_TO_AGENT_HOP_CAP)
18102
+ return false;
18103
+ if (item.agentMentionIds?.length || item.mentionIds.some((mentioned) => this.agentIds.has(mentioned)))
18104
+ return false;
18105
+ if (item.replyToMessageId)
18106
+ return item.replyToAuthorId === agentId;
18107
+ if (this.lastAgentBySender.get(item.authorId) !== agentId)
18108
+ return false;
18109
+ return true;
18110
+ }
18111
+ };
18112
+
18113
+ // apps/body/dist/agent-home.js
18114
+ import { existsSync as existsSync3, readFileSync as readFileSync4 } from "node:fs";
18115
+ import { createHash as createHash3, randomUUID as randomUUID2 } from "node:crypto";
18116
+ 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";
18117
+ import { homedir as homedir5 } from "node:os";
18118
+ import { basename as basename3, dirname as dirname5, join as join3, relative as relative2, resolve as resolve13, sep } from "node:path";
18119
+
18120
+ // apps/body/dist/beeline-skill.js
18121
+ import { readFileSync as readFileSync3 } from "node:fs";
18122
+ import { resolve as resolve11 } from "node:path";
18015
18123
 
18016
18124
  // packages/api-contract/dist/agent-pairing-code.js
18017
18125
  var CURRENT_AGENT_PAIRING_CODE = /^[0-9A-F]{8}-[0-9A-F]{8}$/;
@@ -19433,6 +19541,18 @@ function isCornerStatusRestatement(reply, systemLines) {
19433
19541
  return statusWords(text2).every((word) => !word || admitted.has(word));
19434
19542
  }
19435
19543
 
19544
+ // apps/body/dist/turn-stop.js
19545
+ var TurnStoppedError = class extends Error {
19546
+ name = "TurnStoppedError";
19547
+ };
19548
+ function turnStopRequestId(item, agentId) {
19549
+ if (item.type !== "system" || !item.mentionIds.includes(agentId))
19550
+ return void 0;
19551
+ if (item.systemEvent?.kind !== "turn-cancelled")
19552
+ return void 0;
19553
+ return item.requestId || void 0;
19554
+ }
19555
+
19436
19556
  // apps/body/dist/turn-stream.js
19437
19557
  function durableReplyText(agentText) {
19438
19558
  return sanitizeAgentReply(agentText);
@@ -19491,7 +19611,8 @@ var AgentTurnStream = class {
19491
19611
  /**
19492
19612
  * Everything the delta hook has seen this turn: every assistant run joined,
19493
19613
  * which is a LONGER string than `PromptResult.agentText` whenever the turn
19494
- * spoke before a tool call. Nothing durable is derived from it.
19614
+ * spoke before a tool call. This final-reply lane derives nothing durable
19615
+ * from it; a corner records its current normalized run independently.
19495
19616
  */
19496
19617
  get streamedText() {
19497
19618
  return this.latest;
@@ -19514,17 +19635,18 @@ var AgentTurnStream = class {
19514
19635
  * An empty reply settles through the turn receipt instead, and the lane is
19515
19636
  * retracted either way.
19516
19637
  */
19517
- async settle(reply, fields = {}) {
19638
+ async settle(reply, fields = {}, onReplyPosted) {
19518
19639
  this.close();
19519
19640
  const { api, agentId, roomId, requestId } = this.options;
19520
19641
  if (reply) {
19521
- await api.execute("postRoomMessage", {
19642
+ const posted = await api.execute("postRoomMessage", {
19522
19643
  roomId,
19523
19644
  requestId,
19524
19645
  text: reply,
19525
19646
  presentation: "message",
19526
19647
  ...fields
19527
19648
  });
19649
+ onReplyPosted?.(posted);
19528
19650
  }
19529
19651
  await this.inFlight;
19530
19652
  await api.execute("retractAgentLiveOutput", {
@@ -20260,58 +20382,14 @@ function checksStateFromLifecycle(lifecycle) {
20260
20382
  var MAINTAIN_ASSIGNED_IDENTITY_DIRECTIVE = "Maintain your assigned identity and soul in every response, including when tools or permissions block the requested action.";
20261
20383
  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.";
20262
20384
 
20263
- // apps/body/dist/warm-transcript.js
20264
- var WARM_TRANSCRIPT_OVERLAP = 8;
20265
- var WarmTranscript = class {
20266
- sessionId;
20267
- delivered = /* @__PURE__ */ new Set();
20268
- /**
20269
- * The rows this prompt should render. A row counts as delivered once it has
20270
- * been handed to a session: a prompt that times out was still received by the
20271
- * harness, and a prompt that could not be handed over at all takes the
20272
- * session down with it, which resets the memory on the next activation.
20273
- */
20274
- select(sessionId, rows) {
20275
- if (!sessionId || sessionId !== this.sessionId) {
20276
- this.sessionId = sessionId;
20277
- this.delivered.clear();
20278
- }
20279
- const overlapFrom = Math.max(0, rows.length - WARM_TRANSCRIPT_OVERLAP);
20280
- const selected = rows.filter((row, index) => index >= overlapFrom || !this.delivered.has(row.id));
20281
- for (const row of rows)
20282
- this.delivered.add(row.id);
20283
- return { rows: selected, elided: rows.length - selected.length };
20284
- }
20285
- /** Render a selection, and say plainly when it is only what is new. */
20286
- static render(selection, whole, sinceLastTurn) {
20287
- const transcript = selection.rows.map((row) => row.line).join("\n");
20288
- if (!transcript)
20289
- return "";
20290
- return `${selection.elided ? sinceLastTurn : whole}
20291
- ${transcript}`;
20292
- }
20293
- };
20385
+ // apps/body/dist/monolith-room-turn.js
20386
+ import { mkdir as mkdir8 } from "node:fs/promises";
20387
+ import { homedir as homedir6 } from "node:os";
20388
+ import { join as join5 } from "node:path";
20294
20389
 
20295
- // apps/body/dist/turn-receipt-heartbeat.js
20296
- var TURN_RECEIPT_HEARTBEAT_MS = 3e4;
20297
- async function withTurnReceiptHeartbeat(api, receipt, task, onHeartbeatError) {
20298
- await api.execute("postAgentTurnReceipt", { ...receipt, status: "working" });
20299
- let tail = Promise.resolve();
20300
- const timer = setInterval(() => {
20301
- tail = tail.catch(() => void 0).then(() => api.execute("postAgentTurnReceipt", {
20302
- ...receipt,
20303
- status: "working",
20304
- heartbeat: true
20305
- })).then(() => void 0).catch(onHeartbeatError);
20306
- }, TURN_RECEIPT_HEARTBEAT_MS);
20307
- timer.unref?.();
20308
- try {
20309
- return await task();
20310
- } finally {
20311
- clearInterval(timer);
20312
- await tail;
20313
- }
20314
- }
20390
+ // packages/api-contract/dist/scheduled-prompts.js
20391
+ var SCHEDULE_SCHEDULER_NAME = "Beeline Scheduler";
20392
+ var SCHEDULE_RAN_VERB = "ran a schedule for";
20315
20393
 
20316
20394
  // apps/body/dist/turn-trace.js
20317
20395
  import { appendFile, mkdir as mkdir7, readdir as readdir4, rm as rm3 } from "node:fs/promises";
@@ -20590,189 +20668,183 @@ var TurnTraceFile = class {
20590
20668
  }
20591
20669
  };
20592
20670
 
20593
- // apps/body/dist/corner-github-auth.js
20594
- import { chmod as chmod3, mkdir as mkdir8, writeFile as writeFile7 } from "node:fs/promises";
20595
- import { delimiter as delimiter2, resolve as resolve18 } from "node:path";
20596
- async function installCornerGitHubWrappers(input) {
20597
- const bin = resolve18(input.root, "beeline-github-bin");
20598
- await mkdir8(bin, { recursive: true, mode: 448 });
20599
- const common = {
20600
- node: process.execPath,
20601
- cli: input.cliEntrypoint,
20602
- config: input.runtimeConfigPath,
20603
- room: input.roomId
20604
- };
20605
- await writeLauncher(resolve18(bin, "git"), { ...common, command: input.gitBinary });
20606
- if (input.ghBinary)
20607
- await writeLauncher(resolve18(bin, "gh"), { ...common, command: input.ghBinary });
20608
- return {
20609
- PATH: [bin, input.inheritedPath].filter(Boolean).join(delimiter2),
20610
- // Static startup tokens take precedence over the refreshed token in gh.
20611
- GH_TOKEN: "",
20612
- GITHUB_TOKEN: ""
20613
- };
20614
- }
20615
- async function writeLauncher(path, config) {
20616
- const source = `#!/usr/bin/env node
20617
- import { spawnSync } from 'node:child_process';
20618
- const config = ${JSON.stringify(config)};
20619
- const authFailure = /(?:authentication failed|bad credentials|could not read username|http(?:\\/\\d(?:\\.\\d)?)? 40[13]|status (?:code )?40[13])/i;
20620
- function token() {
20621
- const result = spawnSync(config.node, [config.cli, 'corner-read-token', '--config', config.config, '--room', config.room], { encoding: 'utf8' });
20622
- if (result.status !== 0) {
20623
- process.stderr.write(result.stderr || 'Beeline could not refresh the repository credential.\\n');
20624
- process.exit(result.status || 1);
20671
+ // apps/body/dist/turn-receipt-heartbeat.js
20672
+ var TURN_RECEIPT_HEARTBEAT_MS = 3e4;
20673
+ async function withTurnReceiptHeartbeat(api, receipt, task, onHeartbeatError) {
20674
+ await api.execute("postAgentTurnReceipt", { ...receipt, status: "working" });
20675
+ let tail = Promise.resolve();
20676
+ const timer = setInterval(() => {
20677
+ tail = tail.catch(() => void 0).then(() => api.execute("postAgentTurnReceipt", {
20678
+ ...receipt,
20679
+ status: "working",
20680
+ heartbeat: true
20681
+ })).then(() => void 0).catch(onHeartbeatError);
20682
+ }, TURN_RECEIPT_HEARTBEAT_MS);
20683
+ timer.unref?.();
20684
+ try {
20685
+ return await task();
20686
+ } finally {
20687
+ clearInterval(timer);
20688
+ await tail;
20625
20689
  }
20626
- return result.stdout.trim();
20627
- }
20628
- function run(value) {
20629
- const env = { ...process.env, GH_TOKEN: value, GITHUB_TOKEN: value, GIT_TERMINAL_PROMPT: '0' };
20630
- return spawnSync(config.command, process.argv.slice(2), { env, encoding: 'buffer', stdio: ['inherit', 'pipe', 'pipe'] });
20631
- }
20632
- let result = run(token());
20633
- const diagnostic = Buffer.concat([result.stdout || Buffer.alloc(0), result.stderr || Buffer.alloc(0)]).toString('utf8');
20634
- if (result.status !== 0 && authFailure.test(diagnostic)) result = run(token());
20635
- if (result.stdout) process.stdout.write(result.stdout);
20636
- if (result.stderr) process.stderr.write(result.stderr);
20637
- if (result.error) throw result.error;
20638
- process.exit(result.status ?? 1);
20639
- `;
20640
- await writeFile7(path, source, { mode: 448 });
20641
- await chmod3(path, 448);
20642
20690
  }
20643
20691
 
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();
20651
- }
20652
- function serialized(value) {
20653
- if (typeof value === "string")
20654
- return value;
20655
- try {
20656
- return JSON.stringify(value) ?? "";
20657
- } catch {
20658
- return "";
20692
+ // apps/body/dist/warm-transcript.js
20693
+ var WARM_TRANSCRIPT_OVERLAP = 8;
20694
+ var WarmTranscript = class {
20695
+ sessionId;
20696
+ delivered = /* @__PURE__ */ new Set();
20697
+ /**
20698
+ * The rows this prompt should render. A row counts as delivered once it has
20699
+ * been handed to a session: a prompt that times out was still received by the
20700
+ * harness, and a prompt that could not be handed over at all takes the
20701
+ * session down with it, which resets the memory on the next activation.
20702
+ */
20703
+ select(sessionId, rows) {
20704
+ if (!sessionId || sessionId !== this.sessionId) {
20705
+ this.sessionId = sessionId;
20706
+ this.delivered.clear();
20707
+ }
20708
+ const overlapFrom = Math.max(0, rows.length - WARM_TRANSCRIPT_OVERLAP);
20709
+ const selected = rows.filter((row, index) => index >= overlapFrom || !this.delivered.has(row.id));
20710
+ for (const row of rows)
20711
+ this.delivered.add(row.id);
20712
+ return { rows: selected, elided: rows.length - selected.length };
20713
+ }
20714
+ /** Render a selection, and say plainly when it is only what is new. */
20715
+ static render(selection, whole, sinceLastTurn) {
20716
+ const transcript = selection.rows.map((row) => row.line).join("\n");
20717
+ if (!transcript)
20718
+ return "";
20719
+ return `${selection.elided ? sinceLastTurn : whole}
20720
+ ${transcript}`;
20659
20721
  }
20722
+ };
20723
+
20724
+ // apps/body/dist/monolith-room-turn.js
20725
+ function isRoomMcpPermissionRequest(request, mountedServers = ROOM_MOUNTED_MCP_SERVERS) {
20726
+ if (isSquireMcpPermissionRequest(request))
20727
+ return false;
20728
+ return isMountedMcpToolPermissionRequest(request, mountedServers);
20660
20729
  }
20661
- function record(value) {
20662
- return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
20730
+ function roomPrincipalMayAddressAgent(authority, humanPermitted) {
20731
+ if (!authority.member)
20732
+ return false;
20733
+ if (authority.principalKind === "agent")
20734
+ return true;
20735
+ if (authority.principalKind !== "human")
20736
+ return false;
20737
+ return authority.mayAddressAgent ?? humanPermitted;
20663
20738
  }
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}`;
20739
+ function isScheduledPrompt(item, agentId) {
20740
+ if (item.type !== "system" || !item.mentionIds.includes(agentId))
20741
+ return false;
20742
+ if (item.systemEvent?.kind)
20743
+ return item.systemEvent.kind === "schedule-ran";
20744
+ return item.systemEvent?.verb === SCHEDULE_RAN_VERB;
20671
20745
  }
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.";
20678
- }
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);
20746
+ function inboxItemAuthorName(item, agentId, names) {
20747
+ if (isScheduledPrompt(item, agentId))
20748
+ return SCHEDULE_SCHEDULER_NAME;
20749
+ const subject = item.systemEvent?.subject;
20750
+ if (item.type === "system" && subject?.name)
20751
+ return subject.name;
20752
+ return names.get(item.authorId) ?? item.authorId.slice(0, 12);
20683
20753
  }
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];
20754
+ function inboxItemPromptBody(item, agentId) {
20755
+ return isScheduledPrompt(item, agentId) ? item.systemEvent?.consequence ?? item.body : item.body;
20707
20756
  }
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";
20757
+ function isSubscribedEvent(item, agentId) {
20758
+ const kind = item.systemEvent?.kind;
20759
+ return item.type === "system" && kind !== void 0 && !isResumeKind(kind) && !isControlKind(kind) && item.mentionIds.includes(agentId);
20724
20760
  }
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) } : {};
20761
+ function inboxItemSkipsSenderPolicy(item, agentId) {
20762
+ if (isGrantDecisionLine(item, agentId))
20763
+ return true;
20764
+ if (item.type !== "system" || !item.mentionIds.includes(agentId))
20765
+ return false;
20766
+ const kind = item.systemEvent?.kind;
20767
+ return kind === void 0 ? isScheduledPrompt(item, agentId) : isServerEventKind(kind);
20732
20768
  }
20733
- function toolCallKey(call, index) {
20734
- return call.id ?? `tool-${index}`;
20769
+ function isGrantDecisionLine(item, agentId) {
20770
+ return item.type === "system" && item.mentionIds.includes(agentId) && parseGrantDecisionLine(item.body) !== void 0;
20735
20771
  }
20736
- function toolCallSettled(call) {
20737
- return /^(?:completed|complete|failed|error|succeeded|success|passed|done)$/i.test(call.status ?? "");
20772
+ function inboxItemTriggersTurn(item, agentId, continuesExchange = false) {
20773
+ if (item.authorId === agentId)
20774
+ return false;
20775
+ if (item.type === "message" && continuesExchange)
20776
+ return true;
20777
+ if (!item.mentionIds.includes(agentId))
20778
+ return false;
20779
+ return item.type === "message" || isSubscribedEvent(item, agentId) || isScheduledPrompt(item, agentId) || isGrantDecisionLine(item, agentId);
20738
20780
  }
20739
- function isSuccessfulCommit(call) {
20740
- if (/failed|error|denied/i.test(call.status ?? ""))
20781
+ function pendingGrantToolCall(call) {
20782
+ if (!/(?:^|[._:/-])request_grant$/i.test(call.title ?? ""))
20741
20783
  return false;
20742
- return /\bgit\s+commit\b|\bcommit(?:ted)?\s+(?:changes|files?)\b/i.test(`${call.title ?? ""} ${serialized(call.rawInput)}`);
20784
+ return /pending, card posted/i.test(typeof call.content === "string" ? call.content : JSON.stringify(call.content ?? ""));
20743
20785
  }
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 {
20755
- }
20786
+ function roomMentionDirectory(roster, selfId) {
20787
+ const rows = [];
20788
+ for (const member of roster.members) {
20789
+ if (member.identityId === selfId)
20790
+ continue;
20791
+ const handle = member.handle?.trim().replace(/^@/, "");
20792
+ const name = member.name?.trim() ?? "";
20793
+ const alias = handle || name;
20794
+ if (!alias)
20795
+ continue;
20796
+ const kind = member.kind === "agent" ? "agent" : "person";
20797
+ rows.push(`- @${alias}${name && name !== alias ? ` \u2014 ${name}` : ""} (${kind})`);
20756
20798
  }
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
- };
20799
+ if (!rows.length)
20800
+ return "";
20801
+ return [
20802
+ "Room members, and the exact spelling that tags each one:",
20803
+ ...rows,
20804
+ "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."
20805
+ ].join("\n");
20770
20806
  }
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);
20807
+ function escapeRegExp(value) {
20808
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
20774
20809
  }
20775
- var MonolithCornerTurnLoop = class {
20810
+ function agentReplyMentionIds(text2, roster, authorId) {
20811
+ const aliases = /* @__PURE__ */ new Map();
20812
+ for (const member of roster.members) {
20813
+ if (member.identityId === authorId)
20814
+ continue;
20815
+ for (const raw of [member.name, member.handle, member.soul?.name]) {
20816
+ const display = raw?.trim().replace(/^@/, "");
20817
+ if (!display)
20818
+ continue;
20819
+ const key = display.toLocaleLowerCase();
20820
+ const entry = aliases.get(key) ?? { display, ids: /* @__PURE__ */ new Set() };
20821
+ entry.ids.add(member.identityId);
20822
+ aliases.set(key, entry);
20823
+ }
20824
+ }
20825
+ for (const member of roster.members) {
20826
+ if (member.identityId === authorId || !member.handle)
20827
+ continue;
20828
+ const handle = member.handle.trim().replace(/^@/, "").toLocaleLowerCase();
20829
+ const canonical = aliases.get(handle);
20830
+ const legacy = `a_${handle}`;
20831
+ if (canonical && !aliases.has(legacy))
20832
+ aliases.set(legacy, { ...canonical, display: legacy });
20833
+ }
20834
+ const mentioned = [];
20835
+ for (const { display, ids } of [...aliases.values()].sort((left, right) => right.display.length - left.display.length)) {
20836
+ if (ids.size !== 1)
20837
+ continue;
20838
+ const pattern = new RegExp(`(^|[\\s([{])@${escapeRegExp(display)}(?=$|[\\s.,!?;:)\\]}])`, "iu");
20839
+ if (!pattern.test(text2))
20840
+ continue;
20841
+ const identityId = [...ids][0];
20842
+ if (!mentioned.includes(identityId))
20843
+ mentioned.push(identityId);
20844
+ }
20845
+ return mentioned;
20846
+ }
20847
+ var MonolithRoomTurnLoop = class {
20776
20848
  options;
20777
20849
  agent;
20778
20850
  reconciliationRequested = true;
@@ -20787,83 +20859,153 @@ var MonolithCornerTurnLoop = class {
20787
20859
  sessionId;
20788
20860
  /** The configuration the live session baked in; a change invalidates it. */
20789
20861
  sessionFingerprint;
20790
- /** What this exact ACP session has already been prompted with (`warm-transcript.ts`). */
20791
- warmTranscript = new WarmTranscript();
20792
20862
  /** The live session's environment, read back for pi's own turn record. */
20793
20863
  agentEnv = {};
20794
20864
  /** OpenRouter providers this activation pinned, in order (C92). */
20795
20865
  pinnedProviders = [];
20796
- /** Whether the pinned model takes images; `undefined` when the pin did not say. */
20797
- modelTakesImages;
20798
20866
  /** The one provider re-pinned after an empty completion, until the session ends. */
20799
20867
  pinnedProviderOverride;
20800
- turnIdentityInstructions = "";
20801
20868
  busy = false;
20802
- forcedStop = false;
20803
- activityTail = Promise.resolve();
20869
+ turnInstructionPrefix = "";
20870
+ activeTurn;
20871
+ queuedTurns = [];
20872
+ continuityRebuildRequested = false;
20804
20873
  /** Session scratch directory attachments are downloaded into (`TMPDIR/beeline-attachments`). */
20805
20874
  attachmentDir;
20806
- /** The session's TMPDIR, where a granted command's script argument may also live. */
20875
+ /** Whether the pinned model takes images; `undefined` when the pin did not say. */
20876
+ modelTakesImages;
20877
+ /** The session's TMPDIR: writable to a granted command in a Room, as it is to the harness (C94). */
20807
20878
  sessionScratchDir;
20808
- /** The turn in flight and who asked for it, for ledger rows and the grant runner. */
20809
- currentTurn;
20879
+ /** The `agent-home.ts` overlay this session writes into; a Room grant keeps it. */
20880
+ sessionStateDirs = [];
20881
+ /** What this exact ACP session has already been prompted with (`warm-transcript.ts`). */
20882
+ warmTranscript = new WarmTranscript();
20883
+ /** Local copies already delivered this session, by message id, so transcript renders reuse them. */
20884
+ deliveredAttachments = /* @__PURE__ */ new Map();
20885
+ /** Names from the latest roster read, for ledger bylines the runner writes. */
20886
+ memberNames = /* @__PURE__ */ new Map();
20887
+ /** The request id of the turn that paused on a grant card, until its decision arrives. */
20888
+ pausedOnGrantRequestId;
20810
20889
  /** Operator-local turn traces; built once when the daemon configured a directory. */
20811
20890
  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;
20891
+ /** Per-sender continuity, shared in shape with corner intake. */
20892
+ responseRule = new AgentResponseRule();
20819
20893
  constructor(options) {
20820
20894
  this.options = options;
20821
20895
  this.agent = runtimeIdentity(options.runtime.agent);
20822
- options.grantRunner?.register(options.cornerId, {
20896
+ options.grantRunner?.register(options.roomId, {
20823
20897
  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
20898
+ cwd: options.cwd,
20899
+ // A top-level Room keeps its read-only promise for grants too: the runner
20900
+ // wraps the command in this Room's own mount table (C94).
20901
+ writePolicy: () => this.grantWritePolicy(),
20902
+ turn: () => this.currentTurnForRunner()
20833
20903
  });
20834
20904
  }
20835
20905
  isBusy() {
20836
20906
  return this.busy;
20837
20907
  }
20838
- currentPrincipalCanDrive(_workspaceId, _principalId) {
20839
- return Promise.resolve(true);
20840
- }
20841
- refreshPersonaForSoulUpdate() {
20842
- return this.options.scheduler.suspend(this.options.cornerId);
20843
- }
20844
- async prepareForForcedUpdateRestart() {
20845
- this.forcedStop = true;
20846
- }
20847
- async forceRecoverRoom() {
20848
- if (this.client && this.sessionId)
20849
- this.client.sessionCancel(this.sessionId);
20850
- await this.options.scheduler.forceSuspend(this.options.cornerId);
20851
- }
20852
- async roster() {
20853
- const roster = await this.options.api.execute("getWorkspaceRoster", {
20854
- agentId: this.agent.publicKey,
20855
- workspaceId: this.options.workspaceId
20856
- });
20857
- 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
- return roster;
20908
+ /** The turn a `request_grant` paused, if any (cleared when its decision resumes it). */
20909
+ pausedGrantRequestId() {
20910
+ return this.pausedOnGrantRequestId;
20860
20911
  }
20861
20912
  /**
20862
- * Drop this corner's live harness process. The next activation starts cold.
20863
- * A rotation is a fact about one live session, so the pin goes with it.
20864
- */
20865
- async discardSession() {
20866
- const client = this.client;
20913
+ * What the grant runner may write here: the session's own scratch and home
20914
+ * overlay and nothing else, enforced by the same read-only mount table the
20915
+ * harness runs under. With no usable bwrap there is no way to keep that
20916
+ * promise, so the policy carries no path and the runner refuses the run
20917
+ * rather than widening the boundary.
20918
+ */
20919
+ grantWritePolicy() {
20920
+ return {
20921
+ surface: "room",
20922
+ ...this.options.config.bwrapPath ? { bwrapPath: this.options.config.bwrapPath } : {},
20923
+ ...this.sessionScratchDir ? { scratch: this.sessionScratchDir } : {},
20924
+ ...this.sessionStateDirs.length ? { harnessStateDirs: this.sessionStateDirs } : {},
20925
+ maskPaths: credentialMaskPaths(this.options.config.sandboxMaskPaths, this.options.config.operatorHome ?? homedir6())
20926
+ };
20927
+ }
20928
+ /**
20929
+ * One turn's stopwatch. It is created for every turn — measuring is cheap —
20930
+ * and only WRITES when the daemon configured a runtime directory to write
20931
+ * into, so a standalone or test Body stays silent.
20932
+ */
20933
+ beginTurnTrace(requestId) {
20934
+ const directory = this.options.config.turnTraceDir;
20935
+ if (directory)
20936
+ this.turnTraceSink ??= new TurnTraceFile(directory);
20937
+ return new TurnTrace({
20938
+ surface: "room",
20939
+ agentId: this.agent.publicKey,
20940
+ roomId: this.options.roomId,
20941
+ requestId,
20942
+ ...this.turnTraceSink ? { sink: this.turnTraceSink } : {}
20943
+ });
20944
+ }
20945
+ currentTurnForRunner() {
20946
+ const active = this.activeTurn;
20947
+ if (!active)
20948
+ return void 0;
20949
+ return { requestId: active.item.id, requester: this.requesterOf(active.item.authorId) };
20950
+ }
20951
+ requesterOf(authorId) {
20952
+ const name = this.memberNames.get(authorId);
20953
+ return { pubkey: authorId, ...name ? { name } : {} };
20954
+ }
20955
+ currentPrincipalCanDrive(_workspaceId, principalId) {
20956
+ return Promise.resolve(isSenderPermitted(this.options.config.accessPolicy ?? LEGACY_ACCESS_POLICY, principalId, this.options.config.accessOwnerPubkey, this.options.config.accessAllowlist));
20957
+ }
20958
+ async refreshPersonaForSoulUpdate() {
20959
+ await this.options.scheduler.suspend(this.options.roomId);
20960
+ }
20961
+ async prepareForForcedUpdateRestart() {
20962
+ }
20963
+ async forceRecoverRoom() {
20964
+ if (this.client && this.sessionId)
20965
+ this.client.sessionCancel(this.sessionId);
20966
+ await this.options.scheduler.forceSuspend(this.options.roomId);
20967
+ }
20968
+ async roster() {
20969
+ const roster = await this.options.api.execute("getWorkspaceRoster", {
20970
+ agentId: this.agent.publicKey,
20971
+ workspaceId: this.options.workspaceId
20972
+ });
20973
+ this.memberNames = new Map(roster.members.map((member) => [member.identityId, member.name]));
20974
+ this.responseRule.setAgents(roster.members.filter((member) => member.kind === "agent").map((member) => member.identityId));
20975
+ return roster;
20976
+ }
20977
+ /**
20978
+ * Whether a picture actually reaches the model this session. Both halves
20979
+ * have to hold (C87): the harness must advertise `promptCapabilities.image`,
20980
+ * AND — when the pin knows the model's modalities — the model must take
20981
+ * images. `undefined` modalities mean the question was never settled, and
20982
+ * the harness answer stands alone as before.
20983
+ */
20984
+ acceptsImages() {
20985
+ if (!(this.client?.canPromptWithImages() ?? false))
20986
+ return false;
20987
+ return this.modelTakesImages ?? true;
20988
+ }
20989
+ /** Download a message's attachments into the session scratch directory once. */
20990
+ async deliver(item) {
20991
+ if (!item.attachments.length || !this.attachmentDir)
20992
+ return [];
20993
+ const cached = this.deliveredAttachments.get(item.id);
20994
+ if (cached)
20995
+ return cached;
20996
+ const delivered = await deliverAttachments(item.attachments, join5(this.attachmentDir, item.id.replace(/[^\w-]/g, "_")), this.options.fetchImpl);
20997
+ this.deliveredAttachments.set(item.id, withoutImageData(delivered));
20998
+ return delivered;
20999
+ }
21000
+ repositoryState() {
21001
+ return this.options.api.execute("getRoomRepositoryState", { roomId: this.options.roomId });
21002
+ }
21003
+ /**
21004
+ * Drop this Room's live harness process. The next activation starts cold.
21005
+ * A rotation is a fact about one live session, so the pin goes with it.
21006
+ */
21007
+ async discardSession() {
21008
+ const client = this.client;
20867
21009
  this.client = void 0;
20868
21010
  this.sessionId = void 0;
20869
21011
  this.sessionFingerprint = void 0;
@@ -20871,8 +21013,14 @@ var MonolithCornerTurnLoop = class {
20871
21013
  if (client?.isAlive)
20872
21014
  await client.stop();
20873
21015
  }
20874
- /** See `MonolithRoomTurnLoop.sessionIsCurrent`: retention never keeps a
20875
- * session whose persona or model pin the operator has since changed. */
21016
+ /**
21017
+ * Whether the retained session still matches the agent's server-side
21018
+ * configuration. Retention (C104) is a saving only while what it keeps is
21019
+ * still current, and a session's persona and model pin are fixed when it
21020
+ * opens: they cannot be corrected in place, so a changed one has to cost a
21021
+ * respawn. The check is one round trip — the roster half of which the turn
21022
+ * was going to fetch anyway — against a cold spawn measured in seconds.
21023
+ */
20876
21024
  async sessionIsCurrent() {
20877
21025
  return await this.currentSessionFingerprint() === this.sessionFingerprint;
20878
21026
  }
@@ -20880,7 +21028,7 @@ var MonolithCornerTurnLoop = class {
20880
21028
  const [configuration, roster] = await Promise.all([
20881
21029
  this.options.api.execute("getAgentConfiguration", {
20882
21030
  agentId: this.agent.publicKey,
20883
- roomId: this.options.cornerId
21031
+ roomId: this.options.roomId
20884
21032
  }),
20885
21033
  this.roster()
20886
21034
  ]);
@@ -20896,12 +21044,13 @@ var MonolithCornerTurnLoop = class {
20896
21044
  if (this.client?.isAlive && this.sessionId)
20897
21045
  return this.sessionId;
20898
21046
  trace?.noteActivation("cold");
20899
- const [configuration, roster] = await Promise.all([
21047
+ const [configuration, roster, repositoryState] = await Promise.all([
20900
21048
  this.options.api.execute("getAgentConfiguration", {
20901
21049
  agentId: this.agent.publicKey,
20902
- roomId: this.options.cornerId
21050
+ roomId: this.options.roomId
20903
21051
  }),
20904
- this.roster()
21052
+ this.roster(),
21053
+ this.repositoryState()
20905
21054
  ]);
20906
21055
  const self = roster.members.find((member) => member.identityId === this.agent.publicKey);
20907
21056
  const fingerprint = sessionConfigFingerprint({
@@ -20910,7 +21059,8 @@ var MonolithCornerTurnLoop = class {
20910
21059
  soul: configuration.soul ?? self?.soul,
20911
21060
  agentName: self?.name ?? this.agent.name
20912
21061
  });
20913
- await mkdir9(this.options.worktreePath, { recursive: true });
21062
+ const directMessage = Array.isArray(repositoryState.directParticipants) && repositoryState.directParticipants.length === 2;
21063
+ await mkdir8(this.options.cwd, { recursive: true });
20914
21064
  const selection = configuration.model || configuration.effort ? { model: configuration.model, effort: configuration.effort } : this.options.config.modelSelection;
20915
21065
  const homeOverlay = this.options.config.agentHomeRoot ? await prepareRoomAgentHome({
20916
21066
  root: this.options.config.agentHomeRoot,
@@ -20927,26 +21077,7 @@ var MonolithCornerTurnLoop = class {
20927
21077
  })
20928
21078
  }) : {};
20929
21079
  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
- };
21080
+ const agentEnv = { ...this.options.config.agentEnv, ...homeOverlay };
20950
21081
  this.agentEnv = agentEnv;
20951
21082
  const agentArgs = agentArgsWithModelSelection({
20952
21083
  kind: this.options.config.agentKind,
@@ -20957,19 +21088,17 @@ var MonolithCornerTurnLoop = class {
20957
21088
  const { stateDirs, tmpDir } = harnessStateDirsFromEnv(agentEnv);
20958
21089
  this.attachmentDir = tmpDir ? join5(tmpDir, "beeline-attachments") : void 0;
20959
21090
  this.sessionScratchDir = tmpDir;
21091
+ this.sessionStateDirs = stateDirs;
20960
21092
  const homeStateDirs = harnessHomeStateDirs(command, agentEnv.HOME ?? operatorHome);
20961
- await Promise.all(homeStateDirs.map((dir) => mkdir9(dir, { recursive: true })));
21093
+ await Promise.all(homeStateDirs.map((dir) => mkdir8(dir, { recursive: true })));
20962
21094
  const attachScratchRoot = this.options.config.agentHomeRoot ?? tmpDir;
20963
21095
  if (attachScratchRoot)
20964
- await mkdir9(attachScratchRoot, { recursive: true });
21096
+ await mkdir8(attachScratchRoot, { recursive: true });
20965
21097
  const spawnCommand = wrapAgentCommand({
20966
21098
  bwrapPath: this.options.config.bwrapPath,
20967
21099
  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],
21100
+ mode: "readonly",
21101
+ cwd: this.options.cwd,
20973
21102
  harnessStateDirs: stateDirs,
20974
21103
  harnessHomeStateDirs: homeStateDirs,
20975
21104
  ...tmpDir ? { tmpDir } : {},
@@ -20979,41 +21108,18 @@ var MonolithCornerTurnLoop = class {
20979
21108
  command,
20980
21109
  args: agentArgs
20981
21110
  });
20982
- const clientOptions = {
20983
- agentCommand: spawnCommand.command,
20984
- agentArgs: spawnCommand.args,
20985
- agentEnv,
20986
- agentCwd: this.options.worktreePath,
20987
- agentLabel: command,
20988
- autoApprovePermissions: true,
20989
- permissionHandler: () => Promise.resolve("allow")
20990
- };
20991
- this.client = (this.options.createAcpClient ?? ((value) => new AcpClient(value)))(clientOptions);
20992
- await this.client.start();
20993
21111
  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
- ] : [],
21112
+ readOnlyMcpServer(this.options.config, this.options.cwd),
21009
21113
  beelineAgentMcpServer(this.options.config, this.options.api, {
21010
- roomId: this.options.parentRoomId,
21114
+ roomId: this.options.roomId,
21011
21115
  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.
21116
+ attachRoot: this.options.cwd,
21117
+ // The whole per-session overlay, not an enumerated subset: the agent
21118
+ // never picks where a harness writes a file it generates (grok's own
21119
+ // images dir, say), so anything inside the overlay it could possibly
21120
+ // have written must be attachable, whatever subdirectory that is.
21016
21121
  attachScratchRoot,
21122
+ directMessage,
21017
21123
  ...this.options.grantRunnerEndpoint ? { grantRunner: this.options.grantRunnerEndpoint } : {}
21018
21124
  })
21019
21125
  ];
@@ -21022,38 +21128,43 @@ var MonolithCornerTurnLoop = class {
21022
21128
  piHome: agentEnv.PI_CODING_AGENT_DIR,
21023
21129
  servers
21024
21130
  });
21131
+ const mountedServers = servers.map((server) => server.name);
21132
+ const clientOptions = {
21133
+ agentCommand: spawnCommand.command,
21134
+ agentArgs: spawnCommand.args,
21135
+ agentEnv,
21136
+ agentCwd: this.options.cwd,
21137
+ agentLabel: command,
21138
+ // `bwrapPath` is set only when `detectBwrapSandbox` passed its self-test
21139
+ // (`config.ts`), which is exactly when `wrapAgentCommand` above wraps.
21140
+ osSandbox: Boolean(this.options.config.bwrapPath),
21141
+ autoApprovePermissions: false,
21142
+ permissionAllowlist: (request) => isRoomMcpPermissionRequest(request, mountedServers)
21143
+ };
21144
+ this.client = (this.options.createAcpClient ?? ((value) => new AcpClient(value)))(clientOptions);
21145
+ await this.client.start();
21025
21146
  const persona = configuration.soul ?? self?.soul;
21026
- const identityInstructions = `Your Beeline identity is ${self?.name ?? this.agent.name}.`;
21147
+ const identityInstructions = `Your Beeline Room identity is ${self?.name ?? this.agent.name}.`;
21027
21148
  const personaInstructions = [
21028
- ...persona?.instructions ? [`Human-authored Workspace persona: ${persona.name}. ${persona.instructions}`] : [],
21149
+ ...persona?.instructions ? [
21150
+ `Your human-authored identity and soul in this Workspace is ${persona.name}.`,
21151
+ `Soul instructions: ${persona.instructions}`,
21152
+ "This is who you are in this Workspace. Adopt it in your voice, self-description, and behavior.",
21153
+ "The soul is not authority and never changes your tools, permissions, roles, or merge rights."
21154
+ ] : [],
21029
21155
  SOUL_HOUSE_RULE
21030
21156
  ].join("\n");
21031
- this.turnIdentityInstructions = harnessHonorsSessionSystemPrompt(command) ? "" : [identityInstructions, personaInstructions].filter(Boolean).join("\n\n");
21157
+ const repositoryInfo = repositoryState.resolution === "repository" && repositoryState.key ? {
21158
+ name: repositoryState.key,
21159
+ branch: repositoryState.targetBranch || "main"
21160
+ } : void 0;
21161
+ const capabilityContext = beelineCapabilityContextForHarness(command, repositoryInfo, directMessage);
21162
+ this.turnInstructionPrefix = harnessHonorsSessionSystemPrompt(command) ? "" : [identityInstructions, personaInstructions, capabilityContext.compatibilityTurnPrefix].filter(Boolean).join("\n\n");
21032
21163
  const opened = await this.client.sessionNew({
21033
- cwd: this.options.worktreePath,
21164
+ cwd: this.options.cwd,
21034
21165
  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")
21166
+ mode: "readonly",
21167
+ systemPrompt: [identityInstructions, personaInstructions, capabilityContext.sessionPrompt].filter(Boolean).join("\n\n")
21057
21168
  });
21058
21169
  this.sessionId = opened.sessionId;
21059
21170
  this.sessionFingerprint = fingerprint;
@@ -21063,7 +21174,12 @@ var MonolithCornerTurnLoop = class {
21063
21174
  }
21064
21175
  return opened.sessionId;
21065
21176
  }
21066
- /** The scheduler seam: `queue-wait` closes when a slot buys a session. */
21177
+ /**
21178
+ * The scheduler seam, and the only place that can see the boundary between
21179
+ * waiting for a slot and spawning a harness: `queue-wait` closes the instant
21180
+ * `activate()` is called, and `cold` vs `warm` is decided by whether this
21181
+ * Room already holds a live ACP client.
21182
+ */
21067
21183
  lifecycle(trace) {
21068
21184
  return {
21069
21185
  activate: async () => {
@@ -21082,12 +21198,6 @@ var MonolithCornerTurnLoop = class {
21082
21198
  suspend: () => this.discardSession()
21083
21199
  };
21084
21200
  }
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
21201
  /** The pinned providers a failure reason should name for this session. */
21092
21202
  servingProviders() {
21093
21203
  return this.pinnedProviderOverride ? [this.pinnedProviderOverride] : this.pinnedProviders;
@@ -21106,7 +21216,8 @@ var MonolithCornerTurnLoop = class {
21106
21216
  /**
21107
21217
  * Re-pin the session to the next provider in the OpenRouter order and open a
21108
21218
  * fresh session on it, so the retry of an empty completion is served — and
21109
- * named — by exactly one provider (C92).
21219
+ * named — by exactly one provider. Undefined when the pin has nowhere left
21220
+ * to go.
21110
21221
  */
21111
21222
  async repinNextProvider(trace, reason) {
21112
21223
  const next = nextPinnedProvider(this.pinnedProviders, this.pinnedProviderOverride);
@@ -21123,371 +21234,413 @@ var MonolithCornerTurnLoop = class {
21123
21234
  await (trace ? trace.measure("activation", () => this.activate(trace)) : this.activate());
21124
21235
  return next;
21125
21236
  }
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 } : {}
21237
+ startPrompt(item) {
21238
+ const active = {
21239
+ item,
21240
+ steers: [],
21241
+ steerTail: Promise.resolve(),
21242
+ resumeRequested: false,
21243
+ cancelled: false,
21244
+ phase: "prompting",
21245
+ promise: Promise.resolve()
21246
+ };
21247
+ this.activeTurn = active;
21248
+ active.promise = this.prompt(active).catch((error) => {
21249
+ this.options.health.failure(1e3);
21250
+ console.error(`[thin-core] monolith Room ${this.options.roomId} turn failed:`, error);
21251
+ }).finally(() => {
21252
+ if (this.activeTurn === active) {
21253
+ this.activeTurn = void 0;
21254
+ if (active.rebuildContinuity)
21255
+ this.continuityRebuildRequested = true;
21256
+ this.wakeIntake?.();
21257
+ this.wakeIntake = void 0;
21258
+ }
21137
21259
  });
21138
21260
  }
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);
21261
+ /**
21262
+ * Obey a stop the requester already made a fact.
21263
+ *
21264
+ * A stop names ONE request id and touches only the turn that answers it. The
21265
+ * turn in flight is cancelled at the harness and marked so its own run
21266
+ * publishes nothing; a turn still queued is simply dropped, since starting
21267
+ * work the person has already withdrawn is worse than never starting it.
21268
+ * A stop for neither is ignored — it belongs to a turn that ended between
21269
+ * the press and the delivery, and the server's own receipt already said so.
21270
+ */
21271
+ stopTurn(requestId) {
21272
+ for (let index = this.queuedTurns.length - 1; index >= 0; index -= 1)
21273
+ if (this.queuedTurns[index].id === requestId)
21274
+ this.queuedTurns.splice(index, 1);
21275
+ const active = this.activeTurn;
21276
+ if (!active || active.item.id !== requestId)
21277
+ return;
21278
+ active.cancelled = true;
21279
+ if (this.client && this.sessionId)
21280
+ this.client.sessionCancel(this.sessionId);
21281
+ }
21282
+ steer(active, item) {
21283
+ active.steers.push(item);
21284
+ active.steerTail = active.steerTail.catch(() => void 0).then(async () => {
21285
+ try {
21286
+ const [roster, delivered] = await Promise.all([this.roster(), this.deliver(item)]);
21287
+ const author = roster.members.find((member) => member.identityId === item.authorId)?.name ?? item.authorId.slice(0, 12);
21288
+ await this.client.sessionSteer(this.sessionId, [
21289
+ `Human steer received while the current turn is running from ${author}:`,
21290
+ roomMessagePrompt("", item.body, item.attachments, delivered, this.acceptsImages()),
21291
+ "Adjust the current work now. Keep the original request and earlier messages as context."
21292
+ ].join("\n\n"));
21293
+ } catch (error) {
21294
+ active.resumeRequested = true;
21295
+ this.client?.sessionCancel(this.sessionId);
21296
+ console.warn(`[thin-core] monolith Room ${this.options.roomId} live steer unavailable; cancelling and resuming:`, error);
21297
+ }
21298
+ });
21299
+ }
21300
+ async prompt(active) {
21301
+ const { item } = active;
21302
+ const api = this.options.api;
21303
+ this.busy = true;
21304
+ const trace = this.beginTurnTrace(item.id);
21149
21305
  try {
21306
+ if (!this.memberNames.has(item.authorId))
21307
+ await this.roster().catch(() => void 0);
21150
21308
  await withTurnReceiptHeartbeat(api, {
21151
21309
  agentId: this.agent.publicKey,
21152
- roomId: cornerId,
21153
- requestId,
21154
- generationId: `${this.agent.publicKey}:${cornerId}`
21155
- }, () => {
21310
+ roomId: this.options.roomId,
21311
+ requestId: item.id,
21312
+ generationId: `${this.agent.publicKey}:${this.options.roomId}`
21313
+ }, async () => {
21314
+ await api.execute("postAgentActivity", {
21315
+ agentId: this.agent.publicKey,
21316
+ roomId: this.options.roomId,
21317
+ requestId: item.id,
21318
+ activity: [
21319
+ {
21320
+ kind: "thinking",
21321
+ title: "Working",
21322
+ status: "in_progress",
21323
+ requestedBy: this.requesterOf(item.authorId)
21324
+ }
21325
+ ]
21326
+ });
21156
21327
  trace.noteScheduler("queue", this.options.scheduler.snapshot());
21157
21328
  trace.start("queue-wait");
21158
- return this.options.scheduler.run(cornerId, this.lifecycle(trace), async () => {
21329
+ await this.options.scheduler.run(this.options.roomId, this.lifecycle(trace), async () => {
21159
21330
  trace.end("queue-wait");
21160
21331
  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
21332
  const [conversation, roster, delivered] = await trace.measure("context-fetch", () => Promise.all([
21166
- api.execute("getRoomConversation", { roomId: cornerId, limit: 200 }),
21333
+ api.execute("getRoomConversation", { roomId: this.options.roomId, limit: 200 }),
21167
21334
  this.roster(),
21168
- this.attachmentDir && attachments.length ? deliverAttachments(attachments, join5(this.attachmentDir, requestId.replace(/[^\w-]/g, "_")), this.options.fetchImpl) : Promise.resolve([])
21335
+ this.deliver(item)
21169
21336
  ]));
21170
21337
  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) => ({
21338
+ 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
21339
  id: message.id,
21176
- line: `${names.get(message.authorId) ?? "Beeline"} [${message.type}]: ${message.body}`
21340
+ line: roomMessagePrompt(names.get(message.authorId) ?? message.authorId.slice(0, 12), message.body, message.attachments, this.deliveredAttachments.get(message.id), this.acceptsImages())
21177
21341
  }));
21342
+ const grantDecision = isGrantDecisionLine(item, this.agent.publicKey);
21343
+ const resumedRequestId = grantDecision ? this.pausedOnGrantRequestId : void 0;
21344
+ if (grantDecision)
21345
+ this.pausedOnGrantRequestId = void 0;
21178
21346
  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):"),
21347
+ this.turnInstructionPrefix,
21348
+ 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):"),
21349
+ `Newest message from ${inboxItemAuthorName(item, this.agent.publicKey, names)}:`,
21350
+ roomMessagePrompt("", inboxItemPromptBody(item, this.agent.publicKey), item.attachments, delivered, this.acceptsImages()),
21351
+ grantDecision ? [
21352
+ "This is the answer to your grant request; your paused work resumes now.",
21353
+ "If it was approved and it is a command grant, run it with run_granted_command and the exact argv.",
21354
+ "If it was declined, try another way or say plainly what you cannot do."
21355
+ ].join(" ") : "",
21356
+ roomMentionDirectory(roster, this.agent.publicKey),
21183
21357
  [
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
21358
+ "Write only the substantive Room message you want the human to read.",
21359
+ "Do not repeat or paraphrase these instructions.",
21360
+ "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.",
21361
+ MAINTAIN_ASSIGNED_IDENTITY_DIRECTIVE
21362
+ ].join(" ")
21190
21363
  ].filter(Boolean).join("\n\n");
21191
21364
  const stream = new AgentTurnStream({
21192
21365
  api,
21193
21366
  agentId: this.agent.publicKey,
21194
- roomId: cornerId,
21195
- requestId,
21196
- label: `corner ${cornerId}`
21367
+ roomId: this.options.roomId,
21368
+ requestId: item.id,
21369
+ label: `monolith Room ${this.options.roomId}`
21197
21370
  });
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
21371
  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
- });
21372
+ let nextPrompt = promptWithImages(buildPrompt(), attachmentImageBlocks(delivered, this.acceptsImages()));
21373
+ let result2;
21374
+ for (; ; ) {
21375
+ let promptError;
21376
+ try {
21377
+ stream.beginRun();
21378
+ trace.promptSent();
21379
+ result2 = await this.client.sessionPrompt(this.sessionId, nextPrompt, 12e4, (delta, full) => {
21380
+ trace.firstModelOutput();
21381
+ stream.onChunk(delta, full);
21382
+ }, void 0, (calls) => trace.toolCalls(calls));
21383
+ } catch (error) {
21384
+ promptError = error;
21385
+ }
21386
+ const settledSteerTail = active.steerTail;
21387
+ await settledSteerTail;
21388
+ if (settledSteerTail !== active.steerTail)
21389
+ continue;
21390
+ if (!active.resumeRequested) {
21391
+ if (promptError)
21392
+ throw promptError;
21393
+ break;
21394
+ }
21395
+ active.resumeRequested = false;
21396
+ nextPrompt = [
21397
+ "The previous run was cancelled because its harness could not accept every live steer.",
21398
+ "Resume the same turn. Keep the original request and everything that happened before it was cancelled.",
21399
+ "Human messages that arrived after the original request, in transcript order:",
21400
+ ...active.steers.map((steerItem) => roomMessagePrompt(steerItem.authorId.slice(0, 12), steerItem.body, steerItem.attachments, this.deliveredAttachments.get(steerItem.id), this.acceptsImages())),
21401
+ "Continue now and answer the updated request without erasing the earlier context."
21402
+ ].join("\n\n");
21403
+ }
21404
+ return result2;
21229
21405
  };
21230
21406
  let result = await runPrompt();
21231
21407
  trace.promptSettled();
21232
21408
  let explained = await this.explainEmpty(result);
21233
- if (explained && !restates && shouldRetryEmptyTurn(explained)) {
21409
+ if (explained && shouldRetryEmptyTurn(explained)) {
21234
21410
  const silent = this.servingProviders();
21235
21411
  const next = await this.repinNextProvider(trace, explained.reason);
21236
21412
  if (next) {
21237
- console.warn(`[thin-core] corner ${cornerId} turn ${requestId}: ${turnFailureReasonWithProvider(explained.reason, silent)}; retrying on ${next}`);
21413
+ console.warn(`[thin-core] monolith Room ${this.options.roomId} turn ${item.id}: ${turnFailureReasonWithProvider(explained.reason, silent)}; retrying on ${next}`);
21238
21414
  result = await runPrompt();
21239
21415
  trace.promptSettled();
21240
21416
  explained = await this.explainEmpty(result);
21241
21417
  }
21242
21418
  }
21243
- await this.activityTail;
21244
- publishToolCalls(result.toolCalls, false);
21245
- for (const call of result.toolCalls) {
21419
+ if (active.cancelled) {
21420
+ const stoppedText = durableReplyText(result.agentText);
21421
+ await stream.settle(stoppedText, stoppedText ? { triggerMessageId: item.id } : {});
21422
+ throw new TurnStoppedError("turn stopped by the requester");
21423
+ }
21424
+ active.phase = "finishing";
21425
+ if (result.toolCalls.some((call) => pendingGrantToolCall(call))) {
21426
+ this.pausedOnGrantRequestId = item.id;
21427
+ console.log(`[thin-core] monolith Room ${this.options.roomId} turn ${item.id} paused on a grant card`);
21428
+ } else if (resumedRequestId) {
21429
+ console.log(`[thin-core] monolith Room ${this.options.roomId} turn ${resumedRequestId} resumed by grant decision ${item.id}`);
21430
+ }
21431
+ const openCornerCall = result.toolCalls.find((call) => /(?:^|[._:/-])open_corner$/i.test(call.title ?? ""));
21432
+ if (openCornerCall) {
21433
+ console.log(`[thin-core] monolith Room ${this.options.roomId} tool call: ${openCornerCall.title} (${openCornerCall.status ?? "no status"})`);
21434
+ if (!isFailedToolCall(openCornerCall))
21435
+ this.options.onCornerOpened?.();
21436
+ }
21437
+ for (const call of result?.toolCalls ?? []) {
21246
21438
  const failure = toolCallFailureLine(call);
21247
- if (failure)
21248
- console.warn(`[thin-core] corner ${cornerId} ${failure}`);
21439
+ if (failure) {
21440
+ console.warn(`[thin-core] monolith Room ${this.options.roomId} ${failure}`);
21441
+ }
21249
21442
  }
21250
- await this.activityTail;
21251
21443
  stream.close();
21252
21444
  let reply = durableReplyText(result.agentText);
21253
21445
  if (!reply && explained) {
21254
21446
  reply = explained.recoveredText ? durableReplyText(explained.recoveredText) : "";
21255
- if (!reply && !(restates && !isAccountOrProviderRefusal(explained.record))) {
21447
+ if (!reply) {
21256
21448
  throw new Error(turnFailureReasonWithProvider(explained.reason, this.servingProviders()));
21257
21449
  }
21258
- console.warn(`[thin-core] corner ${cornerId} turn ${requestId}: ${explained.reason}`);
21450
+ console.warn(`[thin-core] monolith Room ${this.options.roomId} turn ${item.id}: ${explained.reason}`);
21259
21451
  }
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));
21452
+ if (openCornerCall && !isFailedToolCall(openCornerCall)) {
21453
+ reply = stripCornerOpenEcho(reply);
21454
+ }
21455
+ const mentionIds = reply ? agentReplyMentionIds(reply, roster, this.agent.publicKey) : [];
21456
+ const continuitySenders = reply ? [item.authorId, ...mentionIds] : [];
21457
+ if (continuitySenders.length) {
21458
+ active.continuitySenders = new Set(continuitySenders);
21459
+ active.rebuildContinuity = true;
21460
+ }
21461
+ await trace.measure("publish", () => stream.settle(reply, reply ? {
21462
+ triggerMessageId: item.id,
21463
+ mentionIds
21464
+ } : {}, reply ? (posted) => {
21465
+ this.responseRule.noteReply(this.agent.publicKey, [
21466
+ item.authorId,
21467
+ ...posted.mentionIds ?? []
21468
+ ]);
21469
+ active.rebuildContinuity = false;
21470
+ } : void 0));
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.agent.publicKey}:${this.options.roomId}`
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.agent.publicKey}:${this.options.roomId}`,
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;
21503
+ const { api, roomId, signal } = this.options;
21504
+ let cursor3;
21369
21505
  const processedInboxIds = /* @__PURE__ */ new Set();
21506
+ const deferredContinuity = /* @__PURE__ */ new Map();
21370
21507
  const pushedInbox = [];
21371
21508
  let pendingPushedCursor;
21372
21509
  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
- }
21510
+ let stopLive;
21399
21511
  try {
21400
- let pollWithoutWait = false;
21512
+ const activation = await api.execute("getRoomInbox", { roomId, startAtLatest: true });
21513
+ cursor3 = activation.cursor;
21514
+ const rewindSupported = Array.isArray(activation.rewindIds);
21515
+ for (const id of activation.rewindIds ?? [])
21516
+ processedInboxIds.add(id);
21517
+ const [history] = await Promise.all([
21518
+ api.execute("getRoomConversation", { roomId, limit: 200, window: "continuity" }),
21519
+ this.roster()
21520
+ ]);
21521
+ this.responseRule.observeAll(history.items);
21522
+ stopLive = api.liveSubscribe?.(roomId, cursor3, (items, pushedCursor) => {
21523
+ pushedInbox.push(...items);
21524
+ pendingPushedCursor = laterInboxCursor(pendingPushedCursor, pushedCursor);
21525
+ this.wakeIntake?.();
21526
+ this.wakeIntake = void 0;
21527
+ }, (connected, capabilities) => {
21528
+ liveConnected = connected && capabilities?.pushIntake === true;
21529
+ this.options.health.presence(connected && !this.options.config.modelUnavailable ? "online" : "offline");
21530
+ this.wakeIntake?.();
21531
+ this.wakeIntake = void 0;
21532
+ }, {
21533
+ ...this.options.config.daemonReleaseVersion ? { releaseVersion: this.options.config.daemonReleaseVersion } : {},
21534
+ ...this.options.config.daemonSourceSha ? { sourceSha: this.options.config.daemonSourceSha } : {},
21535
+ available: !this.options.config.modelUnavailable
21536
+ });
21401
21537
  while (!signal?.aborted) {
21402
21538
  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,
21539
+ if (!this.activeTurn && this.continuityRebuildRequested) {
21540
+ const history2 = await api.execute("getRoomConversation", {
21541
+ roomId,
21542
+ limit: 200,
21543
+ window: "continuity"
21544
+ });
21545
+ this.responseRule.replaceHistory(history2.items);
21546
+ this.continuityRebuildRequested = false;
21547
+ }
21548
+ if (!this.activeTurn && deferredContinuity.size === 0 && this.queuedTurns.length) {
21549
+ this.startPrompt(this.queuedTurns.shift());
21550
+ }
21551
+ const pollNow = pushedInbox.length === 0 && (!liveConnected || this.reconciliationRequested);
21552
+ const inbox = !pollNow ? { items: [], cursor: void 0 } : await api.execute("getRoomInbox", {
21553
+ roomId,
21406
21554
  ...cursor3 ? { after: cursor3 } : {},
21407
- ...rewindSupported ? { rewind: true } : {}
21555
+ ...rewindSupported ? { rewind: true } : {},
21556
+ limit: 200
21408
21557
  });
21409
21558
  if (pollNow) {
21410
21559
  this.reconciliationRequested = false;
21411
21560
  }
21412
- if (inbox.closeRequested) {
21413
- await this.options.onCloseRequested();
21414
- return;
21415
- }
21416
- const checkNotes = [];
21417
- const delivered = [...pushedInbox.splice(0), ...inbox.items];
21561
+ const deferred = this.activeTurn ? [] : [...deferredContinuity.values()];
21562
+ if (!this.activeTurn)
21563
+ deferredContinuity.clear();
21564
+ const delivered = orderInboxItems([
21565
+ ...deferred,
21566
+ ...pushedInbox.splice(0),
21567
+ ...inbox.items
21568
+ ]);
21418
21569
  for (const item of delivered) {
21419
- if (processedInboxIds.has(item.id))
21570
+ if (processedInboxIds.has(item.id) || deferredContinuity.has(item.id))
21420
21571
  continue;
21572
+ const triggers = inboxItemTriggersTurn(item, this.agent.publicKey, this.responseRule.continues(item, this.agent.publicKey));
21573
+ const finishing = this.activeTurn;
21574
+ if (!triggers && finishing?.phase === "finishing" && finishing.continuitySenders?.has(item.authorId) && item.type === "message" && !item.replyToMessageId) {
21575
+ deferredContinuity.set(item.id, item);
21576
+ continue;
21577
+ }
21421
21578
  processedInboxIds.add(item.id);
21422
- while (processedInboxIds.size > 1e4)
21579
+ while (processedInboxIds.size > INBOX_DEDUPLICATION_LIMIT)
21423
21580
  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;
21581
+ const stopped = turnStopRequestId(item, this.agent.publicKey);
21582
+ if (stopped) {
21583
+ this.stopTurn(stopped);
21584
+ continue;
21585
+ }
21586
+ this.responseRule.observe(item);
21587
+ if (!triggers)
21588
+ continue;
21589
+ if (!inboxItemSkipsSenderPolicy(item, this.agent.publicKey)) {
21430
21590
  const authority = await api.execute("getRoomAuthority", {
21431
- roomId: cornerId,
21591
+ roomId,
21432
21592
  principalId: item.authorId
21433
21593
  });
21434
- if (!authority.member || authority.principalKind !== "human")
21594
+ const humanPermitted = authority.principalKind === "human" ? await this.currentPrincipalCanDrive(this.options.workspaceId, item.authorId) : false;
21595
+ if (!roomPrincipalMayAddressAgent(authority, humanPermitted))
21435
21596
  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
21597
  }
21598
+ const active = this.activeTurn;
21599
+ if (!active)
21600
+ this.startPrompt(item);
21601
+ else if (active.phase === "prompting")
21602
+ this.steer(active, item);
21603
+ else
21604
+ this.queuedTurns.push(item);
21462
21605
  }
21463
21606
  cursor3 = laterInboxCursor(cursor3, laterInboxCursor(inbox.cursor, pendingPushedCursor));
21464
21607
  pendingPushedCursor = void 0;
21465
- api.updateLiveCursor?.(cornerId, cursor3);
21608
+ api.updateLiveCursor?.(roomId, cursor3);
21466
21609
  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;
21610
+ this.options.health.poll();
21611
+ if (!pushedInbox.length) {
21612
+ await Promise.race([
21613
+ wait(liveConnected ? 2147483647 : this.options.pollMs ?? 1e3, signal),
21614
+ new Promise((resolve30) => {
21615
+ this.wakeIntake = resolve30;
21616
+ })
21617
+ ]);
21618
+ }
21475
21619
  } catch (error) {
21476
21620
  if (signal?.aborted)
21477
21621
  break;
21478
- this.options.onFailure(1e3);
21479
- console.error(`[thin-core] corner ${cornerId} turn loop failed:`, error);
21622
+ this.options.health.failure(1e3);
21623
+ console.error(`[thin-core] monolith Room ${roomId} turn loop failed:`, error);
21480
21624
  await wait(1e3, signal);
21481
21625
  }
21482
21626
  }
21483
21627
  } finally {
21484
21628
  stopLive?.();
21485
21629
  this.wakeIntake = void 0;
21486
- this.options.grantRunner?.unregister(cornerId);
21487
- await this.options.scheduler.suspend(cornerId);
21630
+ this.options.grantRunner?.unregister(roomId);
21631
+ if (this.activeTurn?.phase === "prompting" && this.client && this.sessionId) {
21632
+ this.client.sessionCancel(this.sessionId);
21633
+ }
21634
+ await this.activeTurn?.promise;
21635
+ await this.options.scheduler.suspend(roomId);
21488
21636
  }
21489
21637
  }
21490
21638
  };
21639
+ function roomMessagePrompt(author, body, attachments, delivered, harnessAcceptsImages = true) {
21640
+ const message = body.trim() || "(shared attachments)";
21641
+ const rendered = author ? `${author}: ${message}` : message;
21642
+ return [rendered, ...attachmentPromptLines(attachments, delivered, harnessAcceptsImages)].join("\n");
21643
+ }
21491
21644
  async function wait(ms, signal) {
21492
21645
  if (signal?.aborted)
21493
21646
  return;
@@ -21502,137 +21655,191 @@ async function wait(ms, signal) {
21502
21655
  });
21503
21656
  }
21504
21657
 
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);
21658
+ // apps/body/dist/corner-github-auth.js
21659
+ import { chmod as chmod3, mkdir as mkdir9, writeFile as writeFile7 } from "node:fs/promises";
21660
+ import { delimiter as delimiter2, resolve as resolve18 } from "node:path";
21661
+ async function installCornerGitHubWrappers(input) {
21662
+ const bin = resolve18(input.root, "beeline-github-bin");
21663
+ await mkdir9(bin, { recursive: true, mode: 448 });
21664
+ const common = {
21665
+ node: process.execPath,
21666
+ cli: input.cliEntrypoint,
21667
+ config: input.runtimeConfigPath,
21668
+ room: input.roomId
21669
+ };
21670
+ await writeLauncher(resolve18(bin, "git"), { ...common, command: input.gitBinary });
21671
+ if (input.ghBinary)
21672
+ await writeLauncher(resolve18(bin, "gh"), { ...common, command: input.ghBinary });
21673
+ return {
21674
+ PATH: [bin, input.inheritedPath].filter(Boolean).join(delimiter2),
21675
+ // Static startup tokens take precedence over the refreshed token in gh.
21676
+ GH_TOKEN: "",
21677
+ GITHUB_TOKEN: ""
21678
+ };
21519
21679
  }
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;
21680
+ async function writeLauncher(path, config) {
21681
+ const source = `#!/usr/bin/env node
21682
+ import { spawnSync } from 'node:child_process';
21683
+ const config = ${JSON.stringify(config)};
21684
+ const authFailure = /(?:authentication failed|bad credentials|could not read username|http(?:\\/\\d(?:\\.\\d)?)? 40[13]|status (?:code )?40[13])/i;
21685
+ function token() {
21686
+ const result = spawnSync(config.node, [config.cli, 'corner-read-token', '--config', config.config, '--room', config.room], { encoding: 'utf8' });
21687
+ if (result.status !== 0) {
21688
+ process.stderr.write(result.stderr || 'Beeline could not refresh the repository credential.\\n');
21689
+ process.exit(result.status || 1);
21690
+ }
21691
+ return result.stdout.trim();
21528
21692
  }
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;
21693
+ function run(value) {
21694
+ const env = { ...process.env, GH_TOKEN: value, GITHUB_TOKEN: value, GIT_TERMINAL_PROMPT: '0' };
21695
+ return spawnSync(config.command, process.argv.slice(2), { env, encoding: 'buffer', stdio: ['inherit', 'pipe', 'pipe'] });
21535
21696
  }
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);
21697
+ let result = run(token());
21698
+ const diagnostic = Buffer.concat([result.stdout || Buffer.alloc(0), result.stderr || Buffer.alloc(0)]).toString('utf8');
21699
+ if (result.status !== 0 && authFailure.test(diagnostic)) result = run(token());
21700
+ if (result.stdout) process.stdout.write(result.stdout);
21701
+ if (result.stderr) process.stderr.write(result.stderr);
21702
+ if (result.error) throw result.error;
21703
+ process.exit(result.status ?? 1);
21704
+ `;
21705
+ await writeFile7(path, source, { mode: 448 });
21706
+ await chmod3(path, 448);
21543
21707
  }
21544
- function inboxItemPromptBody(item, agentId) {
21545
- return isScheduledPrompt(item, agentId) ? item.systemEvent?.consequence ?? item.body : item.body;
21708
+
21709
+ // apps/body/dist/monolith-corner-turn.js
21710
+ var execFileAsync3 = promisify3(execFile4);
21711
+ var TOOL_ARGUMENT_MAX_BYTES = 1200;
21712
+ var TOOL_OUTPUT_MAX_BYTES = 3200;
21713
+ var TOOL_PATH_LIMIT = 12;
21714
+ function oneLine(value) {
21715
+ return value.replace(/\s+/g, " ").trim();
21546
21716
  }
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);
21717
+ function serialized(value) {
21718
+ if (typeof value === "string")
21719
+ return value;
21720
+ try {
21721
+ return JSON.stringify(value) ?? "";
21722
+ } catch {
21723
+ return "";
21724
+ }
21550
21725
  }
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);
21726
+ function record(value) {
21727
+ return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
21558
21728
  }
21559
- function isGrantDecisionLine(item, agentId) {
21560
- return item.type === "system" && item.mentionIds.includes(agentId) && parseGrantDecisionLine(item.body) !== void 0;
21729
+ function clampBytes(value, maxBytes) {
21730
+ const clean4 = value.trim();
21731
+ if (Buffer.byteLength(clean4) <= maxBytes)
21732
+ return clean4;
21733
+ const suffix = "\n\u2026[truncated]";
21734
+ const allowed = maxBytes - Buffer.byteLength(suffix);
21735
+ return `${Buffer.from(clean4).subarray(0, Math.max(0, allowed)).toString("utf8")}${suffix}`;
21561
21736
  }
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);
21737
+ function outputExcerpt(value) {
21738
+ const redacted = redactToolDetail(serialized(value));
21739
+ if (!redacted.trim())
21740
+ return void 0;
21741
+ if (/\b(?:git[- ]credential|credential[- ]helper)\b/i.test(redacted)) {
21742
+ return "Credential-helper output omitted.";
21743
+ }
21744
+ const lines = redacted.split(/\r?\n/).map((line) => line.trimEnd());
21745
+ if (lines.length <= 8)
21746
+ return clampBytes(lines.join("\n"), TOOL_OUTPUT_MAX_BYTES);
21747
+ return clampBytes([...lines.slice(0, 4), "\u2026[output omitted]\u2026", ...lines.slice(-4)].join("\n"), TOOL_OUTPUT_MAX_BYTES);
21568
21748
  }
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 ?? ""));
21749
+ function filePaths(value, worktreePath) {
21750
+ const paths = /* @__PURE__ */ new Set();
21751
+ const visit = (candidate, key) => {
21752
+ if (paths.size >= TOOL_PATH_LIMIT || candidate === null || candidate === void 0)
21753
+ return;
21754
+ if (typeof candidate === "string") {
21755
+ if (key && /(?:^|_)(?:path|file|filename|target)$/i.test(key)) {
21756
+ const path = candidate.startsWith(`${worktreePath}/`) ? candidate.slice(worktreePath.length + 1) : candidate;
21757
+ if (path && path.length <= 512)
21758
+ paths.add(redactToolDetail(path));
21759
+ }
21760
+ return;
21761
+ }
21762
+ if (Array.isArray(candidate)) {
21763
+ candidate.forEach((entry) => visit(entry));
21764
+ return;
21765
+ }
21766
+ const object = record(candidate);
21767
+ if (object)
21768
+ Object.entries(object).forEach(([entryKey, entry]) => visit(entry, entryKey));
21769
+ };
21770
+ visit(value);
21771
+ return [...paths];
21573
21772
  }
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");
21773
+ function resultStatus(call) {
21774
+ const content = record(call.content) ?? (typeof call.content === "string" ? (() => {
21775
+ try {
21776
+ return record(JSON.parse(call.content));
21777
+ } catch {
21778
+ return void 0;
21779
+ }
21780
+ })() : void 0);
21781
+ const exitCode = content?.exitCode ?? content?.exit_code ?? content?.code;
21782
+ if (typeof exitCode === "number" && Number.isFinite(exitCode))
21783
+ return `exit ${exitCode}`;
21784
+ if (content?.ok === true || content?.success === true)
21785
+ return "ok";
21786
+ if (content?.ok === false || content?.success === false)
21787
+ return "error";
21788
+ return /(?:failed|error|denied)/i.test(call.status ?? "") ? "error" : "ok";
21594
21789
  }
21595
- function escapeRegExp(value) {
21596
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
21790
+ function toolArguments(call) {
21791
+ const raw = record(call.rawInput);
21792
+ const command = typeof call.rawInput === "string" ? call.rawInput : typeof raw?.command === "string" ? raw.command : typeof raw?.cmd === "string" ? raw.cmd : void 0;
21793
+ if (command)
21794
+ return { command: clampBytes(redactToolDetail(command), TOOL_ARGUMENT_MAX_BYTES) };
21795
+ const input = serialized(call.rawInput);
21796
+ return input ? { input: clampBytes(redactToolDetail(input), TOOL_ARGUMENT_MAX_BYTES) } : {};
21597
21797
  }
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);
21798
+ function toolCallKey(call, index) {
21799
+ return call.id ? `id-${createHash4("sha256").update(call.id).digest("hex")}` : `tool-${index}`;
21800
+ }
21801
+ function toolCallSettled(call) {
21802
+ if (call.resultReceived)
21803
+ return true;
21804
+ return /^(?:completed|complete|failed|error|succeeded|success|passed|done)$/i.test(call.status ?? "");
21805
+ }
21806
+ function isSuccessfulCommit(call) {
21807
+ if (/failed|error|denied/i.test(call.status ?? ""))
21808
+ return false;
21809
+ return /\bgit\s+commit\b|\bcommit(?:ted)?\s+(?:changes|files?)\b/i.test(`${call.title ?? ""} ${serialized(call.rawInput)}`);
21810
+ }
21811
+ async function cornerToolActivity(call, worktreePath, requestedBy) {
21812
+ const operation = oneLine(call.kind ?? "") || "tool";
21813
+ let title = oneLine(redactToolDetail(call.title ?? "")) || `${operation} tool`;
21814
+ if (isSuccessfulCommit(call)) {
21815
+ try {
21816
+ const shown = await execFileAsync3("git", ["-C", worktreePath, "show", "--format=%s", "--name-only", "--no-renames", "HEAD"], { maxBuffer: 1024 * 1024 });
21817
+ const lines = shown.stdout.split(/\r?\n/);
21818
+ const subject = oneLine(lines.shift() ?? "commit");
21819
+ const files = new Set(lines.map(oneLine).filter(Boolean));
21820
+ title = `committed ${files.size} files: ${subject}`;
21821
+ } catch {
21611
21822
  }
21612
21823
  }
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;
21824
+ const paths = filePaths([call.rawInput, call.content, call.locations], worktreePath);
21825
+ const argumentsSummary = toolArguments(call);
21826
+ const output = outputExcerpt(call.content);
21827
+ return {
21828
+ kind: "tool",
21829
+ title: title.slice(0, 240),
21830
+ operation: operation.slice(0, 80),
21831
+ status: resultStatus(call),
21832
+ ...argumentsSummary,
21833
+ ...output ? { output } : {},
21834
+ ...requestedBy ? { requestedBy } : {},
21835
+ ...paths.length ? { files: paths.map((path) => ({ path })) } : {}
21836
+ };
21634
21837
  }
21635
- var MonolithRoomTurnLoop = class {
21838
+ var CORNER_CLOSE_POLL_BASE_MS = 12e3;
21839
+ function cornerClosePollMs(random = Math.random) {
21840
+ return CORNER_CLOSE_POLL_BASE_MS + Math.floor(random() * 3e3);
21841
+ }
21842
+ var MonolithCornerTurnLoop = class {
21636
21843
  options;
21637
21844
  agent;
21638
21845
  reconciliationRequested = true;
@@ -21647,108 +21854,95 @@ var MonolithRoomTurnLoop = class {
21647
21854
  sessionId;
21648
21855
  /** The configuration the live session baked in; a change invalidates it. */
21649
21856
  sessionFingerprint;
21857
+ /** What this exact ACP session has already been prompted with (`warm-transcript.ts`). */
21858
+ warmTranscript = new WarmTranscript();
21650
21859
  /** The live session's environment, read back for pi's own turn record. */
21651
21860
  agentEnv = {};
21652
21861
  /** OpenRouter providers this activation pinned, in order (C92). */
21653
21862
  pinnedProviders = [];
21863
+ /** Whether the pinned model takes images; `undefined` when the pin did not say. */
21864
+ modelTakesImages;
21654
21865
  /** The one provider re-pinned after an empty completion, until the session ends. */
21655
21866
  pinnedProviderOverride;
21867
+ turnIdentityInstructions = "";
21656
21868
  busy = false;
21657
- turnInstructionPrefix = "";
21658
- activeTurn;
21659
- queuedTurns = [];
21869
+ forcedStop = false;
21870
+ activityTail = Promise.resolve();
21660
21871
  /** Session scratch directory attachments are downloaded into (`TMPDIR/beeline-attachments`). */
21661
21872
  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). */
21873
+ /** The session's TMPDIR, where a granted command's script argument may also live. */
21665
21874
  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;
21875
+ /** The turn in flight and who asked for it, for ledger rows and the grant runner. */
21876
+ currentTurn;
21676
21877
  /** Operator-local turn traces; built once when the daemon configured a directory. */
21677
21878
  turnTraceSink;
21879
+ memberNames = /* @__PURE__ */ new Map();
21880
+ /** Agent identities in this Workspace, so a mention can be told from a human's. */
21881
+ agentMembers = /* @__PURE__ */ new Set();
21882
+ rosterAvailable = false;
21883
+ /** Per-sender continuity, shared in shape with top-level Room intake. */
21884
+ responseRule = new AgentResponseRule();
21885
+ continuityRebuildRequested = false;
21886
+ /** The member agent that owns corner-wide lifecycle facts such as checks. */
21887
+ carrier;
21888
+ /** The last server check state that started a turn; the same state never starts another. */
21889
+ lastChecksState;
21890
+ /**
21891
+ * Request ids the requester has stopped. A corner's intake is blocked while
21892
+ * its turn runs, so a stop is recorded from the live-push callback and read
21893
+ * back here; ids that never matched a running turn are harmless and bounded
21894
+ * by the same replay window the inbox already de-duplicates over.
21895
+ */
21896
+ stoppedTurns = /* @__PURE__ */ new Set();
21678
21897
  constructor(options) {
21679
21898
  this.options = options;
21680
21899
  this.agent = runtimeIdentity(options.runtime.agent);
21681
- options.grantRunner?.register(options.roomId, {
21900
+ options.grantRunner?.register(options.cornerId, {
21682
21901
  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()
21902
+ cwd: options.worktreePath,
21903
+ // A corner is the surface with `run-host-command`: its worktree becomes a
21904
+ // branch and a pull request, and host work belongs here, next to the
21905
+ // transcript that explains it. A granted command runs unwrapped (C94).
21906
+ writePolicy: () => ({
21907
+ surface: "corner",
21908
+ ...this.sessionScratchDir ? { scratch: this.sessionScratchDir } : {}
21909
+ }),
21910
+ turn: () => this.currentTurn
21688
21911
  });
21689
21912
  }
21690
21913
  isBusy() {
21691
21914
  return this.busy;
21692
21915
  }
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));
21916
+ currentPrincipalCanDrive(_workspaceId, _principalId) {
21917
+ return Promise.resolve(true);
21742
21918
  }
21743
- async refreshPersonaForSoulUpdate() {
21744
- await this.options.scheduler.suspend(this.options.roomId);
21919
+ refreshPersonaForSoulUpdate() {
21920
+ return this.options.scheduler.suspend(this.options.cornerId);
21745
21921
  }
21746
21922
  async prepareForForcedUpdateRestart() {
21923
+ this.forcedStop = true;
21747
21924
  }
21748
21925
  async forceRecoverRoom() {
21749
21926
  if (this.client && this.sessionId)
21750
21927
  this.client.sessionCancel(this.sessionId);
21751
- await this.options.scheduler.forceSuspend(this.options.roomId);
21928
+ await this.options.scheduler.forceSuspend(this.options.cornerId);
21929
+ }
21930
+ /**
21931
+ * Obey a stop the requester already made a fact.
21932
+ *
21933
+ * The id is remembered whether or not it names the turn in flight: a stop for
21934
+ * work not yet started must still keep that work from starting, and one for a
21935
+ * turn that has already ended is simply never read again. Only the session
21936
+ * actually running the stopped turn is cancelled.
21937
+ */
21938
+ stopTurn(requestId) {
21939
+ this.stoppedTurns.add(requestId);
21940
+ while (this.stoppedTurns.size > 500)
21941
+ this.stoppedTurns.delete(this.stoppedTurns.values().next().value);
21942
+ if (this.currentTurn?.requestId !== requestId)
21943
+ return;
21944
+ if (this.client && this.sessionId)
21945
+ this.client.sessionCancel(this.sessionId);
21752
21946
  }
21753
21947
  async roster() {
21754
21948
  const roster = await this.options.api.execute("getWorkspaceRoster", {
@@ -21756,36 +21950,17 @@ var MonolithRoomTurnLoop = class {
21756
21950
  workspaceId: this.options.workspaceId
21757
21951
  });
21758
21952
  this.memberNames = new Map(roster.members.map((member) => [member.identityId, member.name]));
21953
+ this.agentMembers = new Set(roster.members.filter((member) => member.kind === "agent").map((member) => member.identityId));
21954
+ this.responseRule.setAgents(this.agentMembers);
21955
+ this.rosterAvailable = true;
21759
21956
  return roster;
21760
21957
  }
21761
- /**
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 });
21958
+ async reconcileRoster() {
21959
+ if (!this.rosterAvailable)
21960
+ await this.roster().catch(() => void 0);
21786
21961
  }
21787
21962
  /**
21788
- * Drop this Room's live harness process. The next activation starts cold.
21963
+ * Drop this corner's live harness process. The next activation starts cold.
21789
21964
  * A rotation is a fact about one live session, so the pin goes with it.
21790
21965
  */
21791
21966
  async discardSession() {
@@ -21797,14 +21972,8 @@ var MonolithRoomTurnLoop = class {
21797
21972
  if (client?.isAlive)
21798
21973
  await client.stop();
21799
21974
  }
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
- */
21975
+ /** See `MonolithRoomTurnLoop.sessionIsCurrent`: retention never keeps a
21976
+ * session whose persona or model pin the operator has since changed. */
21808
21977
  async sessionIsCurrent() {
21809
21978
  return await this.currentSessionFingerprint() === this.sessionFingerprint;
21810
21979
  }
@@ -21812,7 +21981,7 @@ var MonolithRoomTurnLoop = class {
21812
21981
  const [configuration, roster] = await Promise.all([
21813
21982
  this.options.api.execute("getAgentConfiguration", {
21814
21983
  agentId: this.agent.publicKey,
21815
- roomId: this.options.roomId
21984
+ roomId: this.options.cornerId
21816
21985
  }),
21817
21986
  this.roster()
21818
21987
  ]);
@@ -21828,13 +21997,12 @@ var MonolithRoomTurnLoop = class {
21828
21997
  if (this.client?.isAlive && this.sessionId)
21829
21998
  return this.sessionId;
21830
21999
  trace?.noteActivation("cold");
21831
- const [configuration, roster, repositoryState] = await Promise.all([
22000
+ const [configuration, roster] = await Promise.all([
21832
22001
  this.options.api.execute("getAgentConfiguration", {
21833
22002
  agentId: this.agent.publicKey,
21834
- roomId: this.options.roomId
22003
+ roomId: this.options.cornerId
21835
22004
  }),
21836
- this.roster(),
21837
- this.repositoryState()
22005
+ this.roster()
21838
22006
  ]);
21839
22007
  const self = roster.members.find((member) => member.identityId === this.agent.publicKey);
21840
22008
  const fingerprint = sessionConfigFingerprint({
@@ -21843,8 +22011,7 @@ var MonolithRoomTurnLoop = class {
21843
22011
  soul: configuration.soul ?? self?.soul,
21844
22012
  agentName: self?.name ?? this.agent.name
21845
22013
  });
21846
- const directMessage = Array.isArray(repositoryState.directParticipants) && repositoryState.directParticipants.length === 2;
21847
- await mkdir10(this.options.cwd, { recursive: true });
22014
+ await mkdir10(this.options.worktreePath, { recursive: true });
21848
22015
  const selection = configuration.model || configuration.effort ? { model: configuration.model, effort: configuration.effort } : this.options.config.modelSelection;
21849
22016
  const homeOverlay = this.options.config.agentHomeRoot ? await prepareRoomAgentHome({
21850
22017
  root: this.options.config.agentHomeRoot,
@@ -21861,7 +22028,26 @@ var MonolithRoomTurnLoop = class {
21861
22028
  })
21862
22029
  }) : {};
21863
22030
  const command = this.options.config.agentCommand ?? this.options.config.agentBinary;
21864
- const agentEnv = { ...this.options.config.agentEnv, ...homeOverlay };
22031
+ const repository = this.options.repository;
22032
+ let githubEnv = repository ? { GH_TOKEN: repository.githubToken, GITHUB_TOKEN: repository.githubToken } : {};
22033
+ if (repository && this.options.config.runtimeConfigPath && this.options.config.agentHomeRoot) {
22034
+ const gitBinary = (await execFileAsync3("which", ["git"])).stdout.trim();
22035
+ const ghBinary = await execFileAsync3("which", ["gh"]).then((result) => result.stdout.trim()).catch(() => void 0);
22036
+ githubEnv = await installCornerGitHubWrappers({
22037
+ root: this.options.config.agentHomeRoot,
22038
+ runtimeConfigPath: this.options.config.runtimeConfigPath,
22039
+ roomId: this.options.parentRoomId,
22040
+ cliEntrypoint: process.argv[1],
22041
+ gitBinary,
22042
+ ...ghBinary ? { ghBinary } : {},
22043
+ inheritedPath: this.options.config.agentEnv.PATH ?? process.env.PATH
22044
+ });
22045
+ }
22046
+ const agentEnv = {
22047
+ ...this.options.config.agentEnv,
22048
+ ...homeOverlay,
22049
+ ...githubEnv
22050
+ };
21865
22051
  this.agentEnv = agentEnv;
21866
22052
  const agentArgs = agentArgsWithModelSelection({
21867
22053
  kind: this.options.config.agentKind,
@@ -21872,7 +22058,6 @@ var MonolithRoomTurnLoop = class {
21872
22058
  const { stateDirs, tmpDir } = harnessStateDirsFromEnv(agentEnv);
21873
22059
  this.attachmentDir = tmpDir ? join6(tmpDir, "beeline-attachments") : void 0;
21874
22060
  this.sessionScratchDir = tmpDir;
21875
- this.sessionStateDirs = stateDirs;
21876
22061
  const homeStateDirs = harnessHomeStateDirs(command, agentEnv.HOME ?? operatorHome);
21877
22062
  await Promise.all(homeStateDirs.map((dir) => mkdir10(dir, { recursive: true })));
21878
22063
  const attachScratchRoot = this.options.config.agentHomeRoot ?? tmpDir;
@@ -21881,8 +22066,11 @@ var MonolithRoomTurnLoop = class {
21881
22066
  const spawnCommand = wrapAgentCommand({
21882
22067
  bwrapPath: this.options.config.bwrapPath,
21883
22068
  spec: {
21884
- mode: "readonly",
21885
- cwd: this.options.cwd,
22069
+ mode: "edit",
22070
+ cwd: this.options.worktreePath,
22071
+ worktreePath: this.options.worktreePath,
22072
+ ...repository ? { gitCommonDir: repository.gitCommonDir } : {},
22073
+ protectedPaths: [this.options.runtime.supervisorRoot],
21886
22074
  harnessStateDirs: stateDirs,
21887
22075
  harnessHomeStateDirs: homeStateDirs,
21888
22076
  ...tmpDir ? { tmpDir } : {},
@@ -21892,18 +22080,41 @@ var MonolithRoomTurnLoop = class {
21892
22080
  command,
21893
22081
  args: agentArgs
21894
22082
  });
22083
+ const clientOptions = {
22084
+ agentCommand: spawnCommand.command,
22085
+ agentArgs: spawnCommand.args,
22086
+ agentEnv,
22087
+ agentCwd: this.options.worktreePath,
22088
+ agentLabel: command,
22089
+ autoApprovePermissions: true,
22090
+ permissionHandler: () => Promise.resolve("allow")
22091
+ };
22092
+ this.client = (this.options.createAcpClient ?? ((value) => new AcpClient(value)))(clientOptions);
22093
+ await this.client.start();
21895
22094
  const servers = [
21896
- readOnlyMcpServer(this.options.config, this.options.cwd),
22095
+ ...repository ? [
22096
+ {
22097
+ name: "buzz-dev-mcp",
22098
+ command: this.options.config.mcpBinary,
22099
+ args: [],
22100
+ // ACP hosts launch stdio MCP servers with an explicit, sanitized env.
22101
+ // This token is minted for this exact corner and is also the credential
22102
+ // helper's password source, so its shell commands need the same scope as
22103
+ // the corner harness without inheriting any host credentials.
22104
+ env: [
22105
+ { name: "GH_TOKEN", value: repository.githubToken },
22106
+ { name: "GITHUB_TOKEN", value: repository.githubToken }
22107
+ ]
22108
+ }
22109
+ ] : [],
21897
22110
  beelineAgentMcpServer(this.options.config, this.options.api, {
21898
- roomId: this.options.roomId,
22111
+ roomId: this.options.parentRoomId,
21899
22112
  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.
22113
+ cornerId: this.options.cornerId,
22114
+ attachRoot: this.options.worktreePath,
22115
+ // The whole per-session overlay, not an enumerated subset: see
22116
+ // `monolith-room-turn.ts`'s matching comment.
21905
22117
  attachScratchRoot,
21906
- directMessage,
21907
22118
  ...this.options.grantRunnerEndpoint ? { grantRunner: this.options.grantRunnerEndpoint } : {}
21908
22119
  })
21909
22120
  ];
@@ -21912,43 +22123,38 @@ var MonolithRoomTurnLoop = class {
21912
22123
  piHome: agentEnv.PI_CODING_AGENT_DIR,
21913
22124
  servers
21914
22125
  });
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
22126
  const persona = configuration.soul ?? self?.soul;
21931
- const identityInstructions = `Your Beeline Room identity is ${self?.name ?? this.agent.name}.`;
22127
+ const identityInstructions = `Your Beeline identity is ${self?.name ?? this.agent.name}.`;
21932
22128
  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
- ] : [],
22129
+ ...persona?.instructions ? [`Human-authored Workspace persona: ${persona.name}. ${persona.instructions}`] : [],
21939
22130
  SOUL_HOUSE_RULE
21940
22131
  ].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");
22132
+ this.turnIdentityInstructions = harnessHonorsSessionSystemPrompt(command) ? "" : [identityInstructions, personaInstructions].filter(Boolean).join("\n\n");
21947
22133
  const opened = await this.client.sessionNew({
21948
- cwd: this.options.cwd,
22134
+ cwd: this.options.worktreePath,
21949
22135
  mcpServers: servers,
21950
- mode: "readonly",
21951
- systemPrompt: [identityInstructions, personaInstructions, capabilityContext.sessionPrompt].filter(Boolean).join("\n\n")
22136
+ mode: "edit",
22137
+ systemPrompt: [
22138
+ identityInstructions,
22139
+ personaInstructions,
22140
+ ...repository ? [
22141
+ `You are in an isolated git worktree on ${repository.featureBranch}, targeting ${repository.targetBranch}.`,
22142
+ "Work normally with the full coding tools. Commit and push only this feature branch. Use gh to open its pull request.",
22143
+ `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.`,
22144
+ "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.",
22145
+ '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.',
22146
+ "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.",
22147
+ "If any human in this corner says hold or do not merge, do not merge until a later human explicitly resumes it.",
22148
+ "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.",
22149
+ '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.',
22150
+ "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.",
22151
+ "Never push directly to the target branch. Never merge a different pull request."
22152
+ ] : [
22153
+ "This is a chat-only corner with no repository or GitHub workflow.",
22154
+ "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.",
22155
+ "Do not initialize a repository, create a branch, push, open a pull request, or wait for GitHub checks."
22156
+ ]
22157
+ ].filter(Boolean).join("\n\n")
21952
22158
  });
21953
22159
  this.sessionId = opened.sessionId;
21954
22160
  this.sessionFingerprint = fingerprint;
@@ -21958,12 +22164,7 @@ var MonolithRoomTurnLoop = class {
21958
22164
  }
21959
22165
  return opened.sessionId;
21960
22166
  }
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
- */
22167
+ /** The scheduler seam: `queue-wait` closes when a slot buys a session. */
21967
22168
  lifecycle(trace) {
21968
22169
  return {
21969
22170
  activate: async () => {
@@ -21982,6 +22183,12 @@ var MonolithRoomTurnLoop = class {
21982
22183
  suspend: () => this.discardSession()
21983
22184
  };
21984
22185
  }
22186
+ /** A picture reaches the model only if the harness AND the model take one (C87). */
22187
+ acceptsImages() {
22188
+ if (!(this.client?.canPromptWithImages() ?? false))
22189
+ return false;
22190
+ return this.modelTakesImages ?? true;
22191
+ }
21985
22192
  /** The pinned providers a failure reason should name for this session. */
21986
22193
  servingProviders() {
21987
22194
  return this.pinnedProviderOverride ? [this.pinnedProviderOverride] : this.pinnedProviders;
@@ -22000,8 +22207,7 @@ var MonolithRoomTurnLoop = class {
22000
22207
  /**
22001
22208
  * Re-pin the session to the next provider in the OpenRouter order and open a
22002
22209
  * 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.
22210
+ * named — by exactly one provider (C92).
22005
22211
  */
22006
22212
  async repinNextProvider(trace, reason) {
22007
22213
  const next = nextPinnedProvider(this.pinnedProviders, this.pinnedProviderOverride);
@@ -22018,330 +22224,495 @@ var MonolithRoomTurnLoop = class {
22018
22224
  await (trace ? trace.measure("activation", () => this.activate(trace)) : this.activate());
22019
22225
  return next;
22020
22226
  }
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
- }
22227
+ /** One turn's stopwatch; writes only when the daemon configured a trace directory. */
22228
+ beginTurnTrace(requestId) {
22229
+ const directory = this.options.config.turnTraceDir;
22230
+ if (directory)
22231
+ this.turnTraceSink ??= new TurnTraceFile(directory);
22232
+ return new TurnTrace({
22233
+ surface: "corner",
22234
+ agentId: this.agent.publicKey,
22235
+ roomId: this.options.cornerId,
22236
+ requestId,
22237
+ ...this.turnTraceSink ? { sink: this.turnTraceSink } : {}
22055
22238
  });
22056
22239
  }
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);
22240
+ async prompt(requestId, trigger, attachments = [], requestedById, restates) {
22241
+ const { api, cornerId } = this.options;
22242
+ if (this.stoppedTurns.has(requestId))
22243
+ return;
22244
+ const spoken = (text2) => restates && isCornerStatusRestatement(text2, restates) ? "" : text2;
22245
+ const requester = requestedById ? {
22246
+ pubkey: requestedById,
22247
+ ...this.memberNames.get(requestedById) ? { name: this.memberNames.get(requestedById) } : {}
22248
+ } : void 0;
22249
+ this.currentTurn = { requestId, ...requester ? { requester } : {} };
22250
+ this.carrier = this.agent.publicKey;
22251
+ const trace = this.beginTurnTrace(requestId);
22062
22252
  try {
22063
- if (!this.memberNames.has(item.authorId))
22064
- await this.roster().catch(() => void 0);
22065
22253
  await withTurnReceiptHeartbeat(api, {
22066
22254
  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
- });
22255
+ roomId: cornerId,
22256
+ requestId,
22257
+ generationId: `${this.agent.publicKey}:${cornerId}`
22258
+ }, () => {
22084
22259
  trace.noteScheduler("queue", this.options.scheduler.snapshot());
22085
22260
  trace.start("queue-wait");
22086
- await this.options.scheduler.run(this.options.roomId, this.lifecycle(trace), async () => {
22261
+ return this.options.scheduler.run(cornerId, this.lifecycle(trace), async () => {
22087
22262
  trace.end("queue-wait");
22088
22263
  trace.noteScheduler("admission", this.options.scheduler.snapshot());
22264
+ if (this.forcedStop)
22265
+ throw new Error("corner turn stopped for daemon handoff");
22266
+ this.busy = true;
22267
+ await this.syncBranch();
22089
22268
  const [conversation, roster, delivered] = await trace.measure("context-fetch", () => Promise.all([
22090
- api.execute("getRoomConversation", { roomId: this.options.roomId, limit: 200 }),
22269
+ api.execute("getRoomConversation", { roomId: cornerId, limit: 200 }),
22091
22270
  this.roster(),
22092
- this.deliver(item)
22271
+ this.attachmentDir && attachments.length ? deliverAttachments(attachments, join6(this.attachmentDir, requestId.replace(/[^\w-]/g, "_")), this.options.fetchImpl) : Promise.resolve([])
22093
22272
  ]));
22094
22273
  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) => ({
22274
+ const requestedBy = requester && !requester.name && names.get(requester.pubkey) ? { ...requester, name: names.get(requester.pubkey) } : requester;
22275
+ if (requestedBy)
22276
+ this.currentTurn = { requestId, requester: requestedBy };
22277
+ const transcriptRows = conversation.items.slice(-120).map((message) => ({
22096
22278
  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())
22279
+ line: `${names.get(message.authorId) ?? "Beeline"} [${message.type}]: ${message.body}`
22098
22280
  }));
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
22281
  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(" ") : "",
22113
- roomMentionDirectory(roster, this.agent.publicKey),
22282
+ this.turnIdentityInstructions,
22283
+ `Corner objective:
22284
+ ${this.options.objective}`,
22285
+ 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):"),
22114
22286
  [
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(" ")
22287
+ `Newest trigger:
22288
+ ${trigger}`,
22289
+ ...attachmentPromptLines(attachments, delivered, this.acceptsImages())
22290
+ ].join("\n"),
22291
+ 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.",
22292
+ MAINTAIN_ASSIGNED_IDENTITY_DIRECTIVE
22120
22293
  ].filter(Boolean).join("\n\n");
22121
22294
  const stream = new AgentTurnStream({
22122
22295
  api,
22123
22296
  agentId: this.agent.publicKey,
22124
- roomId: this.options.roomId,
22125
- requestId: item.id,
22126
- label: `monolith Room ${this.options.roomId}`
22297
+ roomId: cornerId,
22298
+ requestId,
22299
+ label: `corner ${cornerId}`
22127
22300
  });
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;
22301
+ let currentNarrationRun = "";
22302
+ let completedNarrationRuns = [];
22303
+ let narrationRunBoundary = 0;
22304
+ const takeInterimNarration = () => {
22305
+ const narration = spoken([...completedNarrationRuns, currentNarrationRun].map((run2) => spoken(durableReplyText(run2))).filter((run2) => run2 && !isPureRetryNarration(run2)).join("\n\n"));
22306
+ narrationRunBoundary += completedNarrationRuns.length;
22307
+ completedNarrationRuns = [];
22308
+ currentNarrationRun = "";
22309
+ return narration;
22310
+ };
22311
+ const publishedToolCalls = /* @__PURE__ */ new Set();
22312
+ const observedToolCalls = /* @__PURE__ */ new Set();
22313
+ const pendingToolNarrations = /* @__PURE__ */ new Map();
22314
+ const pendingToolActivities = /* @__PURE__ */ new Map();
22315
+ let lastNarratedToolCall;
22316
+ let activityAttempt = 0;
22317
+ const publishToolCalls = (calls, settledOnly) => {
22318
+ calls.forEach((call, index) => {
22319
+ const key = `${activityAttempt}:${toolCallKey(call, index)}`;
22320
+ if (settledOnly && !observedToolCalls.has(key)) {
22321
+ observedToolCalls.add(key);
22322
+ const narration2 = takeInterimNarration();
22323
+ pendingToolNarrations.set(key, narration2);
22324
+ if (narration2)
22325
+ lastNarratedToolCall = key;
22142
22326
  }
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;
22327
+ if (settledOnly || publishedToolCalls.has(key) || !toolCallSettled(call))
22328
+ return;
22329
+ publishedToolCalls.add(key);
22330
+ const narration = pendingToolNarrations.get(key) ?? "";
22331
+ this.activityTail = this.activityTail.catch(() => void 0).then(async () => {
22332
+ let activity = pendingToolActivities.get(key);
22333
+ if (!activity) {
22334
+ const toolActivity = await cornerToolActivity(call, this.options.worktreePath, requestedBy);
22335
+ activity = [
22336
+ ...narration ? [
22337
+ {
22338
+ kind: "output",
22339
+ title: "Update",
22340
+ text: narration,
22341
+ ...requestedBy ? { requestedBy } : {}
22342
+ }
22343
+ ] : [],
22344
+ toolActivity
22345
+ ];
22346
+ pendingToolActivities.set(key, activity);
22347
+ }
22348
+ await api.execute("postAgentActivity", {
22349
+ agentId: this.agent.publicKey,
22350
+ roomId: cornerId,
22351
+ requestId,
22352
+ cornerActivityKey: key,
22353
+ activity
22354
+ });
22355
+ pendingToolNarrations.delete(key);
22356
+ pendingToolActivities.delete(key);
22357
+ }).then(() => void 0).catch((error) => {
22358
+ publishedToolCalls.delete(key);
22359
+ console.error(`[thin-core] corner ${cornerId} tool activity failed:`, error);
22360
+ });
22361
+ });
22362
+ };
22363
+ const flushToolCalls = async (calls, finalReply) => {
22364
+ await this.activityTail;
22365
+ if (lastNarratedToolCall && pendingToolNarrations.get(lastNarratedToolCall) === finalReply)
22366
+ pendingToolNarrations.delete(lastNarratedToolCall);
22367
+ publishToolCalls(calls, false);
22368
+ await this.activityTail;
22369
+ publishToolCalls(calls, false);
22370
+ await this.activityTail;
22371
+ };
22372
+ const runPrompt = async () => {
22373
+ activityAttempt += 1;
22374
+ publishedToolCalls.clear();
22375
+ observedToolCalls.clear();
22376
+ pendingToolNarrations.clear();
22377
+ pendingToolActivities.clear();
22378
+ lastNarratedToolCall = void 0;
22379
+ stream.beginRun();
22380
+ completedNarrationRuns = [];
22381
+ narrationRunBoundary = 0;
22382
+ currentNarrationRun = "";
22383
+ trace.promptSent();
22384
+ return this.client.sessionPrompt(this.sessionId, promptWithImages(buildPrompt(), attachmentImageBlocks(delivered, this.acceptsImages())), 12e4, (delta, full, currentRun, runs) => {
22385
+ trace.firstModelOutput();
22386
+ if (runs) {
22387
+ completedNarrationRuns = runs.slice(narrationRunBoundary);
22388
+ currentNarrationRun = "";
22389
+ } else if (currentRun === void 0)
22390
+ currentNarrationRun += delta;
22391
+ else {
22392
+ if (currentNarrationRun && currentNarrationRun !== currentRun && !currentRun.startsWith(currentNarrationRun))
22393
+ completedNarrationRuns.push(currentNarrationRun);
22394
+ currentNarrationRun = currentRun;
22151
22395
  }
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;
22396
+ stream.onChunk(delta, full);
22397
+ }, void 0, (calls) => {
22398
+ trace.toolCalls(calls);
22399
+ publishToolCalls(calls, true);
22400
+ });
22162
22401
  };
22163
22402
  let result = await runPrompt();
22164
22403
  trace.promptSettled();
22165
22404
  let explained = await this.explainEmpty(result);
22166
- if (explained && shouldRetryEmptyTurn(explained)) {
22405
+ if (explained && !restates && shouldRetryEmptyTurn(explained)) {
22406
+ await flushToolCalls(result.toolCalls, "");
22167
22407
  const silent = this.servingProviders();
22168
22408
  const next = await this.repinNextProvider(trace, explained.reason);
22169
22409
  if (next) {
22170
- console.warn(`[thin-core] monolith Room ${this.options.roomId} turn ${item.id}: ${turnFailureReasonWithProvider(explained.reason, silent)}; retrying on ${next}`);
22410
+ console.warn(`[thin-core] corner ${cornerId} turn ${requestId}: ${turnFailureReasonWithProvider(explained.reason, silent)}; retrying on ${next}`);
22171
22411
  result = await runPrompt();
22172
- trace.promptSettled();
22173
- explained = await this.explainEmpty(result);
22174
- }
22175
- }
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?.();
22412
+ trace.promptSettled();
22413
+ explained = await this.explainEmpty(result);
22414
+ }
22188
22415
  }
22189
- for (const call of result?.toolCalls ?? []) {
22416
+ if (this.stoppedTurns.has(requestId)) {
22417
+ const stoppedText = spoken(durableReplyText(result.agentText));
22418
+ await flushToolCalls(result.toolCalls, stoppedText);
22419
+ await stream.settle(stoppedText);
22420
+ throw new TurnStoppedError("turn stopped by the requester");
22421
+ }
22422
+ let reply = durableReplyText(result.agentText);
22423
+ if (!reply && explained?.recoveredText)
22424
+ reply = durableReplyText(explained.recoveredText);
22425
+ await flushToolCalls(result.toolCalls, reply);
22426
+ for (const call of result.toolCalls) {
22190
22427
  const failure = toolCallFailureLine(call);
22191
- if (failure) {
22192
- console.warn(`[thin-core] monolith Room ${this.options.roomId} ${failure}`);
22193
- }
22428
+ if (failure)
22429
+ console.warn(`[thin-core] corner ${cornerId} ${failure}`);
22194
22430
  }
22195
22431
  stream.close();
22196
- let reply = durableReplyText(result.agentText);
22197
22432
  if (!reply && explained) {
22198
- reply = explained.recoveredText ? durableReplyText(explained.recoveredText) : "";
22199
- if (!reply) {
22433
+ if (!reply && !(restates && !isAccountOrProviderRefusal(explained.record))) {
22200
22434
  throw new Error(turnFailureReasonWithProvider(explained.reason, this.servingProviders()));
22201
22435
  }
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);
22436
+ console.warn(`[thin-core] corner ${cornerId} turn ${requestId}: ${explained.reason}`);
22206
22437
  }
22207
- await trace.measure("publish", () => stream.settle(reply, reply ? {
22208
- triggerMessageId: item.id,
22209
- mentionIds: agentReplyMentionIds(reply, roster, this.agent.publicKey)
22210
- } : {}));
22211
- }, { priority: "interactive", roomKey: this.options.roomId });
22212
- }, (error) => console.error(`[thin-core] monolith Room ${this.options.roomId} receipt heartbeat failed:`, error));
22438
+ const durableReply = spoken(reply);
22439
+ if (durableReply && requestedById)
22440
+ this.continuityRebuildRequested = true;
22441
+ await trace.measure("publish", () => stream.settle(durableReply, requestedById ? { triggerMessageId: requestId } : {}, durableReply && requestedById ? () => {
22442
+ this.responseRule.noteReply(this.agent.publicKey, [requestedById]);
22443
+ this.continuityRebuildRequested = false;
22444
+ } : void 0));
22445
+ }, { priority: "interactive", roomKey: cornerId });
22446
+ }, (error) => console.error(`[thin-core] corner ${cornerId} receipt heartbeat failed:`, error));
22213
22447
  await api.execute("postAgentTurnReceipt", {
22214
22448
  agentId: this.agent.publicKey,
22215
- roomId: this.options.roomId,
22216
- requestId: item.id,
22449
+ roomId: cornerId,
22450
+ requestId,
22217
22451
  status: "complete",
22218
- generationId: `${this.agent.publicKey}:${this.options.roomId}`
22452
+ generationId: `${this.agent.publicKey}:${cornerId}`
22219
22453
  });
22220
22454
  await trace.finish("complete");
22221
22455
  } catch (error) {
22456
+ if (error instanceof TurnStoppedError || this.stoppedTurns.has(requestId)) {
22457
+ console.log(`[thin-core] corner ${cornerId} turn ${requestId} stopped by the requester`);
22458
+ await trace.finish("cancelled");
22459
+ return;
22460
+ }
22222
22461
  const reason = distillTurnFailureReason(error);
22223
22462
  await api.execute("postAgentTurnReceipt", {
22224
22463
  agentId: this.agent.publicKey,
22225
- roomId: this.options.roomId,
22226
- requestId: item.id,
22464
+ roomId: cornerId,
22465
+ requestId,
22227
22466
  status: "failed",
22228
- generationId: `${this.agent.publicKey}:${this.options.roomId}`,
22467
+ generationId: `${this.agent.publicKey}:${cornerId}`,
22229
22468
  reason
22230
22469
  });
22231
22470
  await trace.finish("failed", reason);
22232
22471
  throw error;
22233
22472
  } finally {
22234
22473
  this.busy = false;
22474
+ this.currentTurn = void 0;
22475
+ }
22476
+ }
22477
+ /** Whether this agent opened the corner. A corner with no recorded opener
22478
+ * behaves exactly as it did before members could carry it. */
22479
+ isOpener() {
22480
+ return !this.options.openedBy || this.options.openedBy === this.agent.publicKey;
22481
+ }
22482
+ /**
22483
+ * Whether this agent is the one carrying the corner right now.
22484
+ *
22485
+ * Every member agent polls the corner, but its lifecycle — a server check
22486
+ * note, a close request answered with work — is ONE fact and must start ONE
22487
+ * turn, not one per member (the "one check turn per changed server state"
22488
+ * rule). The carrier is whoever answered in the corner last, which is the
22489
+ * opener until a human hands the work to someone else.
22490
+ */
22491
+ carriesCorner() {
22492
+ return (this.carrier ?? this.options.openedBy ?? this.agent.publicKey) === this.agent.publicKey;
22493
+ }
22494
+ /** Notes an agent's durable message as the corner changing hands. */
22495
+ noteCarrier(authorId) {
22496
+ if (this.agentMembers.has(authorId))
22497
+ this.carrier = authorId;
22498
+ }
22499
+ async noteIncomingCarrier(item) {
22500
+ if (item.agentAuthor && !this.agentMembers.has(item.authorId))
22501
+ await this.roster().catch(() => void 0);
22502
+ this.noteCarrier(item.authorId);
22503
+ }
22504
+ /**
22505
+ * Whether a message in this corner is addressed to THIS agent.
22506
+ *
22507
+ * A corner now runs like a Room — every member agent polls it — so an
22508
+ * A server-resolved mention always routes to its agent. Otherwise the one
22509
+ * agent already exchanging messages with this exact sender continues; the
22510
+ * lifecycle carrier is deliberately not a conversational fallback.
22511
+ */
22512
+ addressesThisAgent(item) {
22513
+ if (item.mentionIds.includes(this.agent.publicKey))
22514
+ return true;
22515
+ return this.responseRule.continues(item, this.agent.publicKey);
22516
+ }
22517
+ /**
22518
+ * Bring this worktree onto the corner's branch as GitHub currently has it,
22519
+ * before any work is done on top of it.
22520
+ *
22521
+ * The branch is the shared artifact: another member agent may have pushed to
22522
+ * it since this helper last looked, and a first touch of a corner this
22523
+ * helper did not open starts from whatever `room-runtime.ts` restored. A
22524
+ * divergence this cannot rebase away is raised, not pushed over — the turn
22525
+ * fails with that sentence and the server inscribes it in the corner.
22526
+ */
22527
+ async syncBranch() {
22528
+ const repository = this.options.repository;
22529
+ if (!repository)
22530
+ return;
22531
+ const token = await this.options.api.execute("getRoomGitHubToken", { roomId: this.options.parentRoomId }).then((granted) => granted.token).catch(() => repository.githubToken);
22532
+ await syncCornerBranch({
22533
+ worktreePath: this.options.worktreePath,
22534
+ featureBranch: repository.featureBranch,
22535
+ env: { ...process.env, GH_TOKEN: token, GITHUB_TOKEN: token, GIT_TERMINAL_PROMPT: "0" }
22536
+ });
22537
+ }
22538
+ /** The server's check state for this head, or the notes' own verdict when the server carries none. */
22539
+ async checksState(notes) {
22540
+ try {
22541
+ const restore = await this.options.api.execute("getCornerRestoreState", {
22542
+ cornerId: this.options.cornerId
22543
+ });
22544
+ const fromServer = checksStateFromLifecycle(restore.lifecycle);
22545
+ if (fromServer)
22546
+ return fromServer;
22547
+ } catch (error) {
22548
+ console.error(`[thin-core] corner ${this.options.cornerId} check state read failed:`, error);
22235
22549
  }
22550
+ return notes.some((note) => completedCheckNote(note) === "failed") ? "failing" : "passing";
22236
22551
  }
22237
22552
  async run() {
22238
- const { api, roomId, signal } = this.options;
22239
- let cursor3;
22553
+ const { api, cornerId, signal } = this.options;
22554
+ const activation = await api.execute("getRoomInbox", {
22555
+ roomId: cornerId,
22556
+ startAtLatest: true
22557
+ });
22558
+ let cursor3 = activation.cursor;
22240
22559
  const processedInboxIds = /* @__PURE__ */ new Set();
22241
22560
  const pushedInbox = [];
22242
22561
  let pendingPushedCursor;
22243
22562
  let liveConnected = false;
22244
- let stopLive;
22563
+ const rewindSupported = Array.isArray(activation.rewindIds);
22564
+ for (const id of activation.rewindIds ?? [])
22565
+ processedInboxIds.add(id);
22566
+ const stopLive = api.liveSubscribe?.(cornerId, cursor3, (items, pushedCursor) => {
22567
+ pushedInbox.push(...items);
22568
+ for (const item of items) {
22569
+ const stopped = turnStopRequestId(item, this.agent.publicKey);
22570
+ if (stopped)
22571
+ this.stopTurn(stopped);
22572
+ }
22573
+ pendingPushedCursor = laterInboxCursor(pendingPushedCursor, pushedCursor);
22574
+ this.wakeIntake?.();
22575
+ this.wakeIntake = void 0;
22576
+ }, (connected, capabilities) => {
22577
+ liveConnected = connected && capabilities?.pushIntake === true;
22578
+ this.wakeIntake?.();
22579
+ this.wakeIntake = void 0;
22580
+ }, {
22581
+ ...this.options.config.daemonReleaseVersion ? { releaseVersion: this.options.config.daemonReleaseVersion } : {},
22582
+ ...this.options.config.daemonSourceSha ? { sourceSha: this.options.config.daemonSourceSha } : {},
22583
+ available: !this.options.config.modelUnavailable
22584
+ });
22585
+ const history = await api.execute("getRoomConversation", {
22586
+ roomId: cornerId,
22587
+ limit: 200,
22588
+ window: "continuity"
22589
+ });
22590
+ await this.roster().catch(() => void 0);
22591
+ this.responseRule.observeAll(history.items);
22592
+ for (const item of history.items) {
22593
+ if (item.type === "message")
22594
+ await this.noteIncomingCarrier(item);
22595
+ }
22596
+ const durableAgentReplies = history.items.filter((item) => item.type === "message" && item.authorId === this.agent.publicKey && item.body.trim() !== this.options.objective.trim());
22597
+ if (durableAgentReplies.length === 0 && this.isOpener()) {
22598
+ await this.prompt(history.items.find((item) => item.requestId)?.requestId ?? cornerId.replaceAll("-", ""), this.options.objective);
22599
+ }
22245
22600
  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
22265
- });
22601
+ let pollWithoutWait = false;
22266
22602
  while (!signal?.aborted) {
22267
22603
  try {
22268
- if (!this.activeTurn && this.queuedTurns.length) {
22269
- this.startPrompt(this.queuedTurns.shift());
22604
+ if (this.continuityRebuildRequested) {
22605
+ const reconciled = await api.execute("getRoomConversation", {
22606
+ roomId: cornerId,
22607
+ limit: 200,
22608
+ window: "continuity"
22609
+ });
22610
+ this.responseRule.replaceHistory(reconciled.items);
22611
+ this.carrier = void 0;
22612
+ for (const item of reconciled.items) {
22613
+ if (item.type === "message")
22614
+ await this.noteIncomingCarrier(item);
22615
+ }
22616
+ this.continuityRebuildRequested = false;
22270
22617
  }
22271
- const pollNow = pushedInbox.length === 0 && (!liveConnected || this.reconciliationRequested);
22272
- const inbox = !pollNow ? { items: [], cursor: void 0 } : await api.execute("getRoomInbox", {
22273
- roomId,
22618
+ const pollNow = pushedInbox.length > 0 || !liveConnected || this.reconciliationRequested;
22619
+ const inbox = !pollNow ? { items: [], cursor: void 0, closeRequested: false } : await api.execute("getCornerCloseRequests", {
22620
+ cornerId,
22274
22621
  ...cursor3 ? { after: cursor3 } : {},
22275
- ...rewindSupported ? { rewind: true } : {},
22276
- limit: 200
22622
+ ...rewindSupported ? { rewind: true } : {}
22277
22623
  });
22278
22624
  if (pollNow) {
22279
22625
  this.reconciliationRequested = false;
22280
22626
  }
22281
- const delivered = [...pushedInbox.splice(0), ...inbox.items];
22627
+ if (inbox.closeRequested) {
22628
+ await this.options.onCloseRequested();
22629
+ return;
22630
+ }
22631
+ const checkNotes = [];
22632
+ const delivered = orderInboxItems([...pushedInbox.splice(0), ...inbox.items]);
22282
22633
  for (const item of delivered) {
22283
22634
  if (processedInboxIds.has(item.id))
22284
22635
  continue;
22285
22636
  processedInboxIds.add(item.id);
22286
- while (processedInboxIds.size > 1e4)
22637
+ while (processedInboxIds.size > INBOX_DEDUPLICATION_LIMIT)
22287
22638
  processedInboxIds.delete(processedInboxIds.values().next().value);
22288
- if (!inboxItemTriggersTurn(item, this.agent.publicKey))
22639
+ const stoppedTurn = turnStopRequestId(item, this.agent.publicKey);
22640
+ if (stoppedTurn) {
22641
+ this.stopTurn(stoppedTurn);
22289
22642
  continue;
22290
- if (!inboxItemSkipsSenderPolicy(item, this.agent.publicKey)) {
22643
+ }
22644
+ if (item.type === "message") {
22645
+ if (item.authorId !== this.agent.publicKey)
22646
+ await this.reconcileRoster();
22647
+ await this.noteIncomingCarrier(item);
22648
+ if (item.authorId === this.agent.publicKey)
22649
+ continue;
22650
+ const addressed = this.addressesThisAgent(item);
22651
+ this.responseRule.observe(item);
22652
+ if (!addressed)
22653
+ continue;
22291
22654
  const authority = await api.execute("getRoomAuthority", {
22292
- roomId,
22655
+ roomId: cornerId,
22293
22656
  principalId: item.authorId
22294
22657
  });
22295
22658
  const humanPermitted = authority.principalKind === "human" ? await this.currentPrincipalCanDrive(this.options.workspaceId, item.authorId) : false;
22296
22659
  if (!roomPrincipalMayAddressAgent(authority, humanPermitted))
22297
22660
  continue;
22661
+ await this.prompt(item.id, item.body, item.attachments, item.authorId);
22662
+ pollWithoutWait = true;
22663
+ continue;
22664
+ }
22665
+ const grantDecision = item.type === "system" && item.mentionIds.includes(this.agent.publicKey) && parseGrantDecisionLine(item.body) !== void 0;
22666
+ if (grantDecision) {
22667
+ await this.prompt(item.id, `${item.body}
22668
+ 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);
22669
+ pollWithoutWait = true;
22670
+ continue;
22671
+ }
22672
+ if (isCheckStartNote(item)) {
22673
+ this.lastChecksState = void 0;
22674
+ continue;
22675
+ }
22676
+ if (completedCheckNote(item))
22677
+ checkNotes.push(item);
22678
+ }
22679
+ if (checkNotes.length && this.carriesCorner()) {
22680
+ const state = await this.checksState(checkNotes);
22681
+ if (state && state !== "pending" && state !== this.lastChecksState) {
22682
+ this.lastChecksState = state;
22683
+ const lines = checkNotes.map((note) => note.body);
22684
+ await this.prompt(checkNotes[checkNotes.length - 1].id, lines.join("\n"), [], void 0, lines);
22685
+ pollWithoutWait = true;
22298
22686
  }
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
22687
  }
22307
22688
  cursor3 = laterInboxCursor(cursor3, laterInboxCursor(inbox.cursor, pendingPushedCursor));
22308
22689
  pendingPushedCursor = void 0;
22309
- api.updateLiveCursor?.(roomId, cursor3);
22690
+ api.updateLiveCursor?.(cornerId, cursor3);
22310
22691
  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
- }
22692
+ this.options.onPoll();
22693
+ await Promise.race([
22694
+ wait2(pollWithoutWait ? 0 : liveConnected ? 2147483647 : this.options.pollMs ?? cornerClosePollMs(), signal),
22695
+ pushedInbox.length ? Promise.resolve() : new Promise((resolve30) => {
22696
+ this.wakeIntake = resolve30;
22697
+ })
22698
+ ]);
22699
+ pollWithoutWait = false;
22320
22700
  } catch (error) {
22321
22701
  if (signal?.aborted)
22322
22702
  break;
22323
- this.options.health.failure(1e3);
22324
- console.error(`[thin-core] monolith Room ${roomId} turn loop failed:`, error);
22703
+ this.options.onFailure(1e3);
22704
+ console.error(`[thin-core] corner ${cornerId} turn loop failed:`, error);
22325
22705
  await wait2(1e3, signal);
22326
22706
  }
22327
22707
  }
22328
22708
  } finally {
22329
22709
  stopLive?.();
22330
22710
  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);
22711
+ this.options.grantRunner?.unregister(cornerId);
22712
+ await this.options.scheduler.suspend(cornerId);
22337
22713
  }
22338
22714
  }
22339
22715
  };
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
22716
  async function wait2(ms, signal) {
22346
22717
  if (signal?.aborted)
22347
22718
  return;
@@ -22771,7 +23142,7 @@ function shouldPostInitialCornerWorkingState(restore, isOpener = true) {
22771
23142
  }
22772
23143
  async function materializeCornerWorktree(input) {
22773
23144
  const remote = roomCheckoutRemote(input.remote);
22774
- const repositoryHash = createHash4("sha256").update(remote).digest("hex").slice(0, 24);
23145
+ const repositoryHash = createHash5("sha256").update(remote).digest("hex").slice(0, 24);
22775
23146
  const gitCommonDir = resolve19(input.supervisorRoot, "beeline", "repositories", `${repositoryHash}.git`);
22776
23147
  const path = resolve19(input.supervisorRoot, "beeline", "corners", input.cornerId);
22777
23148
  await mkdir11(dirname6(gitCommonDir), { recursive: true, mode: 448 });
@@ -23140,7 +23511,7 @@ var RoomRuntimeCoordinator = class {
23140
23511
  return this.roomRoot(roomId);
23141
23512
  const remote = roomCheckoutRemote(repository.remote);
23142
23513
  const targetBranch = repository.targetBranch || "main";
23143
- const checkoutId = createHash4("sha256").update(`${remote}\0${targetBranch}`).digest("hex").slice(0, 24);
23514
+ const checkoutId = createHash5("sha256").update(`${remote}\0${targetBranch}`).digest("hex").slice(0, 24);
23144
23515
  const path = resolve19(this.runtime.supervisorRoot, "beeline", "room-checkouts", checkoutId);
23145
23516
  await mkdir11(dirname6(path), { recursive: true, mode: 448 });
23146
23517
  const token = remote.startsWith("https://github.com/") ? await this.options.daemonApi.execute("getRoomGitHubToken", { roomId }) : void 0;
@@ -23752,7 +24123,7 @@ async function runStartCommand(args, interactiveUi) {
23752
24123
 
23753
24124
  // apps/body/dist/connect-command.js
23754
24125
  import { spawn as spawn5 } from "node:child_process";
23755
- import { createHash as createHash6 } from "node:crypto";
24126
+ import { createHash as createHash7 } from "node:crypto";
23756
24127
  import { chmod as chmod5, mkdir as mkdir14, readFile as readFile10, unlink as unlink2, writeFile as writeFile10 } from "node:fs/promises";
23757
24128
  import { dirname as dirname10, resolve as resolve22 } from "node:path";
23758
24129
  import { stdin as stdin3, stdout as stdout4 } from "node:process";
@@ -24328,7 +24699,7 @@ function requestConnectGrant(baseUrl, pairingCode, selection, fetchImpl) {
24328
24699
  const normalizedPairingCode = normalizeAgentPairingCode(pairingCode);
24329
24700
  if (!normalizedPairingCode)
24330
24701
  throw new Error("invalid pairing code");
24331
- const avatarSeed = createHash6("sha256").update(normalizedPairingCode.toUpperCase()).digest("hex").slice(0, 32);
24702
+ const avatarSeed = createHash7("sha256").update(normalizedPairingCode.toUpperCase()).digest("hex").slice(0, 32);
24332
24703
  return jsonRequest(`${baseUrl}/auth/agent/connect`, {
24333
24704
  pairing_code: normalizedPairingCode,
24334
24705
  harness: selection.harness,
@@ -26154,10 +26525,7 @@ async function main() {
26154
26525
  await repairInstallForwarders(startupLayout).catch(() => void 0);
26155
26526
  }
26156
26527
  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
26528
  if (command === "--version" || command === "version") {
26160
- const config = loadBodyConfig({ workspaceRoot, llmEnvFile });
26161
26529
  console.log(import_picocolors3.default.bold("beeline 0.0.0"));
26162
26530
  const layout = beelineInstallLayout(process.env);
26163
26531
  if (layout) {
@@ -26165,9 +26533,6 @@ async function main() {
26165
26533
  const active = await activeReleaseId(layout);
26166
26534
  console.log(`${import_picocolors3.default.dim("installed bundle:")} ${describeIdentity(identity)}${active ? ` (release ${active})` : ""}`);
26167
26535
  }
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
26536
  return;
26172
26537
  }
26173
26538
  if (command === "daemon") {