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.
- package/README.md +1 -1
- package/dist/usebeeline.mjs +1543 -1178
- package/package.json +1 -1
package/dist/usebeeline.mjs
CHANGED
|
@@ -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:
|
|
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 =
|
|
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:
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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 {
|
|
17983
|
-
import {
|
|
17984
|
-
import {
|
|
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
|
-
//
|
|
17988
|
-
|
|
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.
|
|
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/
|
|
20264
|
-
|
|
20265
|
-
|
|
20266
|
-
|
|
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
|
-
//
|
|
20296
|
-
var
|
|
20297
|
-
|
|
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/
|
|
20594
|
-
|
|
20595
|
-
|
|
20596
|
-
|
|
20597
|
-
|
|
20598
|
-
|
|
20599
|
-
|
|
20600
|
-
|
|
20601
|
-
|
|
20602
|
-
|
|
20603
|
-
|
|
20604
|
-
};
|
|
20605
|
-
|
|
20606
|
-
|
|
20607
|
-
await
|
|
20608
|
-
|
|
20609
|
-
|
|
20610
|
-
|
|
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/
|
|
20645
|
-
var
|
|
20646
|
-
var
|
|
20647
|
-
|
|
20648
|
-
|
|
20649
|
-
|
|
20650
|
-
|
|
20651
|
-
|
|
20652
|
-
|
|
20653
|
-
|
|
20654
|
-
|
|
20655
|
-
|
|
20656
|
-
|
|
20657
|
-
|
|
20658
|
-
|
|
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
|
|
20662
|
-
|
|
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
|
|
20665
|
-
|
|
20666
|
-
|
|
20667
|
-
|
|
20668
|
-
|
|
20669
|
-
|
|
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
|
|
20673
|
-
|
|
20674
|
-
|
|
20675
|
-
|
|
20676
|
-
if (
|
|
20677
|
-
return
|
|
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
|
|
20685
|
-
|
|
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
|
|
20709
|
-
const
|
|
20710
|
-
|
|
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
|
|
20726
|
-
|
|
20727
|
-
|
|
20728
|
-
if (
|
|
20729
|
-
return
|
|
20730
|
-
const
|
|
20731
|
-
return
|
|
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
|
|
20734
|
-
return
|
|
20769
|
+
function isGrantDecisionLine(item, agentId) {
|
|
20770
|
+
return item.type === "system" && item.mentionIds.includes(agentId) && parseGrantDecisionLine(item.body) !== void 0;
|
|
20735
20771
|
}
|
|
20736
|
-
function
|
|
20737
|
-
|
|
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
|
|
20740
|
-
if (
|
|
20781
|
+
function pendingGrantToolCall(call) {
|
|
20782
|
+
if (!/(?:^|[._:/-])request_grant$/i.test(call.title ?? ""))
|
|
20741
20783
|
return false;
|
|
20742
|
-
return
|
|
20784
|
+
return /pending, card posted/i.test(typeof call.content === "string" ? call.content : JSON.stringify(call.content ?? ""));
|
|
20743
20785
|
}
|
|
20744
|
-
|
|
20745
|
-
const
|
|
20746
|
-
|
|
20747
|
-
|
|
20748
|
-
|
|
20749
|
-
|
|
20750
|
-
|
|
20751
|
-
|
|
20752
|
-
|
|
20753
|
-
|
|
20754
|
-
|
|
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
|
-
|
|
20758
|
-
|
|
20759
|
-
|
|
20760
|
-
|
|
20761
|
-
|
|
20762
|
-
|
|
20763
|
-
|
|
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
|
-
|
|
20772
|
-
|
|
20773
|
-
return CORNER_CLOSE_POLL_BASE_MS + Math.floor(random() * 3e3);
|
|
20807
|
+
function escapeRegExp(value) {
|
|
20808
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
20774
20809
|
}
|
|
20775
|
-
|
|
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
|
-
|
|
20803
|
-
|
|
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
|
-
/**
|
|
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
|
|
20809
|
-
|
|
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
|
-
|
|
20813
|
-
|
|
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.
|
|
20896
|
+
options.grantRunner?.register(options.roomId, {
|
|
20823
20897
|
workspaceId: options.workspaceId,
|
|
20824
|
-
cwd: options.
|
|
20825
|
-
// A
|
|
20826
|
-
//
|
|
20827
|
-
|
|
20828
|
-
|
|
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
|
-
|
|
20839
|
-
|
|
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
|
-
*
|
|
20863
|
-
*
|
|
20864
|
-
|
|
20865
|
-
|
|
20866
|
-
|
|
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
|
-
/**
|
|
20875
|
-
*
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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) =>
|
|
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
|
|
21096
|
+
await mkdir8(attachScratchRoot, { recursive: true });
|
|
20965
21097
|
const spawnCommand = wrapAgentCommand({
|
|
20966
21098
|
bwrapPath: this.options.config.bwrapPath,
|
|
20967
21099
|
spec: {
|
|
20968
|
-
mode: "
|
|
20969
|
-
cwd: this.options.
|
|
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
|
-
|
|
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.
|
|
21114
|
+
roomId: this.options.roomId,
|
|
21011
21115
|
workspaceId: this.options.workspaceId,
|
|
21012
|
-
|
|
21013
|
-
|
|
21014
|
-
//
|
|
21015
|
-
//
|
|
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 ? [
|
|
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
|
-
|
|
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.
|
|
21164
|
+
cwd: this.options.cwd,
|
|
21034
21165
|
mcpServers: servers,
|
|
21035
|
-
mode: "
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
|
|
21127
|
-
|
|
21128
|
-
|
|
21129
|
-
|
|
21130
|
-
|
|
21131
|
-
|
|
21132
|
-
|
|
21133
|
-
|
|
21134
|
-
|
|
21135
|
-
|
|
21136
|
-
|
|
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
|
-
|
|
21140
|
-
|
|
21141
|
-
|
|
21142
|
-
|
|
21143
|
-
|
|
21144
|
-
|
|
21145
|
-
|
|
21146
|
-
|
|
21147
|
-
|
|
21148
|
-
|
|
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:
|
|
21153
|
-
requestId,
|
|
21154
|
-
generationId: `${this.agent.publicKey}:${
|
|
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
|
-
|
|
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:
|
|
21333
|
+
api.execute("getRoomConversation", { roomId: this.options.roomId, limit: 200 }),
|
|
21167
21334
|
this.roster(),
|
|
21168
|
-
this.
|
|
21335
|
+
this.deliver(item)
|
|
21169
21336
|
]));
|
|
21170
21337
|
const names = new Map(roster.members.map((member) => [member.identityId, member.name]));
|
|
21171
|
-
const
|
|
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:
|
|
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.
|
|
21180
|
-
|
|
21181
|
-
${this.
|
|
21182
|
-
|
|
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
|
-
|
|
21185
|
-
|
|
21186
|
-
|
|
21187
|
-
|
|
21188
|
-
|
|
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:
|
|
21195
|
-
requestId,
|
|
21196
|
-
label: `
|
|
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
|
-
|
|
21221
|
-
|
|
21222
|
-
|
|
21223
|
-
|
|
21224
|
-
|
|
21225
|
-
|
|
21226
|
-
|
|
21227
|
-
|
|
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 &&
|
|
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]
|
|
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
|
-
|
|
21244
|
-
|
|
21245
|
-
|
|
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]
|
|
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
|
|
21447
|
+
if (!reply) {
|
|
21256
21448
|
throw new Error(turnFailureReasonWithProvider(explained.reason, this.servingProviders()));
|
|
21257
21449
|
}
|
|
21258
|
-
console.warn(`[thin-core]
|
|
21450
|
+
console.warn(`[thin-core] monolith Room ${this.options.roomId} turn ${item.id}: ${explained.reason}`);
|
|
21259
21451
|
}
|
|
21260
|
-
|
|
21261
|
-
|
|
21262
|
-
|
|
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:
|
|
21266
|
-
requestId,
|
|
21475
|
+
roomId: this.options.roomId,
|
|
21476
|
+
requestId: item.id,
|
|
21267
21477
|
status: "complete",
|
|
21268
|
-
generationId: `${this.agent.publicKey}:${
|
|
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:
|
|
21276
|
-
requestId,
|
|
21490
|
+
roomId: this.options.roomId,
|
|
21491
|
+
requestId: item.id,
|
|
21277
21492
|
status: "failed",
|
|
21278
|
-
generationId: `${this.agent.publicKey}:${
|
|
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,
|
|
21364
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
21404
|
-
|
|
21405
|
-
|
|
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
|
-
|
|
21413
|
-
|
|
21414
|
-
|
|
21415
|
-
|
|
21416
|
-
|
|
21417
|
-
|
|
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 >
|
|
21579
|
+
while (processedInboxIds.size > INBOX_DEDUPLICATION_LIMIT)
|
|
21423
21580
|
processedInboxIds.delete(processedInboxIds.values().next().value);
|
|
21424
|
-
|
|
21425
|
-
|
|
21426
|
-
|
|
21427
|
-
|
|
21428
|
-
|
|
21429
|
-
|
|
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
|
|
21591
|
+
roomId,
|
|
21432
21592
|
principalId: item.authorId
|
|
21433
21593
|
});
|
|
21434
|
-
|
|
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?.(
|
|
21608
|
+
api.updateLiveCursor?.(roomId, cursor3);
|
|
21466
21609
|
if (pollNow)
|
|
21467
|
-
this.options.
|
|
21468
|
-
|
|
21469
|
-
|
|
21470
|
-
|
|
21471
|
-
|
|
21472
|
-
|
|
21473
|
-
|
|
21474
|
-
|
|
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.
|
|
21479
|
-
console.error(`[thin-core]
|
|
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(
|
|
21487
|
-
|
|
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/
|
|
21506
|
-
import { mkdir as
|
|
21507
|
-
import {
|
|
21508
|
-
|
|
21509
|
-
|
|
21510
|
-
|
|
21511
|
-
|
|
21512
|
-
|
|
21513
|
-
|
|
21514
|
-
|
|
21515
|
-
|
|
21516
|
-
|
|
21517
|
-
|
|
21518
|
-
|
|
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
|
|
21521
|
-
|
|
21522
|
-
|
|
21523
|
-
|
|
21524
|
-
|
|
21525
|
-
|
|
21526
|
-
|
|
21527
|
-
|
|
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
|
|
21530
|
-
|
|
21531
|
-
|
|
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
|
-
|
|
21537
|
-
|
|
21538
|
-
|
|
21539
|
-
|
|
21540
|
-
|
|
21541
|
-
|
|
21542
|
-
|
|
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
|
-
|
|
21545
|
-
|
|
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
|
|
21548
|
-
|
|
21549
|
-
|
|
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
|
|
21552
|
-
|
|
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
|
|
21560
|
-
|
|
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
|
|
21563
|
-
|
|
21564
|
-
|
|
21565
|
-
|
|
21566
|
-
|
|
21567
|
-
|
|
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
|
|
21570
|
-
|
|
21571
|
-
|
|
21572
|
-
|
|
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
|
|
21575
|
-
const
|
|
21576
|
-
|
|
21577
|
-
|
|
21578
|
-
|
|
21579
|
-
|
|
21580
|
-
|
|
21581
|
-
|
|
21582
|
-
|
|
21583
|
-
|
|
21584
|
-
|
|
21585
|
-
|
|
21586
|
-
|
|
21587
|
-
if (
|
|
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
|
|
21596
|
-
|
|
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
|
|
21599
|
-
|
|
21600
|
-
|
|
21601
|
-
|
|
21602
|
-
|
|
21603
|
-
|
|
21604
|
-
|
|
21605
|
-
|
|
21606
|
-
|
|
21607
|
-
|
|
21608
|
-
|
|
21609
|
-
|
|
21610
|
-
|
|
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
|
-
|
|
21614
|
-
|
|
21615
|
-
|
|
21616
|
-
|
|
21617
|
-
|
|
21618
|
-
|
|
21619
|
-
|
|
21620
|
-
|
|
21621
|
-
|
|
21622
|
-
|
|
21623
|
-
|
|
21624
|
-
|
|
21625
|
-
|
|
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
|
|
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
|
-
|
|
21658
|
-
|
|
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
|
-
/**
|
|
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
|
|
21667
|
-
|
|
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.
|
|
21900
|
+
options.grantRunner?.register(options.cornerId, {
|
|
21682
21901
|
workspaceId: options.workspaceId,
|
|
21683
|
-
cwd: options.
|
|
21684
|
-
// A
|
|
21685
|
-
//
|
|
21686
|
-
|
|
21687
|
-
|
|
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
|
-
|
|
21694
|
-
|
|
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
|
-
|
|
21744
|
-
|
|
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.
|
|
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
|
-
|
|
21763
|
-
|
|
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
|
|
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
|
-
*
|
|
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.
|
|
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
|
|
22000
|
+
const [configuration, roster] = await Promise.all([
|
|
21832
22001
|
this.options.api.execute("getAgentConfiguration", {
|
|
21833
22002
|
agentId: this.agent.publicKey,
|
|
21834
|
-
roomId: this.options.
|
|
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
|
-
|
|
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
|
|
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: "
|
|
21885
|
-
cwd: this.options.
|
|
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
|
-
|
|
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.
|
|
22111
|
+
roomId: this.options.parentRoomId,
|
|
21899
22112
|
workspaceId: this.options.workspaceId,
|
|
21900
|
-
|
|
21901
|
-
|
|
21902
|
-
//
|
|
21903
|
-
//
|
|
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
|
|
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
|
-
|
|
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.
|
|
22134
|
+
cwd: this.options.worktreePath,
|
|
21949
22135
|
mcpServers: servers,
|
|
21950
|
-
mode: "
|
|
21951
|
-
systemPrompt: [
|
|
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.
|
|
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
|
-
|
|
22022
|
-
|
|
22023
|
-
|
|
22024
|
-
|
|
22025
|
-
|
|
22026
|
-
|
|
22027
|
-
|
|
22028
|
-
|
|
22029
|
-
|
|
22030
|
-
|
|
22031
|
-
|
|
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(
|
|
22058
|
-
const {
|
|
22059
|
-
|
|
22060
|
-
|
|
22061
|
-
const
|
|
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:
|
|
22068
|
-
requestId
|
|
22069
|
-
generationId: `${this.agent.publicKey}:${
|
|
22070
|
-
},
|
|
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
|
-
|
|
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:
|
|
22269
|
+
api.execute("getRoomConversation", { roomId: cornerId, limit: 200 }),
|
|
22091
22270
|
this.roster(),
|
|
22092
|
-
this.
|
|
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
|
|
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:
|
|
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.
|
|
22105
|
-
|
|
22106
|
-
|
|
22107
|
-
|
|
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
|
-
|
|
22116
|
-
|
|
22117
|
-
|
|
22118
|
-
|
|
22119
|
-
|
|
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:
|
|
22125
|
-
requestId
|
|
22126
|
-
label: `
|
|
22297
|
+
roomId: cornerId,
|
|
22298
|
+
requestId,
|
|
22299
|
+
label: `corner ${cornerId}`
|
|
22127
22300
|
});
|
|
22128
|
-
|
|
22129
|
-
|
|
22130
|
-
|
|
22131
|
-
|
|
22132
|
-
|
|
22133
|
-
|
|
22134
|
-
|
|
22135
|
-
|
|
22136
|
-
|
|
22137
|
-
|
|
22138
|
-
|
|
22139
|
-
|
|
22140
|
-
|
|
22141
|
-
|
|
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
|
-
|
|
22144
|
-
|
|
22145
|
-
|
|
22146
|
-
|
|
22147
|
-
|
|
22148
|
-
|
|
22149
|
-
|
|
22150
|
-
|
|
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
|
-
|
|
22153
|
-
|
|
22154
|
-
|
|
22155
|
-
|
|
22156
|
-
|
|
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]
|
|
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
|
-
|
|
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]
|
|
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
|
|
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]
|
|
22203
|
-
}
|
|
22204
|
-
if (openCornerCall && !isFailedToolCall(openCornerCall)) {
|
|
22205
|
-
reply = stripCornerOpenEcho(reply);
|
|
22436
|
+
console.warn(`[thin-core] corner ${cornerId} turn ${requestId}: ${explained.reason}`);
|
|
22206
22437
|
}
|
|
22207
|
-
|
|
22208
|
-
|
|
22209
|
-
|
|
22210
|
-
} : {})
|
|
22211
|
-
|
|
22212
|
-
|
|
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:
|
|
22216
|
-
requestId
|
|
22449
|
+
roomId: cornerId,
|
|
22450
|
+
requestId,
|
|
22217
22451
|
status: "complete",
|
|
22218
|
-
generationId: `${this.agent.publicKey}:${
|
|
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:
|
|
22226
|
-
requestId
|
|
22464
|
+
roomId: cornerId,
|
|
22465
|
+
requestId,
|
|
22227
22466
|
status: "failed",
|
|
22228
|
-
generationId: `${this.agent.publicKey}:${
|
|
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,
|
|
22239
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
22269
|
-
|
|
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
|
|
22272
|
-
const inbox = !pollNow ? { items: [], cursor: void 0 } : await api.execute("
|
|
22273
|
-
|
|
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
|
-
|
|
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 >
|
|
22637
|
+
while (processedInboxIds.size > INBOX_DEDUPLICATION_LIMIT)
|
|
22287
22638
|
processedInboxIds.delete(processedInboxIds.values().next().value);
|
|
22288
|
-
|
|
22639
|
+
const stoppedTurn = turnStopRequestId(item, this.agent.publicKey);
|
|
22640
|
+
if (stoppedTurn) {
|
|
22641
|
+
this.stopTurn(stoppedTurn);
|
|
22289
22642
|
continue;
|
|
22290
|
-
|
|
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?.(
|
|
22690
|
+
api.updateLiveCursor?.(cornerId, cursor3);
|
|
22310
22691
|
if (pollNow)
|
|
22311
|
-
this.options.
|
|
22312
|
-
|
|
22313
|
-
|
|
22314
|
-
|
|
22315
|
-
|
|
22316
|
-
|
|
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.
|
|
22324
|
-
console.error(`[thin-core]
|
|
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(
|
|
22332
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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") {
|