@sjawhar/claude-envoy 1.18.0 → 1.19.0
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/.claude-plugin/plugin.json +2 -2
- package/.omp-plugin/plugin.json +2 -2
- package/AGENTS.md +167 -42
- package/README.md +95 -177
- package/dist/dispatch.js +54 -6
- package/dist/envoy-channel.js +2413 -875
- package/dist/session-hook.js +54 -6
- package/hooks/claude-mod.d.ts +500 -0
- package/hooks/hooks.json +2 -1
- package/hooks/mod-protocol.ts +728 -0
- package/hooks/mod-writer.ts +506 -0
- package/hooks/mod.ts +716 -0
- package/package.json +2 -2
- package/scripts/smoke-clear-rebind.sh +1 -1
- package/scripts/smoke-session-title.sh +1 -1
- package/src/agent-stream-claude.ts +558 -0
- package/src/agent-stream-runner.ts +340 -0
- package/src/bounded-set.ts +54 -0
- package/src/channel-forwarder.ts +1 -1
- package/src/claude-rows.ts +195 -0
- package/src/envoy-channel-server.ts +394 -136
- package/src/envoy-off.ts +6 -1
- package/src/installed-build.ts +26 -0
- package/src/legion-inbox.ts +22 -11
- package/src/mod-link.ts +562 -0
- package/src/mod-session.ts +499 -0
- package/src/replay-state.ts +2 -18
- package/src/state-file.ts +39 -0
- package/src/user-turn-router.ts +496 -0
- package/src/user-turn-state.ts +134 -0
- package/scripts/smoke-channel.sh +0 -178
- package/src/claude-channel.ts +0 -705
package/dist/envoy-channel.js
CHANGED
|
@@ -36572,6 +36572,16 @@ function actorLabel(actor, titles) {
|
|
|
36572
36572
|
return `${actorName(actor, titles)}${actorAttribution(actor)}`;
|
|
36573
36573
|
}
|
|
36574
36574
|
// ../contracts/src/agent-stream.ts
|
|
36575
|
+
var AGENT_STREAM_SUBJECT_PREFIX = "agentstream.";
|
|
36576
|
+
var AGENT_STREAM_PROTOCOL = 1;
|
|
36577
|
+
function agentStreamFramesSubject(sessionID) {
|
|
36578
|
+
return `${AGENT_STREAM_SUBJECT_PREFIX}${sessionID}.frames`;
|
|
36579
|
+
}
|
|
36580
|
+
function agentStreamControlSubject(sessionID) {
|
|
36581
|
+
return `${AGENT_STREAM_SUBJECT_PREFIX}${sessionID}.control`;
|
|
36582
|
+
}
|
|
36583
|
+
var AGENT_STREAM_WATCH_TTL_MS = 30000;
|
|
36584
|
+
var AGENT_STREAM_WATCH_INTERVAL_MS = 1e4;
|
|
36575
36585
|
var AGENT_STREAM_LIMITS = {
|
|
36576
36586
|
partChars: 16000,
|
|
36577
36587
|
toolChars: 8000,
|
|
@@ -36583,6 +36593,16 @@ var AGENT_STREAM_LIMITS = {
|
|
|
36583
36593
|
commandNameChars: 100,
|
|
36584
36594
|
commandDescriptionChars: 200
|
|
36585
36595
|
};
|
|
36596
|
+
var AGENT_STREAM_TRUNCATION_SUFFIX = `
|
|
36597
|
+
\u2026 truncated`;
|
|
36598
|
+
function isTypableCommandName(name) {
|
|
36599
|
+
return /^[^\s\p{Cc}]+$/u.test(name);
|
|
36600
|
+
}
|
|
36601
|
+
function capAgentStreamText(text, limit) {
|
|
36602
|
+
if (text.length <= limit)
|
|
36603
|
+
return text;
|
|
36604
|
+
return text.slice(0, limit) + AGENT_STREAM_TRUNCATION_SUFFIX;
|
|
36605
|
+
}
|
|
36586
36606
|
// ../contracts/src/claim-holds.ts
|
|
36587
36607
|
var HOLDING_OF = {
|
|
36588
36608
|
gone: "lapsed",
|
|
@@ -36657,6 +36677,24 @@ function isIdPrefix(value) {
|
|
|
36657
36677
|
function pictureCaption(name) {
|
|
36658
36678
|
return name.replace(/[\\[\]`]/g, "\\$&").replace(/\s+/g, " ");
|
|
36659
36679
|
}
|
|
36680
|
+
var PICTURE_CAPTION = String.raw`(?:\\.|[^\\\]\n]){0,512}`;
|
|
36681
|
+
var PICTURE_ADDRESS = String.raw`dispatch://[^\s()]{1,2048}@v[1-9][0-9]{0,18}`;
|
|
36682
|
+
var PICTURE_TITLE = String.raw`(?:\s{1,4}"(?:\\.|[^\\"]){0,256}")?`;
|
|
36683
|
+
var PICTURE_SYNTAX = String.raw`!\[${PICTURE_CAPTION}\]\((${PICTURE_ADDRESS})${PICTURE_TITLE}\)`;
|
|
36684
|
+
function pictureSyntax() {
|
|
36685
|
+
return new RegExp(PICTURE_SYNTAX, "g");
|
|
36686
|
+
}
|
|
36687
|
+
function blockPlaceholder(type) {
|
|
36688
|
+
return `[${type}]`;
|
|
36689
|
+
}
|
|
36690
|
+
var PICTURE_PLACEHOLDER = blockPlaceholder("image");
|
|
36691
|
+
function trimmedLines(text) {
|
|
36692
|
+
return text.split(`
|
|
36693
|
+
`).map((line) => line.trim()).filter((line) => line !== "");
|
|
36694
|
+
}
|
|
36695
|
+
function comparableLines(text) {
|
|
36696
|
+
return trimmedLines(text.replace(pictureSyntax(), ""));
|
|
36697
|
+
}
|
|
36660
36698
|
var PICTURE_TYPES = {
|
|
36661
36699
|
"image/png": true,
|
|
36662
36700
|
"image/jpeg": true,
|
|
@@ -42699,9 +42737,9 @@ class DispatchClient {
|
|
|
42699
42737
|
async messageReply(id, input, options = {}) {
|
|
42700
42738
|
return this.#json("POST", ["api", "v1", "messages", id, "reply"], input, options.followUp === true ? { follow_up: "true" } : undefined);
|
|
42701
42739
|
}
|
|
42702
|
-
async deliveryReply(resource, id, input) {
|
|
42740
|
+
async deliveryReply(resource, id, input, options = {}) {
|
|
42703
42741
|
const collection = resource === "message" ? "messages" : "comments";
|
|
42704
|
-
await this.#json("POST", ["api", "v1", collection, id, "reply"], input);
|
|
42742
|
+
await this.#json("POST", ["api", "v1", collection, id, "reply"], input, options.followUp === true ? { follow_up: "true" } : undefined);
|
|
42705
42743
|
}
|
|
42706
42744
|
async getMessage(issue, id) {
|
|
42707
42745
|
return this.#json("GET", [
|
|
@@ -43221,6 +43259,9 @@ function toolImage(bytes) {
|
|
|
43221
43259
|
return;
|
|
43222
43260
|
return { data: Buffer.from(bytes).toString("base64"), mimeType };
|
|
43223
43261
|
}
|
|
43262
|
+
function imageBlocks(images) {
|
|
43263
|
+
return images.map(({ data, mimeType }) => ({ type: "image", data, mimeType }));
|
|
43264
|
+
}
|
|
43224
43265
|
function pictureLine(name, address) {
|
|
43225
43266
|
return ``;
|
|
43226
43267
|
}
|
|
@@ -43234,9 +43275,8 @@ function withPictureLines(text, lines) {
|
|
|
43234
43275
|
${lines.join(`
|
|
43235
43276
|
`)}`;
|
|
43236
43277
|
}
|
|
43237
|
-
var PICTURE_SYNTAX = /!\[(?:\\.|[^\\\]])*\]\((dispatch:\/\/[^\s()]+@v[1-9][0-9]*)(?:\s+"(?:\\.|[^\\"])*")?\)/g;
|
|
43238
43278
|
function pictureAddresses(text) {
|
|
43239
|
-
return [...text.matchAll(
|
|
43279
|
+
return [...text.matchAll(pictureSyntax())].map((match) => match[1]);
|
|
43240
43280
|
}
|
|
43241
43281
|
function picturesNewestFirst(texts) {
|
|
43242
43282
|
const ordered = texts.map((item, index) => ({ item, index, at: Date.parse(item.at) })).sort((left, right) => (Number.isNaN(right.at) ? 0 : right.at) - (Number.isNaN(left.at) ? 0 : left.at) || left.index - right.index);
|
|
@@ -43329,13 +43369,14 @@ function dispatchReplyPath(resource, id) {
|
|
|
43329
43369
|
return `/api/v1/${collection}/${encodeURIComponent(id)}/reply`;
|
|
43330
43370
|
}
|
|
43331
43371
|
async function postDeliveryReply(config, sessionId, delivery, result) {
|
|
43372
|
+
const { followUp, ...reply } = result;
|
|
43332
43373
|
const client = new DispatchClient(config.url, config.credential);
|
|
43333
43374
|
await client.deliveryReply(delivery.resource, delivery.id, {
|
|
43334
43375
|
actor: { kind: "session", id: sessionId },
|
|
43335
43376
|
attempt: delivery.attempt,
|
|
43336
43377
|
...delivery.replyFields,
|
|
43337
|
-
...
|
|
43338
|
-
});
|
|
43378
|
+
...reply
|
|
43379
|
+
}, followUp === undefined ? {} : { followUp });
|
|
43339
43380
|
}
|
|
43340
43381
|
function expectsLaneReceipt(frame) {
|
|
43341
43382
|
return frame.reply !== undefined && frame.reply !== "" && frame.subject === frame.directSubject && frame.envelopeTopic !== undefined && frame.envelopeTopic !== frame.directSubject;
|
|
@@ -46490,6 +46531,154 @@ function createConsumerDelivery(options) {
|
|
|
46490
46531
|
};
|
|
46491
46532
|
}
|
|
46492
46533
|
|
|
46534
|
+
// ../envoy-client/src/dispatch-config.ts
|
|
46535
|
+
var DEFAULT_SERVER_URL = "http://localhost:8766";
|
|
46536
|
+
function withoutTrailingSlashes2(url) {
|
|
46537
|
+
let end = url.length;
|
|
46538
|
+
while (end > 0 && url[end - 1] === "/")
|
|
46539
|
+
end -= 1;
|
|
46540
|
+
return url.slice(0, end);
|
|
46541
|
+
}
|
|
46542
|
+
function checkoutPair(checkout, serverUrl) {
|
|
46543
|
+
const { token } = checkout.dispatch;
|
|
46544
|
+
if (token === undefined) {
|
|
46545
|
+
throw new Error(`${checkout.path}: dispatch.serverUrl requires dispatch.token in the same file`);
|
|
46546
|
+
}
|
|
46547
|
+
return {
|
|
46548
|
+
kind: "file",
|
|
46549
|
+
token,
|
|
46550
|
+
field: "dispatch.token",
|
|
46551
|
+
path: checkout.path,
|
|
46552
|
+
server: serverUrl,
|
|
46553
|
+
checkout: true
|
|
46554
|
+
};
|
|
46555
|
+
}
|
|
46556
|
+
function writtenToken(config, checkout) {
|
|
46557
|
+
if (checkout?.dispatch.token !== undefined) {
|
|
46558
|
+
throw new Error(`${checkout.path}: dispatch.token requires dispatch.serverUrl in the same file`);
|
|
46559
|
+
}
|
|
46560
|
+
const own = config.dispatch;
|
|
46561
|
+
if (own.token === undefined)
|
|
46562
|
+
return;
|
|
46563
|
+
const server = own.serverUrl ?? DEFAULT_SERVER_URL;
|
|
46564
|
+
return {
|
|
46565
|
+
token: own.token,
|
|
46566
|
+
field: "dispatch.token",
|
|
46567
|
+
path: config.path,
|
|
46568
|
+
server,
|
|
46569
|
+
checkout: false
|
|
46570
|
+
};
|
|
46571
|
+
}
|
|
46572
|
+
function lostDispatchUrlNotice(env, config) {
|
|
46573
|
+
if (nonEmpty(env.DISPATCH_TOKEN_FILE) === undefined)
|
|
46574
|
+
return;
|
|
46575
|
+
const link = config.missing?.link ?? null;
|
|
46576
|
+
const old = config.missing?.oldUserFileNotice;
|
|
46577
|
+
return [
|
|
46578
|
+
"DISPATCH_TOKEN_FILE is set and DISPATCH_URL is not, so Dispatch is off: this shell lost the DISPATCH_URL it started with. Export DISPATCH_URL again, set to the Dispatch server that token is for",
|
|
46579
|
+
...link === null ? [] : [`${config.path} is a link to ${link}, which does not exist`],
|
|
46580
|
+
...old === undefined ? [] : [old]
|
|
46581
|
+
].join("; ");
|
|
46582
|
+
}
|
|
46583
|
+
function dispatchSettings(env, config, options = {}) {
|
|
46584
|
+
const target = dispatchTarget(env, config);
|
|
46585
|
+
if (!target.enabled)
|
|
46586
|
+
return target;
|
|
46587
|
+
const { url, source } = target;
|
|
46588
|
+
let credential;
|
|
46589
|
+
try {
|
|
46590
|
+
credential = source.kind === "broker" ? brokerCredential(env, options.session) : bearerCredential(source.kind === "variable" ? source.token : fileBearer(source, url, "DISPATCH", env, options.commands));
|
|
46591
|
+
} catch (error) {
|
|
46592
|
+
return offWithError(messageFor(error));
|
|
46593
|
+
}
|
|
46594
|
+
const publicUrl = nonEmpty(env.DISPATCH_PUBLIC_URL);
|
|
46595
|
+
const publicParsed = publicUrl === undefined ? undefined : httpUrl2(publicUrl);
|
|
46596
|
+
if (publicUrl !== undefined && (publicParsed === undefined || publicParsed.port === "0")) {
|
|
46597
|
+
return offWithError("DISPATCH_PUBLIC_URL must be an http or https URL");
|
|
46598
|
+
}
|
|
46599
|
+
return {
|
|
46600
|
+
enabled: true,
|
|
46601
|
+
url,
|
|
46602
|
+
credential,
|
|
46603
|
+
...publicUrl === undefined ? {} : { publicUrl: publicUrl.replace(/\/+$/, "") }
|
|
46604
|
+
};
|
|
46605
|
+
}
|
|
46606
|
+
function dispatchUrl(env, config) {
|
|
46607
|
+
const target = dispatchTarget(env, config);
|
|
46608
|
+
return target.enabled ? target.url : undefined;
|
|
46609
|
+
}
|
|
46610
|
+
function brokerRouteForced(env) {
|
|
46611
|
+
const written = (env.DISPATCH_TOKEN_BROKER ?? "").trim();
|
|
46612
|
+
if (written === "")
|
|
46613
|
+
return false;
|
|
46614
|
+
if (written === "1")
|
|
46615
|
+
return true;
|
|
46616
|
+
return offWithError(`DISPATCH_TOKEN_BROKER is "${written}" and takes 1: set it to 1 where this session's Dispatch credential is this machine's own identity traded at the secrets broker, and leave it unset where a bearer is read as usual`);
|
|
46617
|
+
}
|
|
46618
|
+
function dispatchTarget(env, config) {
|
|
46619
|
+
if (config.kind === "failed")
|
|
46620
|
+
return offWithError(config.error);
|
|
46621
|
+
const forced = brokerRouteForced(env);
|
|
46622
|
+
if (typeof forced !== "boolean")
|
|
46623
|
+
return forced;
|
|
46624
|
+
const explicitUrl = nonEmpty(env.DISPATCH_URL);
|
|
46625
|
+
const checkoutFile = explicitUrl === undefined ? config.checkout : null;
|
|
46626
|
+
if (checkoutFile?.kind === "invalid")
|
|
46627
|
+
return offWithError(checkoutFile.error);
|
|
46628
|
+
const checkout = checkoutFile?.kind === "valid" ? checkoutFile : null;
|
|
46629
|
+
const own = config.dispatch;
|
|
46630
|
+
if (own.enabled === false) {
|
|
46631
|
+
return offWithNotice(`${config.path} sets dispatch.enabled to false: Dispatch is off`);
|
|
46632
|
+
}
|
|
46633
|
+
if (checkout?.dispatch.enabled === false) {
|
|
46634
|
+
return offWithNotice(`${checkout.path} sets dispatch.enabled to false: Dispatch is off`);
|
|
46635
|
+
}
|
|
46636
|
+
const checkoutServer = checkout?.dispatch.serverUrl;
|
|
46637
|
+
const addressed = explicitUrl !== undefined || checkoutServer !== undefined || own.serverUrl !== undefined || own.enabled === true;
|
|
46638
|
+
if (!addressed) {
|
|
46639
|
+
const lost = lostDispatchUrlNotice(env, config);
|
|
46640
|
+
if (lost !== undefined)
|
|
46641
|
+
return offWithNotice(lost);
|
|
46642
|
+
return offWithNotice(config.missing === null ? `${config.path} names no Dispatch server, and DISPATCH_URL is not set: Dispatch is off` : missingConfigNotice(config.path, config.missing, "Dispatch is off"));
|
|
46643
|
+
}
|
|
46644
|
+
if (explicitUrl !== undefined && httpUrl2(explicitUrl) === undefined) {
|
|
46645
|
+
return offWithError("DISPATCH_URL must be an http or https URL");
|
|
46646
|
+
}
|
|
46647
|
+
let source = { kind: "none" };
|
|
46648
|
+
if (!forced) {
|
|
46649
|
+
try {
|
|
46650
|
+
source = checkout !== null && checkoutServer !== undefined ? checkoutPair(checkout, checkoutServer) : bearerSource(env, "DISPATCH", () => writtenToken(config, checkout));
|
|
46651
|
+
} catch (error) {
|
|
46652
|
+
return offWithError(messageFor(error));
|
|
46653
|
+
}
|
|
46654
|
+
}
|
|
46655
|
+
if (source.kind === "none") {
|
|
46656
|
+
let route;
|
|
46657
|
+
try {
|
|
46658
|
+
route = brokerRouteConfigured(env);
|
|
46659
|
+
} catch (error) {
|
|
46660
|
+
return offWithError(messageFor(error));
|
|
46661
|
+
}
|
|
46662
|
+
if (route.traded) {
|
|
46663
|
+
const brokered = withoutTrailingSlashes2(explicitUrl ?? own.serverUrl ?? DEFAULT_SERVER_URL);
|
|
46664
|
+
return { enabled: true, url: brokered, source: { kind: "broker" } };
|
|
46665
|
+
}
|
|
46666
|
+
if (forced) {
|
|
46667
|
+
return offWithError(`DISPATCH_TOKEN_BROKER is set, so this session's Dispatch credential is this machine's own identity traded at the secrets broker, and ${route.gap}: Dispatch is off rather than written with whatever bearer this box holds`);
|
|
46668
|
+
}
|
|
46669
|
+
return offWithError(explicitUrl === undefined ? `${config.path}: dispatch is enabled and no token is: set dispatch.token, or DISPATCH_TOKEN or DISPATCH_TOKEN_FILE` : `DISPATCH_URL is set and no token is: set DISPATCH_TOKEN or DISPATCH_TOKEN_FILE, or dispatch.token in ${config.path}`);
|
|
46670
|
+
}
|
|
46671
|
+
const url = (explicitUrl ?? (source.kind === "file" ? source.server : own.serverUrl ?? DEFAULT_SERVER_URL)).replace(/\/+$/, "");
|
|
46672
|
+
return { enabled: true, url, source };
|
|
46673
|
+
}
|
|
46674
|
+
function activeDispatchSettings(resolution) {
|
|
46675
|
+
if (resolution.enabled)
|
|
46676
|
+
return resolution;
|
|
46677
|
+
if (resolution.error !== null)
|
|
46678
|
+
throw new Error(`dispatch config: ${resolution.error}`);
|
|
46679
|
+
return null;
|
|
46680
|
+
}
|
|
46681
|
+
|
|
46493
46682
|
// ../envoy-client/src/dispatch-session-directory.ts
|
|
46494
46683
|
import { randomUUID } from "crypto";
|
|
46495
46684
|
import { renameSync, rmSync as rmSync2, writeFileSync } from "fs";
|
|
@@ -51310,164 +51499,23 @@ function natsConnectionOptions(input) {
|
|
|
51310
51499
|
};
|
|
51311
51500
|
}
|
|
51312
51501
|
|
|
51313
|
-
// ../envoy-client/src/
|
|
51314
|
-
|
|
51315
|
-
|
|
51316
|
-
|
|
51317
|
-
|
|
51318
|
-
|
|
51319
|
-
return url.slice(0, end);
|
|
51320
|
-
}
|
|
51321
|
-
function checkoutPair(checkout, serverUrl) {
|
|
51322
|
-
const { token } = checkout.dispatch;
|
|
51323
|
-
if (token === undefined) {
|
|
51324
|
-
throw new Error(`${checkout.path}: dispatch.serverUrl requires dispatch.token in the same file`);
|
|
51325
|
-
}
|
|
51326
|
-
return {
|
|
51327
|
-
kind: "file",
|
|
51328
|
-
token,
|
|
51329
|
-
field: "dispatch.token",
|
|
51330
|
-
path: checkout.path,
|
|
51331
|
-
server: serverUrl,
|
|
51332
|
-
checkout: true
|
|
51333
|
-
};
|
|
51334
|
-
}
|
|
51335
|
-
function writtenToken(config, checkout) {
|
|
51336
|
-
if (checkout?.dispatch.token !== undefined) {
|
|
51337
|
-
throw new Error(`${checkout.path}: dispatch.token requires dispatch.serverUrl in the same file`);
|
|
51502
|
+
// ../envoy-client/src/session-settings.ts
|
|
51503
|
+
function sessionConfigFile(harness, env) {
|
|
51504
|
+
try {
|
|
51505
|
+
return { path: envoyConfigPath(harness, env), error: null };
|
|
51506
|
+
} catch (error) {
|
|
51507
|
+
return { path: null, error: messageFor(error) };
|
|
51338
51508
|
}
|
|
51339
|
-
const own = config.dispatch;
|
|
51340
|
-
if (own.token === undefined)
|
|
51341
|
-
return;
|
|
51342
|
-
const server = own.serverUrl ?? DEFAULT_SERVER_URL;
|
|
51343
|
-
return {
|
|
51344
|
-
token: own.token,
|
|
51345
|
-
field: "dispatch.token",
|
|
51346
|
-
path: config.path,
|
|
51347
|
-
server,
|
|
51348
|
-
checkout: false
|
|
51349
|
-
};
|
|
51350
|
-
}
|
|
51351
|
-
function lostDispatchUrlNotice(env, config) {
|
|
51352
|
-
if (nonEmpty(env.DISPATCH_TOKEN_FILE) === undefined)
|
|
51353
|
-
return;
|
|
51354
|
-
const link = config.missing?.link ?? null;
|
|
51355
|
-
const old = config.missing?.oldUserFileNotice;
|
|
51356
|
-
return [
|
|
51357
|
-
"DISPATCH_TOKEN_FILE is set and DISPATCH_URL is not, so Dispatch is off: this shell lost the DISPATCH_URL it started with. Export DISPATCH_URL again, set to the Dispatch server that token is for",
|
|
51358
|
-
...link === null ? [] : [`${config.path} is a link to ${link}, which does not exist`],
|
|
51359
|
-
...old === undefined ? [] : [old]
|
|
51360
|
-
].join("; ");
|
|
51361
51509
|
}
|
|
51362
|
-
function
|
|
51363
|
-
const target = dispatchTarget(env, config);
|
|
51364
|
-
if (!target.enabled)
|
|
51365
|
-
return target;
|
|
51366
|
-
const { url, source } = target;
|
|
51367
|
-
let credential;
|
|
51510
|
+
function handedConfigFile(env) {
|
|
51368
51511
|
try {
|
|
51369
|
-
|
|
51512
|
+
const handed = handedConfigPath(env);
|
|
51513
|
+
return handed === undefined ? null : { path: handed, error: null };
|
|
51370
51514
|
} catch (error) {
|
|
51371
|
-
return
|
|
51372
|
-
}
|
|
51373
|
-
const publicUrl = nonEmpty(env.DISPATCH_PUBLIC_URL);
|
|
51374
|
-
const publicParsed = publicUrl === undefined ? undefined : httpUrl2(publicUrl);
|
|
51375
|
-
if (publicUrl !== undefined && (publicParsed === undefined || publicParsed.port === "0")) {
|
|
51376
|
-
return offWithError("DISPATCH_PUBLIC_URL must be an http or https URL");
|
|
51515
|
+
return { path: null, error: messageFor(error) };
|
|
51377
51516
|
}
|
|
51378
|
-
return {
|
|
51379
|
-
enabled: true,
|
|
51380
|
-
url,
|
|
51381
|
-
credential,
|
|
51382
|
-
...publicUrl === undefined ? {} : { publicUrl: publicUrl.replace(/\/+$/, "") }
|
|
51383
|
-
};
|
|
51384
|
-
}
|
|
51385
|
-
function dispatchUrl(env, config) {
|
|
51386
|
-
const target = dispatchTarget(env, config);
|
|
51387
|
-
return target.enabled ? target.url : undefined;
|
|
51388
|
-
}
|
|
51389
|
-
function brokerRouteForced(env) {
|
|
51390
|
-
const written = (env.DISPATCH_TOKEN_BROKER ?? "").trim();
|
|
51391
|
-
if (written === "")
|
|
51392
|
-
return false;
|
|
51393
|
-
if (written === "1")
|
|
51394
|
-
return true;
|
|
51395
|
-
return offWithError(`DISPATCH_TOKEN_BROKER is "${written}" and takes 1: set it to 1 where this session's Dispatch credential is this machine's own identity traded at the secrets broker, and leave it unset where a bearer is read as usual`);
|
|
51396
51517
|
}
|
|
51397
|
-
function
|
|
51398
|
-
if (config.kind === "failed")
|
|
51399
|
-
return offWithError(config.error);
|
|
51400
|
-
const forced = brokerRouteForced(env);
|
|
51401
|
-
if (typeof forced !== "boolean")
|
|
51402
|
-
return forced;
|
|
51403
|
-
const explicitUrl = nonEmpty(env.DISPATCH_URL);
|
|
51404
|
-
const checkoutFile = explicitUrl === undefined ? config.checkout : null;
|
|
51405
|
-
if (checkoutFile?.kind === "invalid")
|
|
51406
|
-
return offWithError(checkoutFile.error);
|
|
51407
|
-
const checkout = checkoutFile?.kind === "valid" ? checkoutFile : null;
|
|
51408
|
-
const own = config.dispatch;
|
|
51409
|
-
if (own.enabled === false) {
|
|
51410
|
-
return offWithNotice(`${config.path} sets dispatch.enabled to false: Dispatch is off`);
|
|
51411
|
-
}
|
|
51412
|
-
if (checkout?.dispatch.enabled === false) {
|
|
51413
|
-
return offWithNotice(`${checkout.path} sets dispatch.enabled to false: Dispatch is off`);
|
|
51414
|
-
}
|
|
51415
|
-
const checkoutServer = checkout?.dispatch.serverUrl;
|
|
51416
|
-
const addressed = explicitUrl !== undefined || checkoutServer !== undefined || own.serverUrl !== undefined || own.enabled === true;
|
|
51417
|
-
if (!addressed) {
|
|
51418
|
-
const lost = lostDispatchUrlNotice(env, config);
|
|
51419
|
-
if (lost !== undefined)
|
|
51420
|
-
return offWithNotice(lost);
|
|
51421
|
-
return offWithNotice(config.missing === null ? `${config.path} names no Dispatch server, and DISPATCH_URL is not set: Dispatch is off` : missingConfigNotice(config.path, config.missing, "Dispatch is off"));
|
|
51422
|
-
}
|
|
51423
|
-
if (explicitUrl !== undefined && httpUrl2(explicitUrl) === undefined) {
|
|
51424
|
-
return offWithError("DISPATCH_URL must be an http or https URL");
|
|
51425
|
-
}
|
|
51426
|
-
let source = { kind: "none" };
|
|
51427
|
-
if (!forced) {
|
|
51428
|
-
try {
|
|
51429
|
-
source = checkout !== null && checkoutServer !== undefined ? checkoutPair(checkout, checkoutServer) : bearerSource(env, "DISPATCH", () => writtenToken(config, checkout));
|
|
51430
|
-
} catch (error) {
|
|
51431
|
-
return offWithError(messageFor(error));
|
|
51432
|
-
}
|
|
51433
|
-
}
|
|
51434
|
-
if (source.kind === "none") {
|
|
51435
|
-
let route;
|
|
51436
|
-
try {
|
|
51437
|
-
route = brokerRouteConfigured(env);
|
|
51438
|
-
} catch (error) {
|
|
51439
|
-
return offWithError(messageFor(error));
|
|
51440
|
-
}
|
|
51441
|
-
if (route.traded) {
|
|
51442
|
-
const brokered = withoutTrailingSlashes2(explicitUrl ?? own.serverUrl ?? DEFAULT_SERVER_URL);
|
|
51443
|
-
return { enabled: true, url: brokered, source: { kind: "broker" } };
|
|
51444
|
-
}
|
|
51445
|
-
if (forced) {
|
|
51446
|
-
return offWithError(`DISPATCH_TOKEN_BROKER is set, so this session's Dispatch credential is this machine's own identity traded at the secrets broker, and ${route.gap}: Dispatch is off rather than written with whatever bearer this box holds`);
|
|
51447
|
-
}
|
|
51448
|
-
return offWithError(explicitUrl === undefined ? `${config.path}: dispatch is enabled and no token is: set dispatch.token, or DISPATCH_TOKEN or DISPATCH_TOKEN_FILE` : `DISPATCH_URL is set and no token is: set DISPATCH_TOKEN or DISPATCH_TOKEN_FILE, or dispatch.token in ${config.path}`);
|
|
51449
|
-
}
|
|
51450
|
-
const url = (explicitUrl ?? (source.kind === "file" ? source.server : own.serverUrl ?? DEFAULT_SERVER_URL)).replace(/\/+$/, "");
|
|
51451
|
-
return { enabled: true, url, source };
|
|
51452
|
-
}
|
|
51453
|
-
|
|
51454
|
-
// ../envoy-client/src/session-settings.ts
|
|
51455
|
-
function sessionConfigFile(harness, env) {
|
|
51456
|
-
try {
|
|
51457
|
-
return { path: envoyConfigPath(harness, env), error: null };
|
|
51458
|
-
} catch (error) {
|
|
51459
|
-
return { path: null, error: messageFor(error) };
|
|
51460
|
-
}
|
|
51461
|
-
}
|
|
51462
|
-
function handedConfigFile(env) {
|
|
51463
|
-
try {
|
|
51464
|
-
const handed = handedConfigPath(env);
|
|
51465
|
-
return handed === undefined ? null : { path: handed, error: null };
|
|
51466
|
-
} catch (error) {
|
|
51467
|
-
return { path: null, error: messageFor(error) };
|
|
51468
|
-
}
|
|
51469
|
-
}
|
|
51470
|
-
function startNotice(config, dispatch, envoy, nats, lost) {
|
|
51518
|
+
function startNotice(config, dispatch, envoy, nats, lost) {
|
|
51471
51519
|
if (config.kind === "failed")
|
|
51472
51520
|
return `${config.error}: Dispatch and Envoy are off`;
|
|
51473
51521
|
const { missing, checkout } = config;
|
|
@@ -55214,604 +55262,953 @@ var import_nats2 = __toESM(require_mod4(), 1);
|
|
|
55214
55262
|
|
|
55215
55263
|
// package.json
|
|
55216
55264
|
var name = "@sjawhar/claude-envoy";
|
|
55217
|
-
var version2 = "1.
|
|
55265
|
+
var version2 = "1.19.0";
|
|
55218
55266
|
var legion = {
|
|
55219
55267
|
daemonApiVersion: 56
|
|
55220
55268
|
};
|
|
55221
55269
|
|
|
55222
|
-
// src/
|
|
55223
|
-
var
|
|
55224
|
-
|
|
55225
|
-
|
|
55226
|
-
|
|
55227
|
-
|
|
55228
|
-
|
|
55229
|
-
|
|
55230
|
-
|
|
55231
|
-
var DEFAULT_DRAIN_TIMEOUT_MS = 1000;
|
|
55232
|
-
function deliveryIdentity(raw) {
|
|
55233
|
-
let parsed;
|
|
55234
|
-
try {
|
|
55235
|
-
parsed = JSON.parse(raw);
|
|
55236
|
-
} catch {
|
|
55270
|
+
// ../pi-shared/src/agent-stream.ts
|
|
55271
|
+
var encoder4 = new TextEncoder;
|
|
55272
|
+
var REPLAY_ARRAY_BYTES = 2;
|
|
55273
|
+
function jsonBytes(value) {
|
|
55274
|
+
return encoder4.encode(JSON.stringify(value)).length;
|
|
55275
|
+
}
|
|
55276
|
+
var SEQ_PER_MS = 1000;
|
|
55277
|
+
function modelReference(provider, id) {
|
|
55278
|
+
if (typeof id !== "string" || id === "")
|
|
55237
55279
|
return;
|
|
55238
|
-
|
|
55239
|
-
|
|
55240
|
-
return
|
|
55280
|
+
if (typeof provider !== "string" || provider === "")
|
|
55281
|
+
return;
|
|
55282
|
+
return `${provider}/${id}`;
|
|
55241
55283
|
}
|
|
55242
|
-
function
|
|
55243
|
-
|
|
55244
|
-
|
|
55284
|
+
function readModel(message) {
|
|
55285
|
+
const model = modelReference(message.provider, message.model);
|
|
55286
|
+
return model === undefined ? undefined : capAgentStreamText(model, AGENT_STREAM_LIMITS.modelChars);
|
|
55245
55287
|
}
|
|
55246
|
-
function
|
|
55247
|
-
|
|
55248
|
-
|
|
55249
|
-
const
|
|
55250
|
-
|
|
55251
|
-
|
|
55252
|
-
for await (const { message, receivedAt } of readArrivals(subscription)) {
|
|
55253
|
-
const raw = decoder5.decode(message.data);
|
|
55254
|
-
const identity = deliveryIdentity(raw);
|
|
55255
|
-
let claim = options.relayed?.(topic) === true ? undefined : dedupe.claim(identity);
|
|
55256
|
-
if (claim !== undefined && identity !== undefined && options.ledger?.delivered(identity)) {
|
|
55257
|
-
claim.settle();
|
|
55258
|
-
claim = undefined;
|
|
55259
|
-
}
|
|
55260
|
-
const inbound = {
|
|
55261
|
-
subject: message.subject,
|
|
55262
|
-
data: message.data,
|
|
55263
|
-
raw,
|
|
55264
|
-
...message.reply ? { reply: message.reply } : {},
|
|
55265
|
-
...identity?.topic === undefined ? {} : { envelopeTopic: identity.topic },
|
|
55266
|
-
...claim === undefined ? { duplicate: true } : {}
|
|
55267
|
-
};
|
|
55268
|
-
let handed;
|
|
55269
|
-
try {
|
|
55270
|
-
handed = await options.deliver(inbound);
|
|
55271
|
-
} catch (error) {
|
|
55272
|
-
claim?.release();
|
|
55273
|
-
report(`could not deliver a message on ${message.subject}`, error);
|
|
55274
|
-
if (claim !== undefined)
|
|
55275
|
-
options.failed?.(inbound, topic, error, receivedAt);
|
|
55276
|
-
continue;
|
|
55277
|
-
}
|
|
55278
|
-
if (claim === undefined)
|
|
55279
|
-
continue;
|
|
55280
|
-
if (!handed) {
|
|
55281
|
-
claim.release();
|
|
55282
|
-
continue;
|
|
55283
|
-
}
|
|
55284
|
-
const taken = claim;
|
|
55285
|
-
const settle = () => taken.settle();
|
|
55286
|
-
if (identity === undefined || options.ledger === undefined)
|
|
55287
|
-
settle();
|
|
55288
|
-
else
|
|
55289
|
-
options.ledger.recordDelivered(identity).then(settle, settle);
|
|
55290
|
-
}
|
|
55291
|
-
} catch (error) {
|
|
55292
|
-
report(`delivery from ${topic} stopped`, error);
|
|
55293
|
-
}
|
|
55294
|
-
};
|
|
55295
|
-
const stop = async (topics) => {
|
|
55296
|
-
const stopping = topics.flatMap((topic) => {
|
|
55297
|
-
const entry = following.get(topic);
|
|
55298
|
-
if (entry === undefined)
|
|
55299
|
-
return [];
|
|
55300
|
-
following.delete(topic);
|
|
55301
|
-
entry.subscription.unsubscribe();
|
|
55302
|
-
return [entry.done];
|
|
55303
|
-
});
|
|
55304
|
-
await Promise.all(stopping);
|
|
55305
|
-
};
|
|
55288
|
+
function readMessage(raw) {
|
|
55289
|
+
if (typeof raw !== "object" || raw === null)
|
|
55290
|
+
return null;
|
|
55291
|
+
const message = raw;
|
|
55292
|
+
if (typeof message.role !== "string" || typeof message.timestamp !== "number")
|
|
55293
|
+
return null;
|
|
55306
55294
|
return {
|
|
55307
|
-
|
|
55308
|
-
|
|
55309
|
-
|
|
55310
|
-
|
|
55311
|
-
|
|
55312
|
-
|
|
55313
|
-
|
|
55314
|
-
|
|
55315
|
-
|
|
55316
|
-
|
|
55317
|
-
|
|
55318
|
-
|
|
55319
|
-
|
|
55320
|
-
|
|
55321
|
-
|
|
55322
|
-
|
|
55323
|
-
|
|
55324
|
-
|
|
55325
|
-
|
|
55326
|
-
if (connection.isClosed())
|
|
55327
|
-
return;
|
|
55328
|
-
const timeout = Promise.withResolvers();
|
|
55329
|
-
const timer = setTimeout(timeout.resolve, drainTimeoutMs);
|
|
55330
|
-
try {
|
|
55331
|
-
await Promise.race([connection.drain(), timeout.promise]);
|
|
55332
|
-
} catch (error) {
|
|
55333
|
-
report("draining the broker connection failed", error);
|
|
55334
|
-
}
|
|
55335
|
-
clearTimeout(timer);
|
|
55336
|
-
if (connection.isClosed())
|
|
55337
|
-
return;
|
|
55338
|
-
try {
|
|
55339
|
-
await connection.close();
|
|
55340
|
-
} catch (error) {
|
|
55341
|
-
report("closing the broker connection failed", error);
|
|
55342
|
-
}
|
|
55343
|
-
}
|
|
55344
|
-
};
|
|
55295
|
+
host: message,
|
|
55296
|
+
isError: message.isError === true,
|
|
55297
|
+
model: readModel(message),
|
|
55298
|
+
role: message.role,
|
|
55299
|
+
timestamp: message.timestamp,
|
|
55300
|
+
toolCallId: typeof message.toolCallId === "string" ? message.toolCallId : undefined,
|
|
55301
|
+
toolName: typeof message.toolName === "string" ? message.toolName : undefined,
|
|
55302
|
+
tag: typeof message.tag === "string" ? message.tag : undefined
|
|
55303
|
+
};
|
|
55304
|
+
}
|
|
55305
|
+
function keyFor(message) {
|
|
55306
|
+
if (message.role === "toolResult") {
|
|
55307
|
+
return message.toolCallId === undefined ? null : `t:${message.toolCallId}`;
|
|
55308
|
+
}
|
|
55309
|
+
if (message.role === "assistant")
|
|
55310
|
+
return `m:a${message.timestamp}`;
|
|
55311
|
+
if (message.role !== "user")
|
|
55312
|
+
return null;
|
|
55313
|
+
return message.tag === undefined ? `m:u${message.timestamp}` : `m:u${message.timestamp}:${message.tag}`;
|
|
55345
55314
|
}
|
|
55346
|
-
|
|
55347
|
-
|
|
55348
|
-
|
|
55349
|
-
|
|
55350
|
-
|
|
55351
|
-
|
|
55352
|
-
|
|
55353
|
-
|
|
55354
|
-
|
|
55355
|
-
|
|
55356
|
-
|
|
55357
|
-
|
|
55358
|
-
|
|
55359
|
-
|
|
55360
|
-
const at = spec.indexOf("@");
|
|
55361
|
-
if (at <= 0 || at === spec.length - 1)
|
|
55362
|
-
return;
|
|
55363
|
-
return { kind: "plugin", name: spec.slice(0, at), marketplace: spec.slice(at + 1), dev };
|
|
55364
|
-
}
|
|
55365
|
-
if (value.startsWith("server:") && value.length > "server:".length) {
|
|
55366
|
-
return { kind: "server", name: value.slice("server:".length), dev };
|
|
55315
|
+
function contentText(content) {
|
|
55316
|
+
if (typeof content === "string")
|
|
55317
|
+
return content;
|
|
55318
|
+
if (!Array.isArray(content))
|
|
55319
|
+
return "";
|
|
55320
|
+
const pieces = [];
|
|
55321
|
+
for (const raw of content) {
|
|
55322
|
+
if (typeof raw !== "object" || raw === null)
|
|
55323
|
+
continue;
|
|
55324
|
+
const part = raw;
|
|
55325
|
+
if (typeof part.text === "string")
|
|
55326
|
+
pieces.push(part.text);
|
|
55327
|
+
else if (typeof part.type === "string")
|
|
55328
|
+
pieces.push(blockPlaceholder(part.type));
|
|
55367
55329
|
}
|
|
55368
|
-
return
|
|
55330
|
+
return pieces.join(`
|
|
55331
|
+
`);
|
|
55369
55332
|
}
|
|
55370
|
-
|
|
55371
|
-
"
|
|
55372
|
-
"--prefill-b64",
|
|
55373
|
-
"--deep-link-repo",
|
|
55374
|
-
"--deep-link-last-fetch",
|
|
55375
|
-
"--deep-link-cwd-b64",
|
|
55376
|
-
"--handle-uri",
|
|
55377
|
-
"--settings",
|
|
55378
|
-
"--managed-settings",
|
|
55379
|
-
"--setting-sources",
|
|
55380
|
-
"--client-data-url",
|
|
55381
|
-
"--watch-artifact",
|
|
55382
|
-
"--watch-artifact-no-autoreact",
|
|
55383
|
-
"--team-name",
|
|
55384
|
-
"--agent-id",
|
|
55385
|
-
"--agent-name",
|
|
55386
|
-
"--agent-color",
|
|
55387
|
-
"--parent-session-id",
|
|
55388
|
-
"--agent-type",
|
|
55389
|
-
"--model",
|
|
55390
|
-
"--agent",
|
|
55391
|
-
"--routine",
|
|
55392
|
-
"--effort",
|
|
55393
|
-
"--fast-mode",
|
|
55394
|
-
"--permission-mode",
|
|
55395
|
-
"--inherit-permission-mode",
|
|
55396
|
-
"--proactivity",
|
|
55397
|
-
"--debug-file",
|
|
55398
|
-
"--system-prompt",
|
|
55399
|
-
"--system-prompt-file",
|
|
55400
|
-
"--append-system-prompt",
|
|
55401
|
-
"--append-system-prompt-file",
|
|
55402
|
-
"--system-prompt-snapshot",
|
|
55403
|
-
"--append-subagent-system-prompt",
|
|
55404
|
-
"--append-subagent-system-prompt-file",
|
|
55405
|
-
"--plan-mode-instructions",
|
|
55406
|
-
"--permission-prompt-tool",
|
|
55407
|
-
"--permission-prompts",
|
|
55408
|
-
"--json-schema",
|
|
55409
|
-
"--fallback-model",
|
|
55410
|
-
"--advisor",
|
|
55411
|
-
"--agents",
|
|
55412
|
-
"--name",
|
|
55413
|
-
"-n",
|
|
55414
|
-
"--allowedTools",
|
|
55415
|
-
"--allowed-tools",
|
|
55416
|
-
"--disallowedTools",
|
|
55417
|
-
"--disallowed-tools",
|
|
55418
|
-
"--tools",
|
|
55419
|
-
"--add-dir",
|
|
55420
|
-
"--mcp-config",
|
|
55421
|
-
"--betas",
|
|
55422
|
-
"--file",
|
|
55423
|
-
"--channels",
|
|
55424
|
-
"--dangerously-load-development-channels",
|
|
55425
|
-
"--plugin-dir",
|
|
55426
|
-
"--plugin-dir-no-mcp",
|
|
55427
|
-
"--plugin-url",
|
|
55428
|
-
"--remote-control-session-name-prefix",
|
|
55429
|
-
"--exec",
|
|
55430
|
-
"-m",
|
|
55431
|
-
"--sdk-url",
|
|
55432
|
-
"--thinking",
|
|
55433
|
-
"--thinking-display",
|
|
55434
|
-
"--max-thinking-tokens",
|
|
55435
|
-
"--max-turns",
|
|
55436
|
-
"--max-budget-usd",
|
|
55437
|
-
"--task-budget",
|
|
55438
|
-
"--autocompact",
|
|
55439
|
-
"--rewind-files",
|
|
55440
|
-
"--resume-session-at",
|
|
55441
|
-
"--resume-drops-turn",
|
|
55442
|
-
"--workload",
|
|
55443
|
-
"--output-format",
|
|
55444
|
-
"--input-format",
|
|
55445
|
-
"--teammate-mode",
|
|
55446
|
-
"--messaging-socket-path",
|
|
55447
|
-
"--session-id",
|
|
55448
|
-
"--environment",
|
|
55449
|
-
"--pool",
|
|
55450
|
-
"--ref",
|
|
55451
|
-
"--on-branch",
|
|
55452
|
-
"--correlation-id",
|
|
55453
|
-
"--forward-home-settings",
|
|
55454
|
-
"--project-config-root",
|
|
55455
|
-
"--attach-serve"
|
|
55456
|
-
]);
|
|
55457
|
-
var OPTIONAL_VALUE_OPTIONS = new Set([
|
|
55458
|
-
"-d",
|
|
55459
|
-
"--debug",
|
|
55460
|
-
"-r",
|
|
55461
|
-
"--resume",
|
|
55462
|
-
"--from-pr",
|
|
55463
|
-
"-w",
|
|
55464
|
-
"--worktree",
|
|
55465
|
-
"--teleport",
|
|
55466
|
-
"--cloud",
|
|
55467
|
-
"--remote",
|
|
55468
|
-
"--project",
|
|
55469
|
-
"--remote-control",
|
|
55470
|
-
"--rc",
|
|
55471
|
-
"--prompt-suggestions"
|
|
55472
|
-
]);
|
|
55473
|
-
var MULTI_VALUE_OPTIONS = new Set([
|
|
55474
|
-
"--allowedTools",
|
|
55475
|
-
"--allowed-tools",
|
|
55476
|
-
"--disallowedTools",
|
|
55477
|
-
"--disallowed-tools",
|
|
55478
|
-
"--tools",
|
|
55479
|
-
"--add-dir",
|
|
55480
|
-
"--mcp-config",
|
|
55481
|
-
"--betas",
|
|
55482
|
-
"--file",
|
|
55483
|
-
"--channels",
|
|
55484
|
-
"--dangerously-load-development-channels"
|
|
55485
|
-
]);
|
|
55486
|
-
function isOption(argument) {
|
|
55487
|
-
return argument.length > 1 && argument.startsWith("-");
|
|
55488
|
-
}
|
|
55489
|
-
function lastValueIndex(argv, index) {
|
|
55490
|
-
const argument = argv[index] ?? "";
|
|
55491
|
-
if (OPTIONAL_VALUE_OPTIONS.has(argument)) {
|
|
55492
|
-
const next = argv[index + 1];
|
|
55493
|
-
return next !== undefined && !isOption(next) ? index + 1 : index;
|
|
55494
|
-
}
|
|
55495
|
-
if (!VALUE_OPTIONS.has(argument))
|
|
55496
|
-
return index;
|
|
55497
|
-
let last = index + 1;
|
|
55498
|
-
if (MULTI_VALUE_OPTIONS.has(argument)) {
|
|
55499
|
-
while (last + 1 < argv.length && !isOption(argv[last + 1] ?? ""))
|
|
55500
|
-
last++;
|
|
55501
|
-
}
|
|
55502
|
-
return last;
|
|
55503
|
-
}
|
|
55504
|
-
function asksForPrintMode(argv) {
|
|
55505
|
-
for (let index = 1;index < argv.length; index++) {
|
|
55506
|
-
const argument = argv[index] ?? "";
|
|
55507
|
-
if (argument === "--")
|
|
55508
|
-
return false;
|
|
55509
|
-
if (argument === "-p" || argument === "--print" || argument === "--init-only" || argument.startsWith("--sdk-url")) {
|
|
55510
|
-
return true;
|
|
55511
|
-
}
|
|
55512
|
-
index = lastValueIndex(argv, index);
|
|
55513
|
-
}
|
|
55514
|
-
return false;
|
|
55333
|
+
function boundedStrings(_key, value) {
|
|
55334
|
+
return typeof value === "string" ? capAgentStreamText(value, AGENT_STREAM_LIMITS.toolChars) : value;
|
|
55515
55335
|
}
|
|
55516
|
-
function
|
|
55517
|
-
|
|
55518
|
-
|
|
55519
|
-
|
|
55520
|
-
|
|
55521
|
-
|
|
55522
|
-
|
|
55523
|
-
break;
|
|
55524
|
-
const equals = argument.indexOf("=");
|
|
55525
|
-
const flag = equals === -1 ? argument : argument.slice(0, equals);
|
|
55526
|
-
const dev = CHANNEL_FLAGS[flag];
|
|
55527
|
-
if (dev === undefined) {
|
|
55528
|
-
if (exact)
|
|
55529
|
-
index = lastValueIndex(argv, index);
|
|
55530
|
-
continue;
|
|
55531
|
-
}
|
|
55532
|
-
const values = [];
|
|
55533
|
-
if (equals !== -1)
|
|
55534
|
-
values.push(argument.slice(equals + 1));
|
|
55535
|
-
else {
|
|
55536
|
-
while (index + 1 < argv.length && !isOption(argv[index + 1] ?? "-")) {
|
|
55537
|
-
values.push(argv[index + 1] ?? "");
|
|
55538
|
-
index++;
|
|
55539
|
-
}
|
|
55540
|
-
}
|
|
55541
|
-
if (dev && !interactive)
|
|
55542
|
-
continue;
|
|
55543
|
-
for (const value of values) {
|
|
55544
|
-
const entry = parseEntry(value, dev);
|
|
55545
|
-
if (entry !== undefined)
|
|
55546
|
-
(dev ? development : approved).push(entry);
|
|
55547
|
-
}
|
|
55336
|
+
function renderArguments(value) {
|
|
55337
|
+
if (value === undefined || value === null)
|
|
55338
|
+
return "";
|
|
55339
|
+
try {
|
|
55340
|
+
return JSON.stringify(value, boundedStrings, 2) ?? "";
|
|
55341
|
+
} catch {
|
|
55342
|
+
return "[unserialisable arguments]";
|
|
55548
55343
|
}
|
|
55549
|
-
return [...approved, ...development];
|
|
55550
55344
|
}
|
|
55551
|
-
function
|
|
55552
|
-
const {
|
|
55553
|
-
if (
|
|
55554
|
-
return {
|
|
55555
|
-
const ours = plugin === undefined ? "server:<name>" : `plugin:${plugin.name}@${plugin.marketplace ?? "<marketplace>"}`;
|
|
55556
|
-
const entry = claudeChannelEntries(commandLine.argv, commandLine.exact, commandLine.stdoutIsTerminal).find((candidate) => plugin === undefined ? candidate.kind === "server" : candidate.kind === "plugin" && candidate.name === plugin.name || candidate.kind === "server" && candidate.name === `plugin:${plugin.name}:${PLUGIN_SERVER}`);
|
|
55557
|
-
if (entry === undefined) {
|
|
55558
|
-
return {
|
|
55559
|
-
loaded: false,
|
|
55560
|
-
why: `claude was started without --channels ${ours} (or --dangerously-load-development-channels ${ours})`
|
|
55561
|
-
};
|
|
55562
|
-
}
|
|
55563
|
-
if (entry.kind === "server") {
|
|
55564
|
-
return entry.dev ? { loaded: true } : {
|
|
55565
|
-
loaded: false,
|
|
55566
|
-
why: `server:${entry.name} is not on Claude Code's approved channels allowlist (use --dangerously-load-development-channels server:${entry.name})`
|
|
55567
|
-
};
|
|
55345
|
+
function conversationParts(content) {
|
|
55346
|
+
const { partChars, toolChars } = AGENT_STREAM_LIMITS;
|
|
55347
|
+
if (typeof content === "string") {
|
|
55348
|
+
return content === "" ? [] : [{ text: capAgentStreamText(content, partChars), type: "text" }];
|
|
55568
55349
|
}
|
|
55569
|
-
|
|
55570
|
-
|
|
55350
|
+
if (!Array.isArray(content))
|
|
55351
|
+
return [];
|
|
55352
|
+
const parts = [];
|
|
55353
|
+
for (const raw of content) {
|
|
55354
|
+
if (typeof raw !== "object" || raw === null)
|
|
55355
|
+
continue;
|
|
55356
|
+
const part = raw;
|
|
55357
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
55358
|
+
parts.push({ text: capAgentStreamText(part.text, partChars), type: "text" });
|
|
55359
|
+
} else if (part.type === "thinking" && typeof part.thinking === "string") {
|
|
55360
|
+
parts.push({ text: capAgentStreamText(part.thinking, partChars), type: "reasoning" });
|
|
55361
|
+
} else if (part.type === "toolCall" && typeof part.id === "string") {
|
|
55362
|
+
parts.push({
|
|
55363
|
+
argsText: capAgentStreamText(renderArguments(part.arguments), toolChars),
|
|
55364
|
+
toolCallId: part.id,
|
|
55365
|
+
toolName: typeof part.name === "string" ? part.name : "tool",
|
|
55366
|
+
type: "tool-call"
|
|
55367
|
+
});
|
|
55368
|
+
} else if (typeof part.type === "string") {
|
|
55369
|
+
parts.push({ text: blockPlaceholder(part.type), type: "text" });
|
|
55370
|
+
}
|
|
55371
|
+
}
|
|
55372
|
+
return parts;
|
|
55373
|
+
}
|
|
55374
|
+
function frameFor(entry) {
|
|
55375
|
+
const { dispatchMessageId, message } = entry;
|
|
55376
|
+
if (message.role === "toolResult" && message.toolCallId !== undefined) {
|
|
55571
55377
|
return {
|
|
55572
|
-
|
|
55573
|
-
|
|
55378
|
+
kind: "tool-result",
|
|
55379
|
+
result: {
|
|
55380
|
+
at: message.timestamp,
|
|
55381
|
+
isError: message.isError,
|
|
55382
|
+
output: capAgentStreamText(contentText(message.host.content), AGENT_STREAM_LIMITS.toolChars),
|
|
55383
|
+
toolCallId: message.toolCallId,
|
|
55384
|
+
toolName: message.toolName ?? "tool"
|
|
55385
|
+
},
|
|
55386
|
+
seq: entry.seq,
|
|
55387
|
+
v: AGENT_STREAM_PROTOCOL
|
|
55574
55388
|
};
|
|
55575
55389
|
}
|
|
55576
|
-
|
|
55577
|
-
|
|
55578
|
-
|
|
55579
|
-
|
|
55580
|
-
|
|
55581
|
-
|
|
55582
|
-
|
|
55390
|
+
return {
|
|
55391
|
+
kind: "message",
|
|
55392
|
+
message: {
|
|
55393
|
+
at: message.timestamp,
|
|
55394
|
+
id: entry.key.slice(2),
|
|
55395
|
+
parts: conversationParts(message.host.content),
|
|
55396
|
+
role: message.role === "assistant" ? "assistant" : "user",
|
|
55397
|
+
streaming: entry.streaming,
|
|
55398
|
+
...dispatchMessageId === undefined ? {} : { dispatchMessageId },
|
|
55399
|
+
...message.role === "assistant" && message.model !== undefined ? { model: message.model } : {}
|
|
55400
|
+
},
|
|
55401
|
+
seq: entry.seq,
|
|
55402
|
+
v: AGENT_STREAM_PROTOCOL
|
|
55403
|
+
};
|
|
55404
|
+
}
|
|
55405
|
+
|
|
55406
|
+
class AgentStreamPublisher {
|
|
55407
|
+
#deps;
|
|
55408
|
+
#history = new Map;
|
|
55409
|
+
#settled = new Set;
|
|
55410
|
+
#publishedAt = new Map;
|
|
55411
|
+
#seq;
|
|
55412
|
+
#watchedUntil = 0;
|
|
55413
|
+
#listedCommands;
|
|
55414
|
+
#commands;
|
|
55415
|
+
#commandsSeq = 0;
|
|
55416
|
+
#settings;
|
|
55417
|
+
#settingsSeq = 0;
|
|
55418
|
+
#settingsJson;
|
|
55419
|
+
constructor(deps) {
|
|
55420
|
+
this.#deps = deps;
|
|
55421
|
+
this.#seq = Math.trunc(deps.now()) * SEQ_PER_MS;
|
|
55422
|
+
}
|
|
55423
|
+
noteViewer() {
|
|
55424
|
+
this.#watchedUntil = this.#deps.now() + AGENT_STREAM_WATCH_TTL_MS;
|
|
55425
|
+
}
|
|
55426
|
+
publishState(subject) {
|
|
55427
|
+
if (!this.watched)
|
|
55428
|
+
return;
|
|
55429
|
+
if (this.#settings !== undefined) {
|
|
55430
|
+
this.#deps.publish(subject, JSON.stringify(this.#settingsFrame(this.#settings)));
|
|
55431
|
+
}
|
|
55432
|
+
if (this.#commands !== undefined) {
|
|
55433
|
+
this.#deps.publish(subject, JSON.stringify(this.#commandsFrame(this.#commands)));
|
|
55434
|
+
}
|
|
55435
|
+
}
|
|
55436
|
+
get watched() {
|
|
55437
|
+
return this.#deps.now() < this.#watchedUntil;
|
|
55438
|
+
}
|
|
55439
|
+
reset() {
|
|
55440
|
+
this.#history.clear();
|
|
55441
|
+
this.#settled.clear();
|
|
55442
|
+
this.#publishedAt.clear();
|
|
55443
|
+
this.#watchedUntil = 0;
|
|
55444
|
+
this.#listedCommands = undefined;
|
|
55445
|
+
this.#commands = undefined;
|
|
55446
|
+
this.#settings = undefined;
|
|
55447
|
+
this.#settingsJson = undefined;
|
|
55448
|
+
}
|
|
55449
|
+
setCommands(subject, commands) {
|
|
55450
|
+
const seen = new Set;
|
|
55451
|
+
const listed = [];
|
|
55452
|
+
for (const command of commands) {
|
|
55453
|
+
if (listed.length >= AGENT_STREAM_LIMITS.commands)
|
|
55454
|
+
break;
|
|
55455
|
+
const name = command.name;
|
|
55456
|
+
if (name.length > AGENT_STREAM_LIMITS.commandNameChars)
|
|
55457
|
+
continue;
|
|
55458
|
+
if (!isTypableCommandName(name) || seen.has(name))
|
|
55459
|
+
continue;
|
|
55460
|
+
seen.add(name);
|
|
55461
|
+
listed.push({
|
|
55462
|
+
name,
|
|
55463
|
+
source: command.source,
|
|
55464
|
+
...command.description !== undefined && {
|
|
55465
|
+
description: capAgentStreamText(command.description, AGENT_STREAM_LIMITS.commandDescriptionChars)
|
|
55466
|
+
},
|
|
55467
|
+
...command.terminalOnly === true && { terminalOnly: true }
|
|
55468
|
+
});
|
|
55469
|
+
}
|
|
55470
|
+
this.#listedCommands = listed;
|
|
55471
|
+
this.#fitCommands(subject);
|
|
55583
55472
|
}
|
|
55584
|
-
|
|
55585
|
-
|
|
55586
|
-
|
|
55587
|
-
|
|
55588
|
-
|
|
55589
|
-
|
|
55590
|
-
|
|
55591
|
-
|
|
55592
|
-
|
|
55593
|
-
|
|
55594
|
-
|
|
55595
|
-
|
|
55596
|
-
|
|
55597
|
-
|
|
55473
|
+
setSettings(subject, settings) {
|
|
55474
|
+
const kept = settings.reported ? {
|
|
55475
|
+
...settings.model !== undefined && { model: settings.model },
|
|
55476
|
+
models: [...settings.models],
|
|
55477
|
+
reported: true,
|
|
55478
|
+
thinking: settings.thinking === null ? null : { level: settings.thinking.level, levels: [...settings.thinking.levels] }
|
|
55479
|
+
} : { reported: false };
|
|
55480
|
+
const json = JSON.stringify(kept);
|
|
55481
|
+
if (json === this.#settingsJson)
|
|
55482
|
+
return;
|
|
55483
|
+
this.#settings = kept;
|
|
55484
|
+
this.#settingsJson = json;
|
|
55485
|
+
this.#seq += 1;
|
|
55486
|
+
this.#settingsSeq = this.#seq;
|
|
55487
|
+
if (this.watched)
|
|
55488
|
+
this.#deps.publish(subject, JSON.stringify(this.#settingsFrame(kept)));
|
|
55489
|
+
this.#fitCommands(subject);
|
|
55490
|
+
}
|
|
55491
|
+
#fitCommands(subject) {
|
|
55492
|
+
const listed = this.#listedCommands;
|
|
55493
|
+
if (listed === undefined)
|
|
55598
55494
|
return;
|
|
55495
|
+
const settings = this.#settings === undefined ? 0 : jsonBytes(this.#settingsFrame(this.#settings)) + 1;
|
|
55496
|
+
const limit = AGENT_STREAM_LIMITS.historyBytes - REPLAY_ARRAY_BYTES - settings;
|
|
55497
|
+
let bytes = jsonBytes(this.#commandsFrame([], this.#seq + 1));
|
|
55498
|
+
const kept = [];
|
|
55499
|
+
for (const command of listed) {
|
|
55500
|
+
const cost = jsonBytes(command) + (kept.length === 0 ? 0 : 1);
|
|
55501
|
+
if (bytes + cost > limit)
|
|
55502
|
+
break;
|
|
55503
|
+
bytes += cost;
|
|
55504
|
+
kept.push(command);
|
|
55599
55505
|
}
|
|
55506
|
+
if (JSON.stringify(kept) === JSON.stringify(this.#commands))
|
|
55507
|
+
return;
|
|
55508
|
+
this.#commands = kept;
|
|
55509
|
+
this.#seq += 1;
|
|
55510
|
+
this.#commandsSeq = this.#seq;
|
|
55511
|
+
if (this.watched)
|
|
55512
|
+
this.#deps.publish(subject, JSON.stringify(this.#commandsFrame(kept)));
|
|
55600
55513
|
}
|
|
55601
|
-
|
|
55602
|
-
return;
|
|
55603
|
-
const result = spawnSync("lsof", ["-a", "-p", String(pid), "-d", "txt", "-Fn"], {
|
|
55604
|
-
encoding: "utf8",
|
|
55605
|
-
timeout: 2000
|
|
55606
|
-
});
|
|
55607
|
-
const first = (result.stdout ?? "").split(`
|
|
55608
|
-
`).find((line) => line.startsWith("n"));
|
|
55609
|
-
return first === undefined ? undefined : first.slice(1);
|
|
55610
|
-
}
|
|
55611
|
-
function stdoutIsTerminal(pid) {
|
|
55612
|
-
if (process.platform !== "linux")
|
|
55613
|
-
return;
|
|
55614
|
-
try {
|
|
55615
|
-
const target = readlinkSync3(`/proc/${pid}/fd/1`);
|
|
55616
|
-
return target.startsWith("/dev/pts/") || /^\/dev\/tty[^S]/.test(target) || target === "/dev/tty";
|
|
55617
|
-
} catch {
|
|
55618
|
-
return;
|
|
55514
|
+
#commandsFrame(commands, seq = this.#commandsSeq) {
|
|
55515
|
+
return { commands, kind: "commands", seq, v: AGENT_STREAM_PROTOCOL };
|
|
55619
55516
|
}
|
|
55620
|
-
|
|
55621
|
-
|
|
55622
|
-
try {
|
|
55623
|
-
return readlinkSync3(`/proc/${pid}/cwd`);
|
|
55624
|
-
} catch {
|
|
55625
|
-
return;
|
|
55517
|
+
#settingsFrame(settings) {
|
|
55518
|
+
return { ...settings, kind: "settings", seq: this.#settingsSeq, v: AGENT_STREAM_PROTOCOL };
|
|
55626
55519
|
}
|
|
55627
|
-
|
|
55628
|
-
|
|
55629
|
-
|
|
55630
|
-
|
|
55631
|
-
|
|
55632
|
-
|
|
55633
|
-
|
|
55634
|
-
|
|
55635
|
-
|
|
55636
|
-
|
|
55637
|
-
|
|
55638
|
-
|
|
55639
|
-
|
|
55640
|
-
|
|
55641
|
-
|
|
55642
|
-
|
|
55643
|
-
|
|
55644
|
-
|
|
55645
|
-
|
|
55646
|
-
|
|
55647
|
-
|
|
55648
|
-
|
|
55649
|
-
|
|
55650
|
-
|
|
55651
|
-
|
|
55652
|
-
|
|
55520
|
+
record(subject, raw, streaming, dispatchMessageId) {
|
|
55521
|
+
const message = readMessage(raw);
|
|
55522
|
+
if (message === null)
|
|
55523
|
+
return false;
|
|
55524
|
+
const key = keyFor(message);
|
|
55525
|
+
if (key === null)
|
|
55526
|
+
return false;
|
|
55527
|
+
const stillStreaming = message.role === "assistant" && streaming;
|
|
55528
|
+
if (stillStreaming && this.#settled.has(key))
|
|
55529
|
+
return false;
|
|
55530
|
+
this.#seq += 1;
|
|
55531
|
+
const entry = {
|
|
55532
|
+
at: message.timestamp,
|
|
55533
|
+
dispatchMessageId: dispatchMessageId ?? this.#history.get(key)?.dispatchMessageId,
|
|
55534
|
+
key,
|
|
55535
|
+
message,
|
|
55536
|
+
seq: this.#seq,
|
|
55537
|
+
streaming: stillStreaming
|
|
55538
|
+
};
|
|
55539
|
+
this.#history.set(key, entry);
|
|
55540
|
+
if (!stillStreaming)
|
|
55541
|
+
this.#settled.add(key);
|
|
55542
|
+
while (this.#history.size > AGENT_STREAM_LIMITS.historyMessages) {
|
|
55543
|
+
const oldest = this.#history.keys().next();
|
|
55544
|
+
if (oldest.done === true)
|
|
55545
|
+
break;
|
|
55546
|
+
this.#history.delete(oldest.value);
|
|
55547
|
+
this.#settled.delete(oldest.value);
|
|
55548
|
+
this.#publishedAt.delete(oldest.value);
|
|
55653
55549
|
}
|
|
55654
|
-
|
|
55655
|
-
|
|
55656
|
-
|
|
55657
|
-
|
|
55658
|
-
|
|
55659
|
-
|
|
55660
|
-
|
|
55550
|
+
if (!this.watched)
|
|
55551
|
+
return true;
|
|
55552
|
+
const now = this.#deps.now();
|
|
55553
|
+
const published = this.#publishedAt.get(key);
|
|
55554
|
+
const tooSoon = published !== undefined && now - published < AGENT_STREAM_LIMITS.snapshotIntervalMs;
|
|
55555
|
+
if (stillStreaming && tooSoon)
|
|
55556
|
+
return true;
|
|
55557
|
+
this.#publishedAt.set(key, now);
|
|
55558
|
+
this.#deps.publish(subject, JSON.stringify(frameFor(entry)));
|
|
55559
|
+
return true;
|
|
55560
|
+
}
|
|
55561
|
+
replay(sessionID) {
|
|
55562
|
+
if (!this.watched)
|
|
55563
|
+
return { frames: [], session_id: sessionID, v: AGENT_STREAM_PROTOCOL };
|
|
55564
|
+
const ordered = [...this.#history.values()].sort((left, right) => left.at === right.at ? left.seq - right.seq : left.at - right.at);
|
|
55565
|
+
const tail = [];
|
|
55566
|
+
if (this.#settings !== undefined)
|
|
55567
|
+
tail.push(this.#settingsFrame(this.#settings));
|
|
55568
|
+
if (this.#commands !== undefined)
|
|
55569
|
+
tail.push(this.#commandsFrame(this.#commands));
|
|
55570
|
+
let used = REPLAY_ARRAY_BYTES + tail.reduce((bytes, frame, index) => bytes + jsonBytes(frame) + (index === 0 ? 0 : 1), 0);
|
|
55571
|
+
const kept = [];
|
|
55572
|
+
for (let index = ordered.length - 1;index >= 0; index -= 1) {
|
|
55573
|
+
const entry = ordered[index];
|
|
55574
|
+
if (entry === undefined)
|
|
55575
|
+
continue;
|
|
55576
|
+
const frame = frameFor(entry);
|
|
55577
|
+
const separated = kept.length > 0 || tail.length > 0;
|
|
55578
|
+
const cost = jsonBytes(frame) + (separated ? 1 : 0);
|
|
55579
|
+
if (used + cost > AGENT_STREAM_LIMITS.historyBytes)
|
|
55580
|
+
break;
|
|
55581
|
+
used += cost;
|
|
55582
|
+
kept.unshift(frame);
|
|
55583
|
+
}
|
|
55584
|
+
kept.push(...tail);
|
|
55585
|
+
return { frames: kept, session_id: sessionID, v: AGENT_STREAM_PROTOCOL };
|
|
55661
55586
|
}
|
|
55662
|
-
if (commandLine === undefined || commandLine.argv.length > 1)
|
|
55663
|
-
return commandLine;
|
|
55664
|
-
return basename2(executable(pid) ?? "").startsWith("node") ? undefined : commandLine;
|
|
55665
55587
|
}
|
|
55666
|
-
|
|
55667
|
-
|
|
55668
|
-
|
|
55669
|
-
|
|
55670
|
-
|
|
55671
|
-
|
|
55588
|
+
|
|
55589
|
+
// src/claude-rows.ts
|
|
55590
|
+
var COMMAND_BOOKKEEPING_TAGS = [
|
|
55591
|
+
"<local-command-caveat>",
|
|
55592
|
+
"<local-command-stdout>",
|
|
55593
|
+
"<local-command-stderr>"
|
|
55594
|
+
];
|
|
55595
|
+
var COMMAND_OUTPUT_TAGS = ["local-command-stdout", "local-command-stderr"];
|
|
55596
|
+
function claudeRowText(value) {
|
|
55597
|
+
if (typeof value === "string")
|
|
55598
|
+
return value;
|
|
55599
|
+
if (!Array.isArray(value))
|
|
55600
|
+
return "";
|
|
55601
|
+
const pieces = [];
|
|
55602
|
+
for (const item of value) {
|
|
55603
|
+
if (typeof item === "string") {
|
|
55604
|
+
pieces.push(item);
|
|
55605
|
+
continue;
|
|
55672
55606
|
}
|
|
55673
|
-
if (
|
|
55674
|
-
|
|
55675
|
-
const
|
|
55676
|
-
|
|
55677
|
-
|
|
55678
|
-
|
|
55679
|
-
|
|
55680
|
-
|
|
55681
|
-
|
|
55607
|
+
if (typeof item !== "object" || item === null)
|
|
55608
|
+
continue;
|
|
55609
|
+
const block = item;
|
|
55610
|
+
const text = block["text"];
|
|
55611
|
+
const type = block["type"];
|
|
55612
|
+
if (typeof text === "string")
|
|
55613
|
+
pieces.push(text);
|
|
55614
|
+
else if (typeof type === "string")
|
|
55615
|
+
pieces.push(`[${type}]`);
|
|
55616
|
+
}
|
|
55617
|
+
return pieces.join(`
|
|
55618
|
+
`);
|
|
55619
|
+
}
|
|
55620
|
+
function taggedValue(text, tag) {
|
|
55621
|
+
const open = `<${tag}>`;
|
|
55622
|
+
const from = text.indexOf(open);
|
|
55623
|
+
if (from === -1)
|
|
55682
55624
|
return;
|
|
55683
|
-
}
|
|
55625
|
+
const to = text.indexOf(`</${tag}>`, from + open.length);
|
|
55626
|
+
if (to === -1)
|
|
55627
|
+
return;
|
|
55628
|
+
return text.slice(from + open.length, to).trim();
|
|
55684
55629
|
}
|
|
55685
|
-
|
|
55686
|
-
|
|
55687
|
-
|
|
55688
|
-
|
|
55689
|
-
|
|
55690
|
-
|
|
55691
|
-
|
|
55692
|
-
|
|
55693
|
-
|
|
55694
|
-
|
|
55630
|
+
function claudeEchoedCommandLine(content) {
|
|
55631
|
+
const text = claudeRowText(content);
|
|
55632
|
+
const name = taggedValue(text, "command-name");
|
|
55633
|
+
if (name === undefined || name === "")
|
|
55634
|
+
return;
|
|
55635
|
+
const args = taggedValue(text, "command-args") ?? "";
|
|
55636
|
+
return args === "" ? name : `${name} ${args}`;
|
|
55637
|
+
}
|
|
55638
|
+
function claudeCommandOutput(content) {
|
|
55639
|
+
const text = claudeRowText(content);
|
|
55640
|
+
for (const tag of COMMAND_OUTPUT_TAGS) {
|
|
55641
|
+
const value = taggedValue(text, tag);
|
|
55642
|
+
if (value !== undefined)
|
|
55643
|
+
return value;
|
|
55695
55644
|
}
|
|
55696
55645
|
return;
|
|
55697
55646
|
}
|
|
55698
|
-
function
|
|
55699
|
-
const
|
|
55700
|
-
|
|
55701
|
-
|
|
55702
|
-
|
|
55647
|
+
function isCommandBookkeeping(content) {
|
|
55648
|
+
const text = claudeRowText(content);
|
|
55649
|
+
return COMMAND_BOOKKEEPING_TAGS.some((tag) => text.startsWith(tag));
|
|
55650
|
+
}
|
|
55651
|
+
var PICTURE_ATTACHMENT_ROW = /^\[Image: source: \/[^\]]+\]$/;
|
|
55652
|
+
function isPictureAttachmentRow(content) {
|
|
55653
|
+
return PICTURE_ATTACHMENT_ROW.test(claudeRowText(content).trim());
|
|
55654
|
+
}
|
|
55655
|
+
var MID_TURN_OPEN = "<system-reminder>";
|
|
55656
|
+
var MID_TURN_CLOSE = "</system-reminder>";
|
|
55657
|
+
var MID_TURN_ANNOUNCEMENT = "The user sent a new message while you were working:";
|
|
55658
|
+
var MID_TURN_NOTE = "This is how Claude Code surfaces messages the user sends mid-turn";
|
|
55659
|
+
function claudeMidTurnMessage(text) {
|
|
55660
|
+
const body = text.trim();
|
|
55661
|
+
if (!body.startsWith(MID_TURN_OPEN) || !body.endsWith(MID_TURN_CLOSE))
|
|
55662
|
+
return;
|
|
55663
|
+
const inner = body.slice(MID_TURN_OPEN.length, body.length - MID_TURN_CLOSE.length).trimStart();
|
|
55664
|
+
if (!inner.startsWith(MID_TURN_ANNOUNCEMENT))
|
|
55665
|
+
return;
|
|
55666
|
+
const note = inner.lastIndexOf(MID_TURN_NOTE);
|
|
55667
|
+
if (note < MID_TURN_ANNOUNCEMENT.length)
|
|
55668
|
+
return;
|
|
55669
|
+
return inner.slice(MID_TURN_ANNOUNCEMENT.length, note).trim();
|
|
55670
|
+
}
|
|
55671
|
+
|
|
55672
|
+
// src/agent-stream-claude.ts
|
|
55673
|
+
var CLAUDE_PROVIDER = "anthropic";
|
|
55674
|
+
var CLAUDE_THINKING_HIDDEN = "Claude Code hides what the model thought here: it sends the thinking text only when the terminal session turns on the showThinkingSummaries setting.";
|
|
55675
|
+
var CLAUDE_EFFORT_LEVELS = [
|
|
55676
|
+
"low",
|
|
55677
|
+
"medium",
|
|
55678
|
+
"high",
|
|
55679
|
+
"xhigh",
|
|
55680
|
+
"max"
|
|
55681
|
+
];
|
|
55682
|
+
function partOf(block) {
|
|
55683
|
+
const text = block["text"];
|
|
55684
|
+
if (block.type === "text" && typeof text === "string") {
|
|
55685
|
+
return { type: "text", text: claudeMidTurnMessage(text) ?? text };
|
|
55686
|
+
}
|
|
55687
|
+
const thinking = block["thinking"];
|
|
55688
|
+
if (block.type === "thinking" && typeof thinking === "string") {
|
|
55689
|
+
return { type: "thinking", thinking };
|
|
55690
|
+
}
|
|
55691
|
+
const id = block["id"];
|
|
55692
|
+
if (block.type === "tool_use" && typeof id === "string") {
|
|
55693
|
+
const name = block["name"];
|
|
55694
|
+
return {
|
|
55695
|
+
arguments: block["input"],
|
|
55696
|
+
id,
|
|
55697
|
+
name: typeof name === "string" ? name : "tool",
|
|
55698
|
+
type: "toolCall"
|
|
55699
|
+
};
|
|
55703
55700
|
}
|
|
55704
|
-
return;
|
|
55701
|
+
return { type: block.type };
|
|
55705
55702
|
}
|
|
55706
|
-
function
|
|
55707
|
-
|
|
55703
|
+
function shows(part) {
|
|
55704
|
+
if (part.type === "text")
|
|
55705
|
+
return part.text !== "";
|
|
55706
|
+
if (part.type === "thinking")
|
|
55707
|
+
return part.thinking !== "";
|
|
55708
|
+
return true;
|
|
55708
55709
|
}
|
|
55709
|
-
function
|
|
55710
|
-
|
|
55710
|
+
function roleOf(message) {
|
|
55711
|
+
if (message.role !== undefined)
|
|
55712
|
+
return message.role;
|
|
55713
|
+
return message.type === "assistant" ? "assistant" : "user";
|
|
55711
55714
|
}
|
|
55712
|
-
var
|
|
55713
|
-
function
|
|
55714
|
-
|
|
55715
|
-
|
|
55716
|
-
|
|
55717
|
-
|
|
55718
|
-
|
|
55719
|
-
|
|
55720
|
-
|
|
55721
|
-
|
|
55722
|
-
|
|
55723
|
-
|
|
55724
|
-
|
|
55725
|
-
|
|
55726
|
-
|
|
55715
|
+
var PANE_SEEDED_COMMAND = "/rename";
|
|
55716
|
+
function isPaneSeededRename(line) {
|
|
55717
|
+
return line === PANE_SEEDED_COMMAND || line.startsWith(`${PANE_SEEDED_COMMAND} `);
|
|
55718
|
+
}
|
|
55719
|
+
function callArguments(args) {
|
|
55720
|
+
if (args === "")
|
|
55721
|
+
return;
|
|
55722
|
+
try {
|
|
55723
|
+
return JSON.parse(args);
|
|
55724
|
+
} catch {
|
|
55725
|
+
return args;
|
|
55726
|
+
}
|
|
55727
|
+
}
|
|
55728
|
+
function listedModels(rows) {
|
|
55729
|
+
const row = rows.find((candidate) => candidate.key === "model");
|
|
55730
|
+
const options = row?.options ?? [];
|
|
55731
|
+
return options.flatMap((option) => modelReference(CLAUDE_PROVIDER, option) ?? []);
|
|
55732
|
+
}
|
|
55733
|
+
function claudeAgentStreamCommands(commands) {
|
|
55734
|
+
const listed = [];
|
|
55735
|
+
for (const command of commands) {
|
|
55736
|
+
const name = command.name.startsWith("/") ? command.name.slice(1) : command.name;
|
|
55737
|
+
if (!isTypableCommandName(name))
|
|
55738
|
+
continue;
|
|
55739
|
+
listed.push({
|
|
55740
|
+
name,
|
|
55741
|
+
source: command.source === "builtin" ? "builtin" : command.source === "plugin" ? "extension" : "prompt",
|
|
55742
|
+
...command.description === "" ? {} : { description: command.description }
|
|
55743
|
+
});
|
|
55744
|
+
}
|
|
55745
|
+
return listed;
|
|
55746
|
+
}
|
|
55747
|
+
function createClaudeAgentStream(deps) {
|
|
55748
|
+
const { isPane, publisher, now } = deps;
|
|
55749
|
+
const stepAt = new Map;
|
|
55750
|
+
let allocated = 0;
|
|
55751
|
+
let observed;
|
|
55752
|
+
let picked;
|
|
55753
|
+
const timestampFor = (turnId, index) => {
|
|
55754
|
+
const key = `${turnId}\x00${index}`;
|
|
55755
|
+
const kept = stepAt.get(key);
|
|
55756
|
+
if (kept !== undefined)
|
|
55757
|
+
return kept;
|
|
55758
|
+
const at = Math.max(now(), allocated + 1);
|
|
55759
|
+
allocated = at;
|
|
55760
|
+
stepAt.set(key, at);
|
|
55761
|
+
return at;
|
|
55762
|
+
};
|
|
55763
|
+
const record = (subject, message, streaming, dispatchMessageId) => {
|
|
55764
|
+
publisher.record(subject, message, streaming, dispatchMessageId);
|
|
55765
|
+
};
|
|
55766
|
+
const recordRow = (subject, uuid, role, content) => {
|
|
55767
|
+
record(subject, { content, role, tag: uuid, timestamp: now() }, false, deps.tagFor?.(uuid));
|
|
55768
|
+
};
|
|
55769
|
+
const recordStep = (subject, event) => {
|
|
55770
|
+
if (event.effort !== picked)
|
|
55771
|
+
picked = undefined;
|
|
55772
|
+
observed = { model: event.model, effort: event.effort };
|
|
55773
|
+
const content = [];
|
|
55774
|
+
let hiddenThinkingShown = false;
|
|
55775
|
+
for (const part of event.parts) {
|
|
55776
|
+
if (part.type === "text") {
|
|
55777
|
+
content.push({ text: part.text, type: "text" });
|
|
55778
|
+
} else if (part.type === "reasoning") {
|
|
55779
|
+
if (part.text !== "") {
|
|
55780
|
+
content.push({ thinking: part.text, type: "thinking" });
|
|
55781
|
+
} else if (!hiddenThinkingShown) {
|
|
55782
|
+
hiddenThinkingShown = true;
|
|
55783
|
+
content.push({ thinking: CLAUDE_THINKING_HIDDEN, type: "thinking" });
|
|
55784
|
+
}
|
|
55785
|
+
} else {
|
|
55786
|
+
content.push({
|
|
55787
|
+
arguments: callArguments(part.args),
|
|
55788
|
+
id: part.id,
|
|
55789
|
+
name: part.name,
|
|
55790
|
+
type: "toolCall"
|
|
55791
|
+
});
|
|
55727
55792
|
}
|
|
55728
|
-
if (equals === -1)
|
|
55729
|
-
index = lastValueIndex(argv, index);
|
|
55730
55793
|
}
|
|
55731
|
-
|
|
55732
|
-
|
|
55794
|
+
record(subject, {
|
|
55795
|
+
content,
|
|
55796
|
+
model: event.model,
|
|
55797
|
+
provider: CLAUDE_PROVIDER,
|
|
55798
|
+
role: "assistant",
|
|
55799
|
+
timestamp: timestampFor(event.turnId, event.index)
|
|
55800
|
+
}, !event.done);
|
|
55801
|
+
};
|
|
55802
|
+
const recordAppend = (subject, event) => {
|
|
55803
|
+
const { door, message, origin, uuid } = event;
|
|
55804
|
+
if (door === "response")
|
|
55805
|
+
return;
|
|
55806
|
+
if (message.isMeta === true && deps.appendedRow?.(uuid) !== true)
|
|
55807
|
+
return;
|
|
55808
|
+
if (isPictureAttachmentRow(message.content))
|
|
55809
|
+
return;
|
|
55810
|
+
if (door === "prompt") {
|
|
55811
|
+
if (deps.runsUnreadPrompt?.(uuid) === true)
|
|
55812
|
+
return;
|
|
55813
|
+
if (isPane && isPaneSeededRename(claudeRowText(message.content)))
|
|
55814
|
+
return;
|
|
55815
|
+
const content = message.content.map(partOf);
|
|
55816
|
+
if (!content.some(shows))
|
|
55817
|
+
return;
|
|
55818
|
+
recordRow(subject, uuid, "user", content);
|
|
55819
|
+
return;
|
|
55820
|
+
}
|
|
55821
|
+
if (door === "tool-result") {
|
|
55822
|
+
for (const block of message.content) {
|
|
55823
|
+
const toolCallId = block["tool_use_id"];
|
|
55824
|
+
if (typeof toolCallId !== "string")
|
|
55825
|
+
continue;
|
|
55826
|
+
record(subject, {
|
|
55827
|
+
content: claudeRowText(block["content"]),
|
|
55828
|
+
isError: block["is_error"] === true,
|
|
55829
|
+
role: "toolResult",
|
|
55830
|
+
timestamp: now(),
|
|
55831
|
+
toolCallId,
|
|
55832
|
+
...origin.tool === undefined ? {} : { toolName: origin.tool }
|
|
55833
|
+
}, false);
|
|
55834
|
+
}
|
|
55835
|
+
return;
|
|
55836
|
+
}
|
|
55837
|
+
if (door === "command") {
|
|
55838
|
+
const line = claudeEchoedCommandLine(message.content);
|
|
55839
|
+
if (line !== undefined) {
|
|
55840
|
+
if (isPane && isPaneSeededRename(line))
|
|
55841
|
+
return;
|
|
55842
|
+
if (deps.runsCommandRow?.(uuid) === true)
|
|
55843
|
+
return;
|
|
55844
|
+
recordRow(subject, uuid, "user", [{ text: line, type: "text" }]);
|
|
55845
|
+
return;
|
|
55846
|
+
}
|
|
55847
|
+
if (isCommandBookkeeping(message.content))
|
|
55848
|
+
return;
|
|
55849
|
+
}
|
|
55850
|
+
const content = message.content.map(partOf);
|
|
55851
|
+
if (!content.some(shows))
|
|
55852
|
+
return;
|
|
55853
|
+
recordRow(subject, uuid, roleOf(message), content);
|
|
55854
|
+
};
|
|
55855
|
+
return {
|
|
55856
|
+
record(subject, event) {
|
|
55857
|
+
if (event.kind === "step")
|
|
55858
|
+
recordStep(subject, event);
|
|
55859
|
+
else if (event.kind === "append")
|
|
55860
|
+
recordAppend(subject, event);
|
|
55861
|
+
},
|
|
55862
|
+
pickedEffort(effort) {
|
|
55863
|
+
picked = effort;
|
|
55864
|
+
},
|
|
55865
|
+
settings(model, rows) {
|
|
55866
|
+
if (rows === undefined)
|
|
55867
|
+
return { reported: false };
|
|
55868
|
+
const reference = modelReference(CLAUDE_PROVIDER, model);
|
|
55869
|
+
return {
|
|
55870
|
+
models: listedModels(rows),
|
|
55871
|
+
reported: true,
|
|
55872
|
+
thinking: picked !== undefined ? { level: String(picked), levels: CLAUDE_EFFORT_LEVELS } : observed !== undefined && observed.model === model ? observed.effort === undefined ? null : { level: String(observed.effort), levels: CLAUDE_EFFORT_LEVELS } : { level: null, levels: CLAUDE_EFFORT_LEVELS },
|
|
55873
|
+
...reference === undefined ? {} : { model: reference }
|
|
55874
|
+
};
|
|
55875
|
+
},
|
|
55876
|
+
reset() {
|
|
55877
|
+
stepAt.clear();
|
|
55878
|
+
observed = undefined;
|
|
55879
|
+
picked = undefined;
|
|
55880
|
+
}
|
|
55881
|
+
};
|
|
55733
55882
|
}
|
|
55734
|
-
|
|
55735
|
-
|
|
55736
|
-
|
|
55737
|
-
|
|
55738
|
-
|
|
55739
|
-
|
|
55740
|
-
|
|
55741
|
-
|
|
55742
|
-
|
|
55743
|
-
|
|
55744
|
-
return value.flatMap((entry) => {
|
|
55745
|
-
const parsed = AllowedChannelEntry.safeParse(entry);
|
|
55746
|
-
return parsed.success ? [parsed.data] : [];
|
|
55747
|
-
});
|
|
55883
|
+
|
|
55884
|
+
// src/bounded-set.ts
|
|
55885
|
+
function rememberBounded(remembered, value, max) {
|
|
55886
|
+
remembered.add(value);
|
|
55887
|
+
while (remembered.size > max) {
|
|
55888
|
+
const oldest = remembered.values().next().value;
|
|
55889
|
+
if (oldest === undefined)
|
|
55890
|
+
break;
|
|
55891
|
+
remembered.delete(oldest);
|
|
55892
|
+
}
|
|
55748
55893
|
}
|
|
55749
|
-
function
|
|
55750
|
-
|
|
55751
|
-
|
|
55752
|
-
|
|
55753
|
-
|
|
55754
|
-
|
|
55755
|
-
|
|
55756
|
-
return "/etc/claude-code";
|
|
55894
|
+
function rememberBoundedEntry(remembered, key, value, max) {
|
|
55895
|
+
remembered.set(key, value);
|
|
55896
|
+
while (remembered.size > max) {
|
|
55897
|
+
const oldest = remembered.keys().next().value;
|
|
55898
|
+
if (oldest === undefined)
|
|
55899
|
+
break;
|
|
55900
|
+
remembered.delete(oldest);
|
|
55757
55901
|
}
|
|
55758
55902
|
}
|
|
55759
|
-
|
|
55760
|
-
|
|
55761
|
-
|
|
55762
|
-
|
|
55763
|
-
|
|
55764
|
-
|
|
55765
|
-
|
|
55766
|
-
|
|
55903
|
+
|
|
55904
|
+
// src/agent-stream-runner.ts
|
|
55905
|
+
var encoder5 = new TextEncoder;
|
|
55906
|
+
var decoder5 = new TextDecoder;
|
|
55907
|
+
var AGENT_STREAM_CALL_TIMEOUT_MS = 5000;
|
|
55908
|
+
var AGENT_STREAM_RESUBSCRIBE_MS = 1000;
|
|
55909
|
+
var AGENT_STREAM_SEEN_ROWS_MAX = 512;
|
|
55910
|
+
function startAgentStream(options) {
|
|
55911
|
+
const { connection, link } = options;
|
|
55912
|
+
const now = options.now ?? Date.now;
|
|
55913
|
+
const warn = options.warn ?? ((text) => void process.stderr.write(`envoy-channel: ${text}
|
|
55914
|
+
`));
|
|
55915
|
+
const publisher = new AgentStreamPublisher({
|
|
55916
|
+
now,
|
|
55917
|
+
publish: (subject, payload) => connection.publish(subject, encoder5.encode(payload))
|
|
55918
|
+
});
|
|
55919
|
+
const stream = createClaudeAgentStream({
|
|
55920
|
+
isPane: options.isPane === true,
|
|
55921
|
+
now,
|
|
55922
|
+
publisher,
|
|
55923
|
+
...options.appendedRow === undefined ? {} : { appendedRow: options.appendedRow },
|
|
55924
|
+
...options.runsCommandRow === undefined ? {} : { runsCommandRow: options.runsCommandRow },
|
|
55925
|
+
...options.runsUnreadPrompt === undefined ? {} : { runsUnreadPrompt: options.runsUnreadPrompt },
|
|
55926
|
+
...options.tagFor === undefined ? {} : { tagFor: options.tagFor }
|
|
55927
|
+
});
|
|
55928
|
+
let session = options.sessionId;
|
|
55929
|
+
let subscription;
|
|
55930
|
+
let stopped = false;
|
|
55931
|
+
let model;
|
|
55932
|
+
let configRows;
|
|
55933
|
+
let listedCommands;
|
|
55934
|
+
let readingSettings = false;
|
|
55935
|
+
let readingCommands = false;
|
|
55936
|
+
let viewerAttached = false;
|
|
55937
|
+
let commandsRefreshedOnWatchAt = Number.NEGATIVE_INFINITY;
|
|
55938
|
+
let primed = false;
|
|
55939
|
+
const seenRows = new Set;
|
|
55940
|
+
const readTwice = (uuid) => {
|
|
55941
|
+
if (seenRows.has(uuid))
|
|
55942
|
+
return true;
|
|
55943
|
+
rememberBounded(seenRows, uuid, AGENT_STREAM_SEEN_ROWS_MAX);
|
|
55944
|
+
return false;
|
|
55945
|
+
};
|
|
55946
|
+
const framesSubject = () => agentStreamFramesSubject(session);
|
|
55947
|
+
const publishSettings = () => {
|
|
55948
|
+
publisher.setSettings(framesSubject(), stream.settings(model, configRows));
|
|
55949
|
+
};
|
|
55950
|
+
const publishCommands = () => {
|
|
55951
|
+
if (listedCommands !== undefined)
|
|
55952
|
+
publisher.setCommands(framesSubject(), listedCommands);
|
|
55953
|
+
};
|
|
55954
|
+
const refreshSettings = () => {
|
|
55955
|
+
if (stopped || readingSettings || !link.connected())
|
|
55767
55956
|
return;
|
|
55768
|
-
|
|
55957
|
+
readingSettings = true;
|
|
55958
|
+
Promise.all([
|
|
55959
|
+
link.request("session.model", undefined, { timeoutMs: AGENT_STREAM_CALL_TIMEOUT_MS }),
|
|
55960
|
+
link.request("config.list", undefined, { timeoutMs: AGENT_STREAM_CALL_TIMEOUT_MS })
|
|
55961
|
+
]).then(([answeredModel, rows]) => {
|
|
55962
|
+
model = answeredModel;
|
|
55963
|
+
configRows = rows;
|
|
55964
|
+
if (!stopped)
|
|
55965
|
+
publishSettings();
|
|
55966
|
+
}).catch((error) => {
|
|
55967
|
+
warn(`the live view could not read this session's model \u2014 ${messageFor(error)}`);
|
|
55968
|
+
}).finally(() => {
|
|
55969
|
+
readingSettings = false;
|
|
55970
|
+
});
|
|
55971
|
+
};
|
|
55972
|
+
const refreshCommands = () => {
|
|
55973
|
+
if (stopped || readingCommands || !link.connected())
|
|
55769
55974
|
return;
|
|
55770
|
-
|
|
55771
|
-
|
|
55772
|
-
|
|
55773
|
-
|
|
55774
|
-
|
|
55775
|
-
}
|
|
55776
|
-
|
|
55777
|
-
|
|
55975
|
+
readingCommands = true;
|
|
55976
|
+
link.request("command.list", undefined, { timeoutMs: AGENT_STREAM_CALL_TIMEOUT_MS }).then((commands) => {
|
|
55977
|
+
listedCommands = claudeAgentStreamCommands(commands);
|
|
55978
|
+
if (!stopped)
|
|
55979
|
+
publishCommands();
|
|
55980
|
+
}).catch((error) => {
|
|
55981
|
+
warn(`the live view could not read this session's commands \u2014 ${messageFor(error)}`);
|
|
55982
|
+
}).finally(() => {
|
|
55983
|
+
readingCommands = false;
|
|
55984
|
+
});
|
|
55985
|
+
};
|
|
55986
|
+
const pump = async (taken, forSession) => {
|
|
55987
|
+
try {
|
|
55988
|
+
for await (const message of taken) {
|
|
55989
|
+
try {
|
|
55990
|
+
const parsed = JSON.parse(decoder5.decode(message.data));
|
|
55991
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
55992
|
+
continue;
|
|
55993
|
+
const request = parsed;
|
|
55994
|
+
if (request.type !== "replay" && request.type !== "watch")
|
|
55995
|
+
continue;
|
|
55996
|
+
refreshSettings();
|
|
55997
|
+
if (request.type === "replay" || !viewerAttached || now() - commandsRefreshedOnWatchAt >= AGENT_STREAM_WATCH_INTERVAL_MS) {
|
|
55998
|
+
refreshCommands();
|
|
55999
|
+
if (request.type === "watch" && viewerAttached)
|
|
56000
|
+
commandsRefreshedOnWatchAt = now();
|
|
56001
|
+
}
|
|
56002
|
+
const reply = message.reply;
|
|
56003
|
+
const answer = request.type === "replay" && reply !== undefined ? JSON.stringify(publisher.replay(forSession)) : undefined;
|
|
56004
|
+
const arming = !publisher.watched;
|
|
56005
|
+
publisher.noteViewer();
|
|
56006
|
+
viewerAttached = true;
|
|
56007
|
+
if (arming)
|
|
56008
|
+
publisher.publishState(agentStreamFramesSubject(forSession));
|
|
56009
|
+
if (answer === undefined || reply === undefined)
|
|
56010
|
+
continue;
|
|
56011
|
+
connection.publish(reply, encoder5.encode(answer));
|
|
56012
|
+
} catch (error) {
|
|
56013
|
+
warn(`a live view control message failed \u2014 ${messageFor(error)}`);
|
|
56014
|
+
}
|
|
56015
|
+
}
|
|
56016
|
+
} catch {}
|
|
56017
|
+
if (subscription === taken)
|
|
56018
|
+
subscription = undefined;
|
|
56019
|
+
if (stopped || session !== forSession)
|
|
56020
|
+
return;
|
|
56021
|
+
const retry = setTimeout(() => {
|
|
56022
|
+
if (stopped || session !== forSession)
|
|
56023
|
+
return;
|
|
56024
|
+
subscribe();
|
|
56025
|
+
}, AGENT_STREAM_RESUBSCRIBE_MS);
|
|
56026
|
+
retry.unref();
|
|
56027
|
+
};
|
|
56028
|
+
function subscribe() {
|
|
56029
|
+
if (stopped)
|
|
56030
|
+
return;
|
|
56031
|
+
subscription?.unsubscribe();
|
|
56032
|
+
const taken = connection.subscribe(agentStreamControlSubject(session));
|
|
56033
|
+
subscription = taken;
|
|
56034
|
+
pump(taken, session);
|
|
55778
56035
|
}
|
|
56036
|
+
link.onSessionId((next) => {
|
|
56037
|
+
if (stopped || !options.followsSessionId || next === session)
|
|
56038
|
+
return;
|
|
56039
|
+
session = next;
|
|
56040
|
+
publisher.reset();
|
|
56041
|
+
stream.reset();
|
|
56042
|
+
viewerAttached = false;
|
|
56043
|
+
commandsRefreshedOnWatchAt = Number.NEGATIVE_INFINITY;
|
|
56044
|
+
publishSettings();
|
|
56045
|
+
publishCommands();
|
|
56046
|
+
subscribe();
|
|
56047
|
+
});
|
|
56048
|
+
const reading = (async () => {
|
|
56049
|
+
for await (const event of link.events) {
|
|
56050
|
+
if (!primed) {
|
|
56051
|
+
primed = true;
|
|
56052
|
+
refreshSettings();
|
|
56053
|
+
refreshCommands();
|
|
56054
|
+
}
|
|
56055
|
+
if (event.kind === "append" && readTwice(event.uuid))
|
|
56056
|
+
continue;
|
|
56057
|
+
if (event.kind === "model")
|
|
56058
|
+
model = event.model;
|
|
56059
|
+
options.observe?.(event);
|
|
56060
|
+
stream.record(framesSubject(), event);
|
|
56061
|
+
if (event.kind === "step" || event.kind === "model")
|
|
56062
|
+
publishSettings();
|
|
56063
|
+
}
|
|
56064
|
+
})();
|
|
56065
|
+
reading.catch((error) => {
|
|
56066
|
+
warn(`the live view stopped reading this session's events \u2014 ${messageFor(error)}`);
|
|
56067
|
+
});
|
|
56068
|
+
subscribe();
|
|
56069
|
+
return {
|
|
56070
|
+
refresh() {
|
|
56071
|
+
refreshSettings();
|
|
56072
|
+
},
|
|
56073
|
+
pickedEffort(effort) {
|
|
56074
|
+
if (stopped)
|
|
56075
|
+
return;
|
|
56076
|
+
stream.pickedEffort(effort);
|
|
56077
|
+
publishSettings();
|
|
56078
|
+
},
|
|
56079
|
+
stop() {
|
|
56080
|
+
if (stopped)
|
|
56081
|
+
return;
|
|
56082
|
+
stopped = true;
|
|
56083
|
+
subscription?.unsubscribe();
|
|
56084
|
+
subscription = undefined;
|
|
56085
|
+
}
|
|
56086
|
+
};
|
|
55779
56087
|
}
|
|
55780
|
-
|
|
55781
|
-
|
|
55782
|
-
|
|
55783
|
-
|
|
56088
|
+
|
|
56089
|
+
// src/channel-forwarder.ts
|
|
56090
|
+
var DeliveryIdentity = DedupeIdentitySchema.extend({
|
|
56091
|
+
event_id: DedupeIdentitySchema.shape.event_id.catch(undefined),
|
|
56092
|
+
dedupe_key: DedupeIdentitySchema.shape.dedupe_key.catch(undefined),
|
|
56093
|
+
source: DedupeIdentitySchema.shape.source.catch(undefined),
|
|
56094
|
+
source_event_id: DedupeIdentitySchema.shape.source_event_id.catch(undefined),
|
|
56095
|
+
topic: string2().min(1).optional().catch(undefined)
|
|
56096
|
+
});
|
|
56097
|
+
var decoder6 = new TextDecoder;
|
|
56098
|
+
var DEFAULT_DRAIN_TIMEOUT_MS = 1000;
|
|
56099
|
+
function deliveryIdentity(raw) {
|
|
56100
|
+
let parsed;
|
|
55784
56101
|
try {
|
|
55785
|
-
|
|
56102
|
+
parsed = JSON.parse(raw);
|
|
55786
56103
|
} catch {
|
|
55787
|
-
|
|
55788
|
-
}
|
|
55789
|
-
const
|
|
55790
|
-
|
|
55791
|
-
|
|
55792
|
-
|
|
55793
|
-
|
|
55794
|
-
|
|
55795
|
-
|
|
55796
|
-
|
|
55797
|
-
|
|
55798
|
-
const
|
|
55799
|
-
|
|
55800
|
-
|
|
55801
|
-
|
|
55802
|
-
|
|
55803
|
-
|
|
55804
|
-
|
|
55805
|
-
|
|
55806
|
-
|
|
55807
|
-
|
|
55808
|
-
|
|
55809
|
-
|
|
55810
|
-
|
|
55811
|
-
|
|
55812
|
-
|
|
55813
|
-
|
|
55814
|
-
|
|
56104
|
+
return;
|
|
56105
|
+
}
|
|
56106
|
+
const identity = DeliveryIdentity.safeParse(parsed);
|
|
56107
|
+
return identity.success ? identity.data : undefined;
|
|
56108
|
+
}
|
|
56109
|
+
function report(what, error) {
|
|
56110
|
+
process.stderr.write(`envoy-channel: ${what} \u2014 ${messageFor(error)}
|
|
56111
|
+
`);
|
|
56112
|
+
}
|
|
56113
|
+
function createChannelForwarder(connection, options) {
|
|
56114
|
+
const following = new Map;
|
|
56115
|
+
const dedupe = options.dedupe ?? createDeliveryDedupe();
|
|
56116
|
+
const drainTimeoutMs = options.drainTimeoutMs ?? DEFAULT_DRAIN_TIMEOUT_MS;
|
|
56117
|
+
const deliver = async (topic, subscription) => {
|
|
56118
|
+
try {
|
|
56119
|
+
for await (const { message, receivedAt } of readArrivals(subscription)) {
|
|
56120
|
+
const raw = decoder6.decode(message.data);
|
|
56121
|
+
const identity = deliveryIdentity(raw);
|
|
56122
|
+
let claim = options.relayed?.(topic) === true ? undefined : dedupe.claim(identity);
|
|
56123
|
+
if (claim !== undefined && identity !== undefined && options.ledger?.delivered(identity)) {
|
|
56124
|
+
claim.settle();
|
|
56125
|
+
claim = undefined;
|
|
56126
|
+
}
|
|
56127
|
+
const inbound = {
|
|
56128
|
+
subject: message.subject,
|
|
56129
|
+
data: message.data,
|
|
56130
|
+
raw,
|
|
56131
|
+
...message.reply ? { reply: message.reply } : {},
|
|
56132
|
+
...identity?.topic === undefined ? {} : { envelopeTopic: identity.topic },
|
|
56133
|
+
...claim === undefined ? { duplicate: true } : {}
|
|
56134
|
+
};
|
|
56135
|
+
let handed;
|
|
56136
|
+
try {
|
|
56137
|
+
handed = await options.deliver(inbound);
|
|
56138
|
+
} catch (error) {
|
|
56139
|
+
claim?.release();
|
|
56140
|
+
report(`could not deliver a message on ${message.subject}`, error);
|
|
56141
|
+
if (claim !== undefined)
|
|
56142
|
+
options.failed?.(inbound, topic, error, receivedAt);
|
|
56143
|
+
continue;
|
|
56144
|
+
}
|
|
56145
|
+
if (claim === undefined)
|
|
56146
|
+
continue;
|
|
56147
|
+
if (!handed) {
|
|
56148
|
+
claim.release();
|
|
56149
|
+
continue;
|
|
56150
|
+
}
|
|
56151
|
+
const taken = claim;
|
|
56152
|
+
const settle = () => taken.settle();
|
|
56153
|
+
if (identity === undefined || options.ledger === undefined)
|
|
56154
|
+
settle();
|
|
56155
|
+
else
|
|
56156
|
+
options.ledger.recordDelivered(identity).then(settle, settle);
|
|
56157
|
+
}
|
|
56158
|
+
} catch (error) {
|
|
56159
|
+
report(`delivery from ${topic} stopped`, error);
|
|
56160
|
+
}
|
|
56161
|
+
};
|
|
56162
|
+
const stop = async (topics) => {
|
|
56163
|
+
const stopping = topics.flatMap((topic) => {
|
|
56164
|
+
const entry = following.get(topic);
|
|
56165
|
+
if (entry === undefined)
|
|
56166
|
+
return [];
|
|
56167
|
+
following.delete(topic);
|
|
56168
|
+
entry.subscription.unsubscribe();
|
|
56169
|
+
return [entry.done];
|
|
56170
|
+
});
|
|
56171
|
+
await Promise.all(stopping);
|
|
56172
|
+
};
|
|
56173
|
+
return {
|
|
56174
|
+
follow(topic) {
|
|
56175
|
+
for (const subject of expandSubscriptionTopics([topic])) {
|
|
56176
|
+
if (following.has(subject))
|
|
56177
|
+
continue;
|
|
56178
|
+
const subscription = connection.subscribe(subject);
|
|
56179
|
+
following.set(subject, { subscription, done: deliver(subject, subscription) });
|
|
56180
|
+
}
|
|
56181
|
+
},
|
|
56182
|
+
unfollow(topics) {
|
|
56183
|
+
return stop(topics.length === 0 ? [...following.keys()] : expandSubscriptionTopics(topics));
|
|
56184
|
+
},
|
|
56185
|
+
topics() {
|
|
56186
|
+
return [...following.keys()];
|
|
56187
|
+
},
|
|
56188
|
+
isClosed() {
|
|
56189
|
+
return connection.isClosed();
|
|
56190
|
+
},
|
|
56191
|
+
async close() {
|
|
56192
|
+
await stop([...following.keys()]);
|
|
56193
|
+
if (connection.isClosed())
|
|
56194
|
+
return;
|
|
56195
|
+
const timeout = Promise.withResolvers();
|
|
56196
|
+
const timer = setTimeout(timeout.resolve, drainTimeoutMs);
|
|
56197
|
+
try {
|
|
56198
|
+
await Promise.race([connection.drain(), timeout.promise]);
|
|
56199
|
+
} catch (error) {
|
|
56200
|
+
report("draining the broker connection failed", error);
|
|
56201
|
+
}
|
|
56202
|
+
clearTimeout(timer);
|
|
56203
|
+
if (connection.isClosed())
|
|
56204
|
+
return;
|
|
56205
|
+
try {
|
|
56206
|
+
await connection.close();
|
|
56207
|
+
} catch (error) {
|
|
56208
|
+
report("closing the broker connection failed", error);
|
|
56209
|
+
}
|
|
56210
|
+
}
|
|
56211
|
+
};
|
|
55815
56212
|
}
|
|
55816
56213
|
|
|
55817
56214
|
// src/claude-session.ts
|
|
@@ -55833,12 +56230,12 @@ function claudeProjectDirectory(environment, fallback) {
|
|
|
55833
56230
|
|
|
55834
56231
|
// src/session-identity.ts
|
|
55835
56232
|
import { readFile as readFile2 } from "fs/promises";
|
|
55836
|
-
import { basename as
|
|
56233
|
+
import { basename as basename2, join as join7 } from "path";
|
|
55837
56234
|
|
|
55838
56235
|
// src/session-state.ts
|
|
55839
|
-
import { readFileSync as
|
|
56236
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
55840
56237
|
import { mkdir, readdir, rename, rm, stat, utimes, writeFile } from "fs/promises";
|
|
55841
|
-
import { dirname as dirname2, join as
|
|
56238
|
+
import { dirname as dirname2, join as join6 } from "path";
|
|
55842
56239
|
async function writeAtomicStateFile(file, contents) {
|
|
55843
56240
|
await mkdir(dirname2(file), { recursive: true, mode: 448 });
|
|
55844
56241
|
const temporary = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
@@ -55856,7 +56253,7 @@ async function pruneStateDirectory(directory, isStale) {
|
|
|
55856
56253
|
throw error;
|
|
55857
56254
|
});
|
|
55858
56255
|
for (const entry of entries) {
|
|
55859
|
-
const file =
|
|
56256
|
+
const file = join6(directory, entry);
|
|
55860
56257
|
if (!await isStale(file))
|
|
55861
56258
|
continue;
|
|
55862
56259
|
await rm(file, { recursive: true, force: true });
|
|
@@ -55871,7 +56268,7 @@ async function writtenLongAgo(file) {
|
|
|
55871
56268
|
}
|
|
55872
56269
|
var TITLES_DIRECTORY = "titles";
|
|
55873
56270
|
function sessionTitleFile(stateDirectory, sessionId) {
|
|
55874
|
-
return
|
|
56271
|
+
return join6(stateDirectory, TITLES_DIRECTORY, `${encodeURIComponent(sessionId)}.txt`);
|
|
55875
56272
|
}
|
|
55876
56273
|
async function writeChosenTitle(file, title) {
|
|
55877
56274
|
await writeAtomicStateFile(file, `${title}
|
|
@@ -55882,7 +56279,7 @@ async function removeChosenTitle(file) {
|
|
|
55882
56279
|
}
|
|
55883
56280
|
function readChosenTitleSync(file) {
|
|
55884
56281
|
try {
|
|
55885
|
-
return
|
|
56282
|
+
return readFileSync6(file, "utf8");
|
|
55886
56283
|
} catch {
|
|
55887
56284
|
return;
|
|
55888
56285
|
}
|
|
@@ -55899,7 +56296,7 @@ async function recordChosenTitle(sessionId, title) {
|
|
|
55899
56296
|
await writeChosenTitle(file, chosen);
|
|
55900
56297
|
}
|
|
55901
56298
|
async function pruneStaleSessionTitles(stateDirectory) {
|
|
55902
|
-
await pruneStateDirectory(
|
|
56299
|
+
await pruneStateDirectory(join6(stateDirectory, TITLES_DIRECTORY), writtenLongAgo);
|
|
55903
56300
|
}
|
|
55904
56301
|
|
|
55905
56302
|
// src/session-identity.ts
|
|
@@ -55924,14 +56321,14 @@ class SessionIdentity {
|
|
|
55924
56321
|
}
|
|
55925
56322
|
}
|
|
55926
56323
|
function sessionHandoffDirectory(stateDirectory, claudePid) {
|
|
55927
|
-
return
|
|
56324
|
+
return join7(stateDirectory, "sessions", String(claudePid));
|
|
55928
56325
|
}
|
|
55929
56326
|
function sessionHandoffFile(stateDirectory, claudePid) {
|
|
55930
|
-
return
|
|
56327
|
+
return join7(sessionHandoffDirectory(stateDirectory, claudePid), "session-id");
|
|
55931
56328
|
}
|
|
55932
56329
|
var ROLES_DIRECTORY = "roles";
|
|
55933
56330
|
function roleStateFile(stateDirectory, sessionId) {
|
|
55934
|
-
return
|
|
56331
|
+
return join7(stateDirectory, ROLES_DIRECTORY, `${encodeURIComponent(sessionId)}.json`);
|
|
55935
56332
|
}
|
|
55936
56333
|
|
|
55937
56334
|
class UnparseableRoleRecordError extends Error {
|
|
@@ -55982,14 +56379,14 @@ function processExists(pid) {
|
|
|
55982
56379
|
}
|
|
55983
56380
|
}
|
|
55984
56381
|
async function pruneStaleSessionHandoffs(stateDirectory) {
|
|
55985
|
-
await pruneStateDirectory(
|
|
55986
|
-
const pid =
|
|
56382
|
+
await pruneStateDirectory(join7(stateDirectory, "sessions"), (file) => {
|
|
56383
|
+
const pid = basename2(file);
|
|
55987
56384
|
return /^\d+$/.test(pid) && !processExists(Number(pid));
|
|
55988
56385
|
});
|
|
55989
56386
|
}
|
|
55990
56387
|
async function pruneStaleSessionRoles(stateDirectory, currentSessionId) {
|
|
55991
56388
|
const own = roleStateFile(stateDirectory, currentSessionId);
|
|
55992
|
-
await pruneStateDirectory(
|
|
56389
|
+
await pruneStateDirectory(join7(stateDirectory, ROLES_DIRECTORY), async (file) => file !== own && await writtenLongAgo(file));
|
|
55993
56390
|
}
|
|
55994
56391
|
|
|
55995
56392
|
// src/envoy-off.ts
|
|
@@ -56005,6 +56402,15 @@ function inboundOffChannelSession(options) {
|
|
|
56005
56402
|
onRoleRegained: () => {
|
|
56006
56403
|
return;
|
|
56007
56404
|
},
|
|
56405
|
+
capabilitiesChanged: () => {
|
|
56406
|
+
return;
|
|
56407
|
+
},
|
|
56408
|
+
observe: () => {
|
|
56409
|
+
return;
|
|
56410
|
+
},
|
|
56411
|
+
messageOf: () => {
|
|
56412
|
+
return;
|
|
56413
|
+
},
|
|
56008
56414
|
async rememberRole(role) {
|
|
56009
56415
|
await writePersistedRole(roleStateFile(stateDirectory, identity.id), {
|
|
56010
56416
|
session_id: identity.id,
|
|
@@ -56037,9 +56443,23 @@ async function serveEnvoyOff(serverInfo, refusal) {
|
|
|
56037
56443
|
}
|
|
56038
56444
|
}
|
|
56039
56445
|
|
|
56446
|
+
// src/installed-build.ts
|
|
56447
|
+
import { sep } from "path";
|
|
56448
|
+
function pluginCacheSegments(bundlePath) {
|
|
56449
|
+
const parts = bundlePath.split(sep);
|
|
56450
|
+
for (let index = 0;index + 1 < parts.length; index++) {
|
|
56451
|
+
if (parts[index] === "plugins" && parts[index + 1] === "cache")
|
|
56452
|
+
return parts.slice(index + 2);
|
|
56453
|
+
}
|
|
56454
|
+
return;
|
|
56455
|
+
}
|
|
56456
|
+
function installedPluginVersion(bundlePath) {
|
|
56457
|
+
return pluginCacheSegments(bundlePath)?.[2];
|
|
56458
|
+
}
|
|
56459
|
+
|
|
56040
56460
|
// src/legion-claim.ts
|
|
56041
56461
|
import { readdir as readdir2, stat as stat2 } from "fs/promises";
|
|
56042
|
-
import { join as
|
|
56462
|
+
import { join as join8 } from "path";
|
|
56043
56463
|
import { setTimeout as delay } from "timers/promises";
|
|
56044
56464
|
|
|
56045
56465
|
// ../pi-shared/src/legion-claim-core.ts
|
|
@@ -56125,12 +56545,12 @@ function stopPane(why) {
|
|
|
56125
56545
|
endPane();
|
|
56126
56546
|
}
|
|
56127
56547
|
async function waitForTranscript(configDir, sessionID, signal, pollMs = 100) {
|
|
56128
|
-
const projects =
|
|
56548
|
+
const projects = join8(configDir, "projects");
|
|
56129
56549
|
const name = `${sessionID}.jsonl`;
|
|
56130
56550
|
for (;; ) {
|
|
56131
56551
|
signal.throwIfAborted();
|
|
56132
56552
|
for (const directory of await readdir2(projects).catch(() => [])) {
|
|
56133
|
-
const file =
|
|
56553
|
+
const file = join8(projects, directory, name);
|
|
56134
56554
|
if (await isFile(file))
|
|
56135
56555
|
return file;
|
|
56136
56556
|
}
|
|
@@ -56278,10 +56698,14 @@ function createInboxNotifier(socketPath) {
|
|
|
56278
56698
|
};
|
|
56279
56699
|
return {
|
|
56280
56700
|
notification(notification) {
|
|
56281
|
-
const id = notification.
|
|
56701
|
+
const id = notification.eventId ?? randomUUID2();
|
|
56282
56702
|
const settle = Promise.withResolvers();
|
|
56283
56703
|
waiting.push({ id, settle });
|
|
56284
|
-
connected().write(`${JSON.stringify({
|
|
56704
|
+
connected().write(`${JSON.stringify({
|
|
56705
|
+
id,
|
|
56706
|
+
content: notification.content,
|
|
56707
|
+
...notification.blocks === undefined ? {} : { blocks: notification.blocks }
|
|
56708
|
+
})}
|
|
56285
56709
|
`);
|
|
56286
56710
|
return settle.promise;
|
|
56287
56711
|
}
|
|
@@ -56290,8 +56714,8 @@ function createInboxNotifier(socketPath) {
|
|
|
56290
56714
|
|
|
56291
56715
|
// src/legion-phase-hook.ts
|
|
56292
56716
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
56293
|
-
import { readFileSync as
|
|
56294
|
-
import { dirname as dirname3, join as
|
|
56717
|
+
import { readFileSync as readFileSync7, renameSync as renameSync3, rmSync as rmSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
56718
|
+
import { dirname as dirname3, join as join9 } from "path";
|
|
56295
56719
|
|
|
56296
56720
|
// ../pi-shared/src/phase-reminder.ts
|
|
56297
56721
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
@@ -56756,7 +57180,7 @@ function phaseHookToolDefinition() {
|
|
|
56756
57180
|
};
|
|
56757
57181
|
}
|
|
56758
57182
|
function phaseStallFile(configDir, sessionID) {
|
|
56759
|
-
return
|
|
57183
|
+
return join9(configDir, `legion-phase-stall-${sessionID}.json`);
|
|
56760
57184
|
}
|
|
56761
57185
|
function paneReminderLog(write) {
|
|
56762
57186
|
const line = (level, message, context) => {
|
|
@@ -56771,7 +57195,7 @@ var NO_RECORD = { entry: undefined, recreatedNoticeSent: false };
|
|
|
56771
57195
|
function readPhaseStallRecord(file, log) {
|
|
56772
57196
|
let raw;
|
|
56773
57197
|
try {
|
|
56774
|
-
raw =
|
|
57198
|
+
raw = readFileSync7(file, "utf8");
|
|
56775
57199
|
} catch (error) {
|
|
56776
57200
|
const missing = typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
56777
57201
|
if (!missing) {
|
|
@@ -56809,7 +57233,7 @@ function readPhaseStallRecord(file, log) {
|
|
|
56809
57233
|
};
|
|
56810
57234
|
}
|
|
56811
57235
|
function writePhaseStallRecord(file, entry, recreatedNoticeSent, log) {
|
|
56812
|
-
const temporary =
|
|
57236
|
+
const temporary = join9(dirname3(file), `.legion-phase-stall-${randomUUID4()}`);
|
|
56813
57237
|
try {
|
|
56814
57238
|
writeFileSync4(temporary, JSON.stringify({ ...entry, ...recreatedNoticeSent ? { recreatedNoticeSent } : {} }), { mode: 384 });
|
|
56815
57239
|
renameSync3(temporary, file);
|
|
@@ -57204,84 +57628,699 @@ async function completeHandoff(runtime, args) {
|
|
|
57204
57628
|
if (args.ready === true) {
|
|
57205
57629
|
throw new Error("handoff_complete on a production check is no READY: leave ready out");
|
|
57206
57630
|
}
|
|
57207
|
-
if (claim.role !== check.role) {
|
|
57208
|
-
throw new Error(`${claim.issue}'s production check is the ${check.role}'s, and this session is the ${claim.role}'s: only the role the check records reports on it`);
|
|
57631
|
+
if (claim.role !== check.role) {
|
|
57632
|
+
throw new Error(`${claim.issue}'s production check is the ${check.role}'s, and this session is the ${claim.role}'s: only the role the check records reports on it`);
|
|
57633
|
+
}
|
|
57634
|
+
if (args.verdict === "pending" && args.wake_at === undefined && !check.last) {
|
|
57635
|
+
throw new Error(`handoff_complete's pending report names wake_at, when the next look of ${claim.issue}'s production check is due`);
|
|
57636
|
+
}
|
|
57637
|
+
if (args.wake_at !== undefined && (args.verdict !== "pending" || check.last)) {
|
|
57638
|
+
const why = args.verdict === "pending" ? `look ${check.looks} is the check's last, after which the check closes as not run` : `a ${args.verdict} report closes the check`;
|
|
57639
|
+
throw new Error(`handoff_complete names wake_at and no look follows it: ${why}; send it again without a time`);
|
|
57640
|
+
}
|
|
57641
|
+
}
|
|
57642
|
+
return daemon.handoffComplete({
|
|
57643
|
+
grantId: await mintClaimGrant(daemon, claim),
|
|
57644
|
+
summary: args.summary,
|
|
57645
|
+
verdict: args.verdict ?? "",
|
|
57646
|
+
ready: false,
|
|
57647
|
+
...args.wake_at === undefined ? {} : { wakeAt: args.wake_at }
|
|
57648
|
+
});
|
|
57649
|
+
}
|
|
57650
|
+
if (args.verdict === "pending") {
|
|
57651
|
+
throw new Error("handoff_complete's verdict is pass or fail on a phase completion; pending is a production check's");
|
|
57652
|
+
}
|
|
57653
|
+
if (args.wake_at !== undefined) {
|
|
57654
|
+
throw new Error("handoff_complete's wake_at is a production check's pending report alone; a phase completion carries none");
|
|
57655
|
+
}
|
|
57656
|
+
const report = await runtime.handoffReport(record.phase, claim.role);
|
|
57657
|
+
return daemon.handoffComplete({
|
|
57658
|
+
grantId: await mintClaimGrant(daemon, claim),
|
|
57659
|
+
summary: args.summary,
|
|
57660
|
+
verdict: args.verdict ?? "",
|
|
57661
|
+
ready: args.ready ?? false,
|
|
57662
|
+
...report
|
|
57663
|
+
});
|
|
57664
|
+
}
|
|
57665
|
+
|
|
57666
|
+
// src/legion-tool.ts
|
|
57667
|
+
var LEGION_TOOL_NAME = "legion";
|
|
57668
|
+
function legionToolDefinition() {
|
|
57669
|
+
return {
|
|
57670
|
+
name: LEGION_TOOL_NAME,
|
|
57671
|
+
description: legionToolDescription({
|
|
57672
|
+
envoyPublish: "mcp__legion__envoy_publish",
|
|
57673
|
+
before: PHASE_OP_SENTENCES.readRecord + PHASE_OP_SENTENCES.readReleaseRecords,
|
|
57674
|
+
after: PHASE_OP_SENTENCES.requestBackwardMove
|
|
57675
|
+
}),
|
|
57676
|
+
inputSchema: toJSONSchema(object({ op: _enum2(PHASE_OPS), ...phaseOpParameters(exports_external) }))
|
|
57677
|
+
};
|
|
57678
|
+
}
|
|
57679
|
+
async function executeLegionTool(runtime, input, signal) {
|
|
57680
|
+
const claim = runtime.claim();
|
|
57681
|
+
if (claim === undefined) {
|
|
57682
|
+
throw new Error("this session has no Legion claim yet: the pane registers with the daemon once its first turn starts");
|
|
57683
|
+
}
|
|
57684
|
+
return executePhaseOp({
|
|
57685
|
+
daemon: runtime.daemon,
|
|
57686
|
+
claim,
|
|
57687
|
+
handoffReport: runtime.handoffReport,
|
|
57688
|
+
branchHead: runtime.branchHead,
|
|
57689
|
+
...signal === undefined ? {} : { restart: { signal } }
|
|
57690
|
+
}, input);
|
|
57691
|
+
}
|
|
57692
|
+
|
|
57693
|
+
// src/mod-link.ts
|
|
57694
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
57695
|
+
import { chmod, lstat as lstat2, mkdir as mkdir2, stat as stat3, unlink } from "fs/promises";
|
|
57696
|
+
import {
|
|
57697
|
+
createServer,
|
|
57698
|
+
request as httpRequest
|
|
57699
|
+
} from "http";
|
|
57700
|
+
import { dirname as dirname4 } from "path";
|
|
57701
|
+
|
|
57702
|
+
// hooks/mod-protocol.ts
|
|
57703
|
+
var MOD_HEALTH_MARKER = "claude-envoy-mod";
|
|
57704
|
+
var MOD_POLL_HOLD_MS = 5000;
|
|
57705
|
+
var MOD_URL_BASE = "http://claude-envoy";
|
|
57706
|
+
var MOD_ROUTE_HEALTH = "/healthz";
|
|
57707
|
+
var MOD_ROUTE_EVENTS = "/events";
|
|
57708
|
+
var MOD_ROUTE_RESULTS = "/results";
|
|
57709
|
+
var MOD_ROUTE_REQUESTS = "/requests";
|
|
57710
|
+
var MOD_SOCKET_PATH_MAX_BYTES = 100;
|
|
57711
|
+
var MOD_STATUS_TAKEN = 409;
|
|
57712
|
+
var MOD_INSTANCE_HEADER = "x-claude-envoy-mod";
|
|
57713
|
+
var MOD_SOCKET_RUNTIME_VARIABLE = "XDG_RUNTIME_DIR";
|
|
57714
|
+
var MOD_SOCKET_CONFIG_VARIABLE = "CLAUDE_CONFIG_DIR";
|
|
57715
|
+
var MOD_SOCKET_HOME_VARIABLE = "HOME";
|
|
57716
|
+
var MOD_SOCKET_HOME_CONFIG = "/.claude";
|
|
57717
|
+
var MOD_PLUGIN_NAME = "claude-envoy";
|
|
57718
|
+
var MOD_SOCKET_DIRECTORY = MOD_PLUGIN_NAME;
|
|
57719
|
+
var MOD_HASH_PRIME_HIGH = 256;
|
|
57720
|
+
var MOD_HASH_PRIME_LOW = 403;
|
|
57721
|
+
function utf8Length(value) {
|
|
57722
|
+
let bytes = 0;
|
|
57723
|
+
for (const character of value) {
|
|
57724
|
+
const code = character.codePointAt(0) ?? 0;
|
|
57725
|
+
bytes += code < 128 ? 1 : code < 2048 ? 2 : code < 65536 ? 3 : 4;
|
|
57726
|
+
}
|
|
57727
|
+
return bytes;
|
|
57728
|
+
}
|
|
57729
|
+
function fnv1a32(value, basis) {
|
|
57730
|
+
let hash = basis;
|
|
57731
|
+
for (let index = 0;index < value.length; index += 1) {
|
|
57732
|
+
hash = (hash ^ value.charCodeAt(index)) >>> 0;
|
|
57733
|
+
const low = hash & 65535;
|
|
57734
|
+
const high = hash >>> 16;
|
|
57735
|
+
const carried = high * MOD_HASH_PRIME_LOW + low * MOD_HASH_PRIME_HIGH & 65535;
|
|
57736
|
+
hash = low * MOD_HASH_PRIME_LOW + (carried << 16) >>> 0;
|
|
57737
|
+
}
|
|
57738
|
+
return hash;
|
|
57739
|
+
}
|
|
57740
|
+
function hashedSessionId(sessionId) {
|
|
57741
|
+
const first = fnv1a32(sessionId, 2166136261);
|
|
57742
|
+
const second = fnv1a32(sessionId, 3421674724);
|
|
57743
|
+
return `${first.toString(16).padStart(8, "0")}${second.toString(16).padStart(8, "0")}`;
|
|
57744
|
+
}
|
|
57745
|
+
function isPathSegment(value) {
|
|
57746
|
+
return value.length > 0 && value !== "." && value !== ".." && !value.includes("/") && !value.includes("\\") && !value.includes("\x00");
|
|
57747
|
+
}
|
|
57748
|
+
function withoutTrailingSlash(base) {
|
|
57749
|
+
let end = base.length;
|
|
57750
|
+
while (end > 1 && base[end - 1] === "/")
|
|
57751
|
+
end -= 1;
|
|
57752
|
+
return end === 1 ? "" : base.slice(0, end);
|
|
57753
|
+
}
|
|
57754
|
+
function absoluteBase(value) {
|
|
57755
|
+
if (value === undefined || value.length === 0 || !value.startsWith("/"))
|
|
57756
|
+
return;
|
|
57757
|
+
return withoutTrailingSlash(value);
|
|
57758
|
+
}
|
|
57759
|
+
function socketBases(env) {
|
|
57760
|
+
const bases = [];
|
|
57761
|
+
const runtime = absoluteBase(env[MOD_SOCKET_RUNTIME_VARIABLE]);
|
|
57762
|
+
if (runtime !== undefined)
|
|
57763
|
+
bases.push(runtime);
|
|
57764
|
+
const configured = absoluteBase(env[MOD_SOCKET_CONFIG_VARIABLE]);
|
|
57765
|
+
const home = absoluteBase(env[MOD_SOCKET_HOME_VARIABLE]);
|
|
57766
|
+
if (configured !== undefined)
|
|
57767
|
+
bases.push(configured);
|
|
57768
|
+
else if (home !== undefined)
|
|
57769
|
+
bases.push(`${home}${MOD_SOCKET_HOME_CONFIG}`);
|
|
57770
|
+
return bases;
|
|
57771
|
+
}
|
|
57772
|
+
function resolveModSocketPath(sessionId, env) {
|
|
57773
|
+
if (!isPathSegment(sessionId)) {
|
|
57774
|
+
return {
|
|
57775
|
+
refusal: `${JSON.stringify(sessionId)} is not a plain path segment, so it names no socket of its own`
|
|
57776
|
+
};
|
|
57777
|
+
}
|
|
57778
|
+
const file = `/${MOD_SOCKET_DIRECTORY}/ce-${hashedSessionId(sessionId)}.sock`;
|
|
57779
|
+
const tried = [];
|
|
57780
|
+
for (const base of socketBases(env)) {
|
|
57781
|
+
const path = `${base}${file}`;
|
|
57782
|
+
if (utf8Length(path) <= MOD_SOCKET_PATH_MAX_BYTES)
|
|
57783
|
+
return { path };
|
|
57784
|
+
tried.push(path);
|
|
57785
|
+
}
|
|
57786
|
+
return {
|
|
57787
|
+
refusal: tried.length === 0 ? `none of ${MOD_SOCKET_RUNTIME_VARIABLE}, ${MOD_SOCKET_CONFIG_VARIABLE} and ${MOD_SOCKET_HOME_VARIABLE} names an absolute directory only this user can create in, and a session's socket never goes in a base other users share` : `every private base makes a socket path longer than ${MOD_SOCKET_PATH_MAX_BYTES} bytes (${tried.join(", ")})`
|
|
57788
|
+
};
|
|
57789
|
+
}
|
|
57790
|
+
|
|
57791
|
+
// src/mod-link.ts
|
|
57792
|
+
var MOD_REQUEST_TIMEOUT_MS = 30000;
|
|
57793
|
+
var MOD_PROBE_TIMEOUT_MS = 1000;
|
|
57794
|
+
var MOD_EVENT_BUFFER = 2048;
|
|
57795
|
+
var MOD_HOST = new URL(MOD_URL_BASE).host;
|
|
57796
|
+
function probeHealth(socketPath, timeoutMs) {
|
|
57797
|
+
const { promise, resolve } = Promise.withResolvers();
|
|
57798
|
+
const probe = httpRequest({
|
|
57799
|
+
socketPath,
|
|
57800
|
+
path: MOD_ROUTE_HEALTH,
|
|
57801
|
+
method: "GET",
|
|
57802
|
+
headers: { host: MOD_HOST },
|
|
57803
|
+
timeout: timeoutMs
|
|
57804
|
+
}, (response) => {
|
|
57805
|
+
let body = "";
|
|
57806
|
+
response.setEncoding("utf8");
|
|
57807
|
+
response.on("data", (chunk) => {
|
|
57808
|
+
body += chunk;
|
|
57809
|
+
});
|
|
57810
|
+
response.on("end", () => {
|
|
57811
|
+
if (response.statusCode !== 200) {
|
|
57812
|
+
resolve(false);
|
|
57813
|
+
return;
|
|
57814
|
+
}
|
|
57815
|
+
let parsed;
|
|
57816
|
+
try {
|
|
57817
|
+
parsed = JSON.parse(body);
|
|
57818
|
+
} catch {
|
|
57819
|
+
resolve(false);
|
|
57820
|
+
return;
|
|
57821
|
+
}
|
|
57822
|
+
if (typeof parsed !== "object" || parsed === null || !("link" in parsed)) {
|
|
57823
|
+
resolve(false);
|
|
57824
|
+
return;
|
|
57825
|
+
}
|
|
57826
|
+
resolve(parsed.link === MOD_HEALTH_MARKER);
|
|
57827
|
+
});
|
|
57828
|
+
});
|
|
57829
|
+
probe.on("error", () => resolve(false));
|
|
57830
|
+
probe.on("timeout", () => {
|
|
57831
|
+
probe.destroy();
|
|
57832
|
+
resolve(false);
|
|
57833
|
+
});
|
|
57834
|
+
probe.end();
|
|
57835
|
+
return promise;
|
|
57836
|
+
}
|
|
57837
|
+
function readEvents(body) {
|
|
57838
|
+
if (typeof body !== "object" || body === null || !("events" in body))
|
|
57839
|
+
return;
|
|
57840
|
+
const events = body.events;
|
|
57841
|
+
if (!Array.isArray(events))
|
|
57842
|
+
return;
|
|
57843
|
+
return events.filter((event) => typeof event === "object" && event !== null && ("kind" in event) && typeof event.kind === "string");
|
|
57844
|
+
}
|
|
57845
|
+
function readResults(body) {
|
|
57846
|
+
if (typeof body !== "object" || body === null || !("results" in body))
|
|
57847
|
+
return;
|
|
57848
|
+
const results = body.results;
|
|
57849
|
+
if (!Array.isArray(results))
|
|
57850
|
+
return;
|
|
57851
|
+
return results.filter((result) => typeof result === "object" && result !== null && ("id" in result) && typeof result.id === "string" && ("ok" in result) && typeof result.ok === "boolean");
|
|
57852
|
+
}
|
|
57853
|
+
function createEventStream(limit) {
|
|
57854
|
+
const buffered = [];
|
|
57855
|
+
let waiting;
|
|
57856
|
+
let ended = false;
|
|
57857
|
+
const iterator = {
|
|
57858
|
+
[Symbol.asyncIterator]() {
|
|
57859
|
+
return iterator;
|
|
57860
|
+
},
|
|
57861
|
+
next() {
|
|
57862
|
+
const next = buffered.shift();
|
|
57863
|
+
if (next !== undefined)
|
|
57864
|
+
return Promise.resolve({ value: next, done: false });
|
|
57865
|
+
if (ended)
|
|
57866
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
57867
|
+
const { promise, resolve } = Promise.withResolvers();
|
|
57868
|
+
waiting = resolve;
|
|
57869
|
+
return promise;
|
|
57870
|
+
}
|
|
57871
|
+
};
|
|
57872
|
+
return {
|
|
57873
|
+
iterator,
|
|
57874
|
+
push(event) {
|
|
57875
|
+
if (ended)
|
|
57876
|
+
return;
|
|
57877
|
+
const settle = waiting;
|
|
57878
|
+
if (settle !== undefined) {
|
|
57879
|
+
waiting = undefined;
|
|
57880
|
+
settle({ value: event, done: false });
|
|
57881
|
+
return;
|
|
57882
|
+
}
|
|
57883
|
+
buffered.push(event);
|
|
57884
|
+
if (buffered.length > limit)
|
|
57885
|
+
buffered.splice(0, buffered.length - limit);
|
|
57886
|
+
},
|
|
57887
|
+
end() {
|
|
57888
|
+
if (ended)
|
|
57889
|
+
return;
|
|
57890
|
+
ended = true;
|
|
57891
|
+
const settle = waiting;
|
|
57892
|
+
if (settle !== undefined) {
|
|
57893
|
+
waiting = undefined;
|
|
57894
|
+
settle({ value: undefined, done: true });
|
|
57895
|
+
}
|
|
57896
|
+
}
|
|
57897
|
+
};
|
|
57898
|
+
}
|
|
57899
|
+
async function exists(path) {
|
|
57900
|
+
try {
|
|
57901
|
+
await stat3(path);
|
|
57902
|
+
return true;
|
|
57903
|
+
} catch {
|
|
57904
|
+
return false;
|
|
57905
|
+
}
|
|
57906
|
+
}
|
|
57907
|
+
async function privateDirectory(path) {
|
|
57908
|
+
await mkdir2(path, { recursive: true, mode: 448 });
|
|
57909
|
+
const directory = await lstat2(path);
|
|
57910
|
+
if (!directory.isDirectory()) {
|
|
57911
|
+
throw new Error(`${path} is not a directory of its own, so the socket has no private home`);
|
|
57912
|
+
}
|
|
57913
|
+
const user = process.getuid?.();
|
|
57914
|
+
if (user !== undefined && directory.uid !== user) {
|
|
57915
|
+
throw new Error(`${path} belongs to uid ${directory.uid}, not to this user (uid ${user})`);
|
|
57916
|
+
}
|
|
57917
|
+
if ((directory.mode & 63) !== 0) {
|
|
57918
|
+
throw new Error(`${path} is mode ${(directory.mode & 511).toString(8)}, which lets another user into this session's socket; chmod 700 it`);
|
|
57919
|
+
}
|
|
57920
|
+
}
|
|
57921
|
+
async function bind(server, socketPath) {
|
|
57922
|
+
await privateDirectory(dirname4(socketPath));
|
|
57923
|
+
if (await exists(socketPath)) {
|
|
57924
|
+
if (await probeHealth(socketPath, MOD_PROBE_TIMEOUT_MS)) {
|
|
57925
|
+
throw new Error(`another claude-envoy server already serves this session at ${socketPath}`);
|
|
57926
|
+
}
|
|
57927
|
+
await unlink(socketPath).catch(() => {
|
|
57928
|
+
return;
|
|
57929
|
+
});
|
|
57930
|
+
}
|
|
57931
|
+
const { promise, resolve, reject } = Promise.withResolvers();
|
|
57932
|
+
server.once("error", reject);
|
|
57933
|
+
server.listen(socketPath, () => {
|
|
57934
|
+
server.removeListener("error", reject);
|
|
57935
|
+
resolve();
|
|
57936
|
+
});
|
|
57937
|
+
await promise;
|
|
57938
|
+
await chmod(socketPath, 384);
|
|
57939
|
+
}
|
|
57940
|
+
async function startModLink(options) {
|
|
57941
|
+
const resolved = resolveModSocketPath(options.sessionId, options.env);
|
|
57942
|
+
if (resolved.path === undefined) {
|
|
57943
|
+
throw new Error(`no socket path for this session: ${resolved.refusal}`);
|
|
57944
|
+
}
|
|
57945
|
+
const socketPath = resolved.path;
|
|
57946
|
+
const stream = createEventStream(MOD_EVENT_BUFFER);
|
|
57947
|
+
const queued = [];
|
|
57948
|
+
const held = new Set;
|
|
57949
|
+
const pending = new Map;
|
|
57950
|
+
const sessionIdListeners = [];
|
|
57951
|
+
const connectedListeners = [];
|
|
57952
|
+
const warn = options.warn ?? ((text) => {
|
|
57953
|
+
process.stderr.write(`claude-envoy: ${text}
|
|
57954
|
+
`);
|
|
57955
|
+
});
|
|
57956
|
+
let instance;
|
|
57957
|
+
let reportedSessionId;
|
|
57958
|
+
let lastPollAt = 0;
|
|
57959
|
+
let closed = false;
|
|
57960
|
+
const isConnected = () => !closed && (held.size > 0 || Date.now() - lastPollAt < MOD_POLL_HOLD_MS * 2);
|
|
57961
|
+
const heard = () => {
|
|
57962
|
+
const before = isConnected();
|
|
57963
|
+
lastPollAt = Date.now();
|
|
57964
|
+
if (!before)
|
|
57965
|
+
for (const listener of connectedListeners)
|
|
57966
|
+
listener();
|
|
57967
|
+
};
|
|
57968
|
+
const reportSessionId = (sessionId) => {
|
|
57969
|
+
if (sessionId === reportedSessionId)
|
|
57970
|
+
return;
|
|
57971
|
+
reportedSessionId = sessionId;
|
|
57972
|
+
for (const listener of sessionIdListeners)
|
|
57973
|
+
listener(sessionId);
|
|
57974
|
+
};
|
|
57975
|
+
const wake = () => {
|
|
57976
|
+
const [poll] = held;
|
|
57977
|
+
if (poll === undefined || queued.length === 0)
|
|
57978
|
+
return;
|
|
57979
|
+
held.delete(poll);
|
|
57980
|
+
clearTimeout(poll.timer);
|
|
57981
|
+
poll.answer(queued.splice(0, queued.length));
|
|
57982
|
+
};
|
|
57983
|
+
const answerJson = (response, status, body) => {
|
|
57984
|
+
const text = JSON.stringify(body);
|
|
57985
|
+
response.writeHead(status, {
|
|
57986
|
+
"content-type": "application/json",
|
|
57987
|
+
"content-length": Buffer.byteLength(text)
|
|
57988
|
+
});
|
|
57989
|
+
response.end(text);
|
|
57990
|
+
};
|
|
57991
|
+
const readBody = (incoming) => {
|
|
57992
|
+
const { promise, resolve, reject } = Promise.withResolvers();
|
|
57993
|
+
let body = "";
|
|
57994
|
+
incoming.setEncoding("utf8");
|
|
57995
|
+
incoming.on("data", (chunk) => {
|
|
57996
|
+
body += chunk;
|
|
57997
|
+
});
|
|
57998
|
+
incoming.on("end", () => resolve(body));
|
|
57999
|
+
incoming.on("error", reject);
|
|
58000
|
+
return promise;
|
|
58001
|
+
};
|
|
58002
|
+
const serve = async (incoming, response) => {
|
|
58003
|
+
const route = (incoming.url ?? "").split("?")[0];
|
|
58004
|
+
if (route === MOD_ROUTE_HEALTH && incoming.method === "GET") {
|
|
58005
|
+
answerJson(response, 200, { link: MOD_HEALTH_MARKER });
|
|
58006
|
+
return;
|
|
58007
|
+
}
|
|
58008
|
+
if (incoming.method !== "POST" || route !== MOD_ROUTE_EVENTS && route !== MOD_ROUTE_RESULTS && route !== MOD_ROUTE_REQUESTS) {
|
|
58009
|
+
answerJson(response, 404, { error: `no route ${incoming.method ?? ""} ${route ?? ""}` });
|
|
58010
|
+
return;
|
|
58011
|
+
}
|
|
58012
|
+
const named = incoming.headers[MOD_INSTANCE_HEADER];
|
|
58013
|
+
const mod = typeof named === "string" ? named : undefined;
|
|
58014
|
+
if (mod !== undefined) {
|
|
58015
|
+
instance ??= mod;
|
|
58016
|
+
if (instance !== mod) {
|
|
58017
|
+
warn(`a second Claude Code mod (${mod}) posted to a link mod ${instance} holds, so two processes booted on one session id; it is refused and stops (${socketPath})`);
|
|
58018
|
+
answerJson(response, MOD_STATUS_TAKEN, {
|
|
58019
|
+
error: `this session's link belongs to mod ${instance}`
|
|
58020
|
+
});
|
|
58021
|
+
return;
|
|
58022
|
+
}
|
|
58023
|
+
}
|
|
58024
|
+
const text = await readBody(incoming);
|
|
58025
|
+
let body;
|
|
58026
|
+
try {
|
|
58027
|
+
body = text.length === 0 ? {} : JSON.parse(text);
|
|
58028
|
+
} catch {
|
|
58029
|
+
answerJson(response, 400, { error: "the body is not JSON" });
|
|
58030
|
+
return;
|
|
58031
|
+
}
|
|
58032
|
+
if (route === MOD_ROUTE_EVENTS) {
|
|
58033
|
+
heard();
|
|
58034
|
+
const events = readEvents(body);
|
|
58035
|
+
if (events === undefined) {
|
|
58036
|
+
answerJson(response, 400, { error: "the body carries no events" });
|
|
58037
|
+
return;
|
|
58038
|
+
}
|
|
58039
|
+
for (const event of events) {
|
|
58040
|
+
if (event.kind === "hello" || event.kind === "session-id")
|
|
58041
|
+
reportSessionId(event.sessionId);
|
|
58042
|
+
stream.push(event);
|
|
58043
|
+
}
|
|
58044
|
+
answerJson(response, 200, { ok: true });
|
|
58045
|
+
return;
|
|
58046
|
+
}
|
|
58047
|
+
if (route === MOD_ROUTE_RESULTS) {
|
|
58048
|
+
const results = readResults(body);
|
|
58049
|
+
if (results === undefined) {
|
|
58050
|
+
answerJson(response, 400, { error: "the body carries no results" });
|
|
58051
|
+
return;
|
|
58052
|
+
}
|
|
58053
|
+
for (const result of results) {
|
|
58054
|
+
const waiting = pending.get(result.id);
|
|
58055
|
+
if (waiting === undefined)
|
|
58056
|
+
continue;
|
|
58057
|
+
pending.delete(result.id);
|
|
58058
|
+
clearTimeout(waiting.timer);
|
|
58059
|
+
if (result.ok)
|
|
58060
|
+
waiting.settle.resolve(result.value);
|
|
58061
|
+
else
|
|
58062
|
+
waiting.settle.reject(new Error(`the mod could not ${waiting.call}: ${result.error}`));
|
|
58063
|
+
}
|
|
58064
|
+
answerJson(response, 200, { ok: true });
|
|
58065
|
+
return;
|
|
58066
|
+
}
|
|
58067
|
+
heard();
|
|
58068
|
+
if (queued.length > 0 || closed) {
|
|
58069
|
+
answerJson(response, 200, {
|
|
58070
|
+
requests: queued.splice(0, queued.length)
|
|
58071
|
+
});
|
|
58072
|
+
return;
|
|
58073
|
+
}
|
|
58074
|
+
const poll = {
|
|
58075
|
+
answer: (requests) => answerJson(response, 200, { requests }),
|
|
58076
|
+
timer: setTimeout(() => {
|
|
58077
|
+
held.delete(poll);
|
|
58078
|
+
answerJson(response, 200, { requests: [] });
|
|
58079
|
+
}, MOD_POLL_HOLD_MS)
|
|
58080
|
+
};
|
|
58081
|
+
poll.timer.unref();
|
|
58082
|
+
held.add(poll);
|
|
58083
|
+
incoming.on("close", () => {
|
|
58084
|
+
if (held.delete(poll))
|
|
58085
|
+
clearTimeout(poll.timer);
|
|
58086
|
+
});
|
|
58087
|
+
};
|
|
58088
|
+
const server = createServer((incoming, response) => {
|
|
58089
|
+
serve(incoming, response).catch(() => {
|
|
58090
|
+
if (response.headersSent)
|
|
58091
|
+
response.end();
|
|
58092
|
+
else
|
|
58093
|
+
answerJson(response, 500, { error: "the mod link failed" });
|
|
58094
|
+
});
|
|
58095
|
+
});
|
|
58096
|
+
await bind(server, socketPath);
|
|
58097
|
+
const close = async () => {
|
|
58098
|
+
if (closed)
|
|
58099
|
+
return;
|
|
58100
|
+
closed = true;
|
|
58101
|
+
for (const waiting of pending.values()) {
|
|
58102
|
+
clearTimeout(waiting.timer);
|
|
58103
|
+
waiting.settle.reject(new Error("the mod link closed before the mod answered"));
|
|
58104
|
+
}
|
|
58105
|
+
pending.clear();
|
|
58106
|
+
queued.length = 0;
|
|
58107
|
+
for (const poll of held) {
|
|
58108
|
+
clearTimeout(poll.timer);
|
|
58109
|
+
poll.answer([]);
|
|
58110
|
+
}
|
|
58111
|
+
held.clear();
|
|
58112
|
+
stream.end();
|
|
58113
|
+
server.closeAllConnections();
|
|
58114
|
+
const { promise, resolve } = Promise.withResolvers();
|
|
58115
|
+
server.close(() => resolve());
|
|
58116
|
+
await promise;
|
|
58117
|
+
await unlink(socketPath).catch(() => {
|
|
58118
|
+
return;
|
|
58119
|
+
});
|
|
58120
|
+
};
|
|
58121
|
+
return {
|
|
58122
|
+
socketPath,
|
|
58123
|
+
events: stream.iterator,
|
|
58124
|
+
request(call, args, requestOptions) {
|
|
58125
|
+
if (closed)
|
|
58126
|
+
return Promise.reject(new Error("the mod link is closed"));
|
|
58127
|
+
const timeoutMs = requestOptions?.timeoutMs ?? MOD_REQUEST_TIMEOUT_MS;
|
|
58128
|
+
const id = randomUUID5();
|
|
58129
|
+
const settle = Promise.withResolvers();
|
|
58130
|
+
const request = { id, call, args };
|
|
58131
|
+
const timer = setTimeout(() => {
|
|
58132
|
+
pending.delete(id);
|
|
58133
|
+
const waiting = queued.indexOf(request);
|
|
58134
|
+
if (waiting !== -1)
|
|
58135
|
+
queued.splice(waiting, 1);
|
|
58136
|
+
settle.reject(new Error(`the mod did not answer ${call} within ${timeoutMs} ms`));
|
|
58137
|
+
}, timeoutMs);
|
|
58138
|
+
timer.unref();
|
|
58139
|
+
pending.set(id, { call, settle, timer });
|
|
58140
|
+
queued.push(request);
|
|
58141
|
+
wake();
|
|
58142
|
+
return settle.promise;
|
|
58143
|
+
},
|
|
58144
|
+
sessionId: () => reportedSessionId,
|
|
58145
|
+
onSessionId(listener) {
|
|
58146
|
+
sessionIdListeners.push(listener);
|
|
58147
|
+
},
|
|
58148
|
+
connected: isConnected,
|
|
58149
|
+
onConnected(listener) {
|
|
58150
|
+
connectedListeners.push(listener);
|
|
58151
|
+
},
|
|
58152
|
+
close
|
|
58153
|
+
};
|
|
58154
|
+
}
|
|
58155
|
+
|
|
58156
|
+
// src/mod-session.ts
|
|
58157
|
+
var SHOW_TIMEOUT_MS = 30000;
|
|
58158
|
+
var COMMAND_TIMEOUT_MS2 = 24 * 60 * 60 * 1000;
|
|
58159
|
+
var FORK_TIMEOUT_MS = 10 * 60 * 1000;
|
|
58160
|
+
var COMMAND_OUTPUT_MS = 2000;
|
|
58161
|
+
var COMMAND_ECHO_MS = 1000;
|
|
58162
|
+
var CLAIMED_ROWS_MAX = 64;
|
|
58163
|
+
var MODEL_CONFIG_KEY = "model";
|
|
58164
|
+
function resultOutput(result) {
|
|
58165
|
+
if (result.text !== undefined && result.text.trim() !== "")
|
|
58166
|
+
return result.text;
|
|
58167
|
+
const context = result.context?.join(`
|
|
58168
|
+
|
|
58169
|
+
`);
|
|
58170
|
+
return context !== undefined && context.trim() !== "" ? context : undefined;
|
|
58171
|
+
}
|
|
58172
|
+
function modSession(options) {
|
|
58173
|
+
const { link, warn } = options;
|
|
58174
|
+
const commandRuns = [];
|
|
58175
|
+
const claimedEchoes = new Set;
|
|
58176
|
+
const claimedUnreadPrompts = new Set;
|
|
58177
|
+
const claimedAppends = new Set;
|
|
58178
|
+
let behindEcho;
|
|
58179
|
+
const outputMs = options.commandOutputMs ?? COMMAND_OUTPUT_MS;
|
|
58180
|
+
const echoMs = options.commandEchoMs ?? COMMAND_ECHO_MS;
|
|
58181
|
+
const showMs = options.showMs ?? SHOW_TIMEOUT_MS;
|
|
58182
|
+
const runsCommandRow = (row) => claimedEchoes.has(row);
|
|
58183
|
+
const runsUnreadPrompt = (row) => claimedUnreadPrompts.has(row);
|
|
58184
|
+
const appendedRow = (row) => claimedAppends.has(row);
|
|
58185
|
+
const commandRow = (uuid, content) => {
|
|
58186
|
+
if (claudeEchoedCommandLine(content) !== undefined) {
|
|
58187
|
+
const claiming = commandRuns.find((waiting) => waiting.claiming);
|
|
58188
|
+
behindEcho = claiming;
|
|
58189
|
+
if (claiming === undefined)
|
|
58190
|
+
return;
|
|
58191
|
+
claiming.claiming = false;
|
|
58192
|
+
claiming.echo = uuid;
|
|
58193
|
+
rememberBounded(claimedEchoes, uuid, CLAIMED_ROWS_MAX);
|
|
58194
|
+
claiming.claimed?.();
|
|
58195
|
+
return;
|
|
58196
|
+
}
|
|
58197
|
+
const run = behindEcho;
|
|
58198
|
+
behindEcho = undefined;
|
|
58199
|
+
const output = claudeCommandOutput(content);
|
|
58200
|
+
if (output === undefined || run === undefined)
|
|
58201
|
+
return;
|
|
58202
|
+
run.output = output;
|
|
58203
|
+
run.arrived?.();
|
|
58204
|
+
};
|
|
58205
|
+
const endRun = (run) => {
|
|
58206
|
+
const at = commandRuns.indexOf(run);
|
|
58207
|
+
if (at !== -1)
|
|
58208
|
+
commandRuns.splice(at, 1);
|
|
58209
|
+
};
|
|
58210
|
+
const commandRowsOf = async (run) => {
|
|
58211
|
+
try {
|
|
58212
|
+
const echoed = Promise.withResolvers();
|
|
58213
|
+
const echoTimer = setTimeout(echoed.resolve, echoMs);
|
|
58214
|
+
echoTimer.unref();
|
|
58215
|
+
run.claimed = () => {
|
|
58216
|
+
clearTimeout(echoTimer);
|
|
58217
|
+
echoed.resolve();
|
|
58218
|
+
};
|
|
58219
|
+
run.claiming = true;
|
|
58220
|
+
await echoed.promise;
|
|
58221
|
+
run.claiming = false;
|
|
58222
|
+
if (run.echo === undefined)
|
|
58223
|
+
return "";
|
|
58224
|
+
if (run.output === undefined) {
|
|
58225
|
+
const waited = Promise.withResolvers();
|
|
58226
|
+
const timer = setTimeout(waited.resolve, outputMs);
|
|
58227
|
+
timer.unref();
|
|
58228
|
+
run.arrived = () => {
|
|
58229
|
+
clearTimeout(timer);
|
|
58230
|
+
waited.resolve();
|
|
58231
|
+
};
|
|
58232
|
+
await waited.promise;
|
|
58233
|
+
}
|
|
58234
|
+
return run.output ?? "";
|
|
58235
|
+
} finally {
|
|
58236
|
+
endRun(run);
|
|
58237
|
+
}
|
|
58238
|
+
};
|
|
58239
|
+
const written = async (message, content, eventId) => await link.request("session.message", { content, message, ...eventId === undefined ? {} : { eventId } }, { timeoutMs: showMs });
|
|
58240
|
+
return {
|
|
58241
|
+
connected: () => link.connected(),
|
|
58242
|
+
observe(event) {
|
|
58243
|
+
if (event.kind !== "append")
|
|
58244
|
+
return;
|
|
58245
|
+
if (event.door === "command")
|
|
58246
|
+
commandRow(event.uuid, event.message.content);
|
|
58247
|
+
if (event.wrote === "message")
|
|
58248
|
+
rememberBounded(claimedAppends, event.uuid, CLAIMED_ROWS_MAX);
|
|
58249
|
+
if (event.wrote === "pointer") {
|
|
58250
|
+
rememberBounded(claimedUnreadPrompts, event.uuid, CLAIMED_ROWS_MAX);
|
|
58251
|
+
}
|
|
58252
|
+
},
|
|
58253
|
+
async show({ text, content, eventId }) {
|
|
58254
|
+
const answer = await written({ kind: "card", text }, content, eventId);
|
|
58255
|
+
if (answer.deny !== undefined)
|
|
58256
|
+
throw new Error(`the session refused the card: ${answer.deny}`);
|
|
58257
|
+
},
|
|
58258
|
+
async deliver({ text, messageId, content, eventId, failed }) {
|
|
58259
|
+
let answer;
|
|
58260
|
+
try {
|
|
58261
|
+
answer = await written({ kind: "person", messageId, text }, content, eventId);
|
|
58262
|
+
} catch (error) {
|
|
58263
|
+
warn(`a message from Dispatch did not enter this session \u2014 ${messageFor(error)}`);
|
|
58264
|
+
throw error;
|
|
58265
|
+
}
|
|
58266
|
+
if (answer.deny !== undefined) {
|
|
58267
|
+
const refusal = new Error(`the session dropped the prompt: ${answer.deny}`);
|
|
58268
|
+
if (answer.as !== "prompt" || failed === undefined)
|
|
58269
|
+
throw refusal;
|
|
58270
|
+
warn(`a message from Dispatch did not enter this session \u2014 ${messageFor(refusal)}`);
|
|
58271
|
+
failed(refusal);
|
|
58272
|
+
return;
|
|
57209
58273
|
}
|
|
57210
|
-
|
|
57211
|
-
|
|
58274
|
+
return answer.as === "row" ? answer.uuid : undefined;
|
|
58275
|
+
},
|
|
58276
|
+
appendedRow,
|
|
58277
|
+
runsCommandRow,
|
|
58278
|
+
runsUnreadPrompt,
|
|
58279
|
+
async runCommand(command, args) {
|
|
58280
|
+
const run = { claiming: false };
|
|
58281
|
+
commandRuns.push(run);
|
|
58282
|
+
let result;
|
|
58283
|
+
try {
|
|
58284
|
+
result = await link.request("command.run", { command, ...args === "" ? {} : { args } }, { timeoutMs: COMMAND_TIMEOUT_MS2 });
|
|
58285
|
+
} catch (error) {
|
|
58286
|
+
endRun(run);
|
|
58287
|
+
throw error;
|
|
57212
58288
|
}
|
|
57213
|
-
|
|
57214
|
-
|
|
57215
|
-
|
|
58289
|
+
const rows = commandRowsOf(run);
|
|
58290
|
+
return resultOutput(result) ?? await rows;
|
|
58291
|
+
},
|
|
58292
|
+
async fork(prompt) {
|
|
58293
|
+
const result = await link.request("model.fork", { prompt }, { timeoutMs: FORK_TIMEOUT_MS });
|
|
58294
|
+
if (!result.isAnswered)
|
|
58295
|
+
throw new Error(result.reason);
|
|
58296
|
+
return result.text;
|
|
58297
|
+
},
|
|
58298
|
+
async setModel(model) {
|
|
58299
|
+
const result = await link.request("config.set", { key: MODEL_CONFIG_KEY, value: model });
|
|
58300
|
+
if (result.deny !== undefined) {
|
|
58301
|
+
throw new Error(`the session refused the model: ${result.deny}`);
|
|
57216
58302
|
}
|
|
58303
|
+
return result.value;
|
|
58304
|
+
},
|
|
58305
|
+
async setEffort(effort) {
|
|
58306
|
+
const result = await link.request("set-effort", { effort });
|
|
58307
|
+
return result.effort;
|
|
57217
58308
|
}
|
|
57218
|
-
return daemon.handoffComplete({
|
|
57219
|
-
grantId: await mintClaimGrant(daemon, claim),
|
|
57220
|
-
summary: args.summary,
|
|
57221
|
-
verdict: args.verdict ?? "",
|
|
57222
|
-
ready: false,
|
|
57223
|
-
...args.wake_at === undefined ? {} : { wakeAt: args.wake_at }
|
|
57224
|
-
});
|
|
57225
|
-
}
|
|
57226
|
-
if (args.verdict === "pending") {
|
|
57227
|
-
throw new Error("handoff_complete's verdict is pass or fail on a phase completion; pending is a production check's");
|
|
57228
|
-
}
|
|
57229
|
-
if (args.wake_at !== undefined) {
|
|
57230
|
-
throw new Error("handoff_complete's wake_at is a production check's pending report alone; a phase completion carries none");
|
|
57231
|
-
}
|
|
57232
|
-
const report = await runtime.handoffReport(record.phase, claim.role);
|
|
57233
|
-
return daemon.handoffComplete({
|
|
57234
|
-
grantId: await mintClaimGrant(daemon, claim),
|
|
57235
|
-
summary: args.summary,
|
|
57236
|
-
verdict: args.verdict ?? "",
|
|
57237
|
-
ready: args.ready ?? false,
|
|
57238
|
-
...report
|
|
57239
|
-
});
|
|
57240
|
-
}
|
|
57241
|
-
|
|
57242
|
-
// src/legion-tool.ts
|
|
57243
|
-
var LEGION_TOOL_NAME = "legion";
|
|
57244
|
-
function legionToolDefinition() {
|
|
57245
|
-
return {
|
|
57246
|
-
name: LEGION_TOOL_NAME,
|
|
57247
|
-
description: legionToolDescription({
|
|
57248
|
-
envoyPublish: "mcp__legion__envoy_publish",
|
|
57249
|
-
before: PHASE_OP_SENTENCES.readRecord + PHASE_OP_SENTENCES.readReleaseRecords,
|
|
57250
|
-
after: PHASE_OP_SENTENCES.requestBackwardMove
|
|
57251
|
-
}),
|
|
57252
|
-
inputSchema: toJSONSchema(object({ op: _enum2(PHASE_OPS), ...phaseOpParameters(exports_external) }))
|
|
57253
58309
|
};
|
|
57254
58310
|
}
|
|
57255
|
-
async function executeLegionTool(runtime, input, signal) {
|
|
57256
|
-
const claim = runtime.claim();
|
|
57257
|
-
if (claim === undefined) {
|
|
57258
|
-
throw new Error("this session has no Legion claim yet: the pane registers with the daemon once its first turn starts");
|
|
57259
|
-
}
|
|
57260
|
-
return executePhaseOp({
|
|
57261
|
-
daemon: runtime.daemon,
|
|
57262
|
-
claim,
|
|
57263
|
-
handoffReport: runtime.handoffReport,
|
|
57264
|
-
branchHead: runtime.branchHead,
|
|
57265
|
-
...signal === undefined ? {} : { restart: { signal } }
|
|
57266
|
-
}, input);
|
|
57267
|
-
}
|
|
57268
58311
|
|
|
57269
58312
|
// src/replay-state.ts
|
|
58313
|
+
import { join as join10 } from "path";
|
|
58314
|
+
|
|
58315
|
+
// src/state-file.ts
|
|
57270
58316
|
import { readFile as readFile3 } from "fs/promises";
|
|
57271
|
-
|
|
57272
|
-
var ReplayStateFileSchema = ReplaySnapshotSchema.extend({
|
|
57273
|
-
consumer: ConsumerDeliverySnapshotSchema.optional()
|
|
57274
|
-
});
|
|
57275
|
-
function replayStateFile(stateDirectory, sessionId) {
|
|
57276
|
-
return join11(stateDirectory, `replay-${encodeURIComponent(sessionId)}.json`);
|
|
57277
|
-
}
|
|
57278
|
-
async function readReplayState(file) {
|
|
58317
|
+
async function readStateFile(file, schema, empty, refusal) {
|
|
57279
58318
|
let text;
|
|
57280
58319
|
try {
|
|
57281
58320
|
text = await readFile3(file, "utf8");
|
|
57282
58321
|
} catch (error) {
|
|
57283
58322
|
if (hasErrnoCode(error, "ENOENT"))
|
|
57284
|
-
return
|
|
58323
|
+
return empty;
|
|
57285
58324
|
throw error;
|
|
57286
58325
|
}
|
|
57287
58326
|
let parsed;
|
|
@@ -57290,10 +58329,21 @@ async function readReplayState(file) {
|
|
|
57290
58329
|
} catch {
|
|
57291
58330
|
throw new Error(`${file} is not JSON`);
|
|
57292
58331
|
}
|
|
57293
|
-
const
|
|
57294
|
-
if (!
|
|
57295
|
-
throw new Error(`${file}
|
|
57296
|
-
return
|
|
58332
|
+
const state = schema.safeParse(parsed);
|
|
58333
|
+
if (!state.success)
|
|
58334
|
+
throw new Error(`${file} ${refusal}`);
|
|
58335
|
+
return state.data;
|
|
58336
|
+
}
|
|
58337
|
+
|
|
58338
|
+
// src/replay-state.ts
|
|
58339
|
+
var ReplayStateFileSchema = ReplaySnapshotSchema.extend({
|
|
58340
|
+
consumer: ConsumerDeliverySnapshotSchema.optional()
|
|
58341
|
+
});
|
|
58342
|
+
function replayStateFile(stateDirectory, sessionId) {
|
|
58343
|
+
return join10(stateDirectory, `replay-${encodeURIComponent(sessionId)}.json`);
|
|
58344
|
+
}
|
|
58345
|
+
async function readReplayState(file) {
|
|
58346
|
+
return await readStateFile(file, ReplayStateFileSchema, { ledger: [] }, "holds no replay state");
|
|
57297
58347
|
}
|
|
57298
58348
|
async function writeReplayState(file, state) {
|
|
57299
58349
|
await writeAtomicStateFile(file, `${JSON.stringify(state)}
|
|
@@ -57318,16 +58368,16 @@ function replayStateWriter(file, restored) {
|
|
|
57318
58368
|
}
|
|
57319
58369
|
|
|
57320
58370
|
// src/session-title.ts
|
|
57321
|
-
import { basename as
|
|
58371
|
+
import { basename as basename3 } from "path";
|
|
57322
58372
|
|
|
57323
58373
|
// ../envoy-client/src/process-ancestry.ts
|
|
57324
58374
|
import { execFileSync } from "child_process";
|
|
57325
|
-
import { readFileSync as
|
|
58375
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
57326
58376
|
var MAX_ANCESTORS = 64;
|
|
57327
58377
|
var PARENT_LOOKUP_MS = 1000;
|
|
57328
58378
|
function procParentPid(pid) {
|
|
57329
58379
|
try {
|
|
57330
|
-
const stat =
|
|
58380
|
+
const stat = readFileSync8(`/proc/${pid}/stat`, "utf8");
|
|
57331
58381
|
const parent = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
|
|
57332
58382
|
return Number.isInteger(parent) ? parent : undefined;
|
|
57333
58383
|
} catch {
|
|
@@ -57457,7 +58507,7 @@ async function sessionTitle(environment, directory, sources = {}) {
|
|
|
57457
58507
|
return () => explicit;
|
|
57458
58508
|
const { serverPid, socket } = tmuxServer(configuredValue(environment.TMUX));
|
|
57459
58509
|
const pane = configuredValue(environment.TMUX_PANE)?.trim();
|
|
57460
|
-
const directoryName = oneLine(
|
|
58510
|
+
const directoryName = oneLine(basename3(directory) || directory);
|
|
57461
58511
|
const name = (socket === "" || serverPid === "" || pane === undefined ? undefined : await tmuxPaneTitle(socket, serverPid, pane, ownPane, query)) ?? (visible(directoryName) ? directoryName : "session");
|
|
57462
58512
|
return (sessionId) => {
|
|
57463
58513
|
const own = oneLine(chosen(sessionId) ?? "");
|
|
@@ -57465,13 +58515,382 @@ async function sessionTitle(environment, directory, sources = {}) {
|
|
|
57465
58515
|
};
|
|
57466
58516
|
}
|
|
57467
58517
|
|
|
58518
|
+
// ../pi-shared/src/dispatch-user-turn.ts
|
|
58519
|
+
function handledAttemptKey(messageId, attempt) {
|
|
58520
|
+
return `${messageId}#${attempt}`;
|
|
58521
|
+
}
|
|
58522
|
+
function typedInputReply(text, outcome) {
|
|
58523
|
+
const typed = /^\/\S+/.exec(text)?.[0];
|
|
58524
|
+
const command = typed ?? "This message";
|
|
58525
|
+
if ("error" in outcome)
|
|
58526
|
+
return `${command} was not run: ${outcome.error}`;
|
|
58527
|
+
if ("hostVersion" in outcome) {
|
|
58528
|
+
return typed === undefined ? undefined : `${typed} was not run: this session's agent, version ${outcome.hostVersion}, cannot run commands sent from Dispatch; nothing was sent.`;
|
|
58529
|
+
}
|
|
58530
|
+
if (outcome.handled === "terminal-only") {
|
|
58531
|
+
return `${command} runs only in the session's own terminal; nothing was sent.`;
|
|
58532
|
+
}
|
|
58533
|
+
if (outcome.handled === "unavailable") {
|
|
58534
|
+
return `${command} was not run: this session's host cannot run typed input from Dispatch; nothing was sent.`;
|
|
58535
|
+
}
|
|
58536
|
+
return outcome.output?.trim() ? outcome.output : undefined;
|
|
58537
|
+
}
|
|
58538
|
+
|
|
58539
|
+
// ../pi-shared/src/accept-user-turn.ts
|
|
58540
|
+
function isUserTurnCandidate(rendered) {
|
|
58541
|
+
const { delivery } = rendered;
|
|
58542
|
+
return rendered.envelope?.source === "dispatch" && delivery?.resource === "message" && delivery.issueKey === null && delivery.broadcastId === undefined && rendered.dispatchActor?.kind === "user" && (delivery.mode === "aside" || delivery.mode === "steer");
|
|
58543
|
+
}
|
|
58544
|
+
var AcceptedDeliverySchema = object({
|
|
58545
|
+
message_id: string2(),
|
|
58546
|
+
body: string2(),
|
|
58547
|
+
delivery: _enum2(["aside", "steer"])
|
|
58548
|
+
});
|
|
58549
|
+
function turnFromAccept(accepted) {
|
|
58550
|
+
const parsed = AcceptedDeliverySchema.safeParse(accepted);
|
|
58551
|
+
if (!parsed.success)
|
|
58552
|
+
return;
|
|
58553
|
+
return { body: parsed.data.body, messageId: parsed.data.message_id, mode: parsed.data.delivery };
|
|
58554
|
+
}
|
|
58555
|
+
async function acceptUserTurn(options) {
|
|
58556
|
+
const { rendered } = options;
|
|
58557
|
+
if (!isUserTurnCandidate(rendered))
|
|
58558
|
+
return;
|
|
58559
|
+
const { delivery } = rendered;
|
|
58560
|
+
const key = handledAttemptKey(delivery.id, delivery.attempt);
|
|
58561
|
+
if (options.handled(key)) {
|
|
58562
|
+
options.warn({ delivery, reason: "handled" });
|
|
58563
|
+
return;
|
|
58564
|
+
}
|
|
58565
|
+
await options.handle(key, delivery);
|
|
58566
|
+
try {
|
|
58567
|
+
const dispatch = options.dispatch();
|
|
58568
|
+
if (dispatch === undefined)
|
|
58569
|
+
return;
|
|
58570
|
+
const accepted = await dispatch.acceptMessageDelivery(delivery.id, delivery.attempt, {
|
|
58571
|
+
actor: { id: options.actorId, kind: "session" }
|
|
58572
|
+
});
|
|
58573
|
+
const turn = turnFromAccept(accepted);
|
|
58574
|
+
if (turn === undefined) {
|
|
58575
|
+
options.warn({ delivery, reason: "no-body" });
|
|
58576
|
+
return;
|
|
58577
|
+
}
|
|
58578
|
+
return { delivery, turn };
|
|
58579
|
+
} catch (error) {
|
|
58580
|
+
options.warn({ delivery, error, reason: "failed" });
|
|
58581
|
+
return;
|
|
58582
|
+
}
|
|
58583
|
+
}
|
|
58584
|
+
|
|
58585
|
+
// ../pi-shared/src/delivery-pictures.ts
|
|
58586
|
+
async function withDeliveredPictures(text, addresses, client, named, shown) {
|
|
58587
|
+
const textAlone = { content: text, shown: [] };
|
|
58588
|
+
if (addresses.length === 0)
|
|
58589
|
+
return textAlone;
|
|
58590
|
+
let dispatch;
|
|
58591
|
+
try {
|
|
58592
|
+
dispatch = client();
|
|
58593
|
+
} catch {
|
|
58594
|
+
return textAlone;
|
|
58595
|
+
}
|
|
58596
|
+
if (dispatch === undefined)
|
|
58597
|
+
return textAlone;
|
|
58598
|
+
const unique = [...new Set(addresses)];
|
|
58599
|
+
const shownEarlier = unique.some((address) => shown.has(address));
|
|
58600
|
+
const pictures = await readPictures(dispatch, unique, shown);
|
|
58601
|
+
const content = named ? `${text}
|
|
58602
|
+
|
|
58603
|
+
${pictures.lines.join(`
|
|
58604
|
+
`)}` : text;
|
|
58605
|
+
if (pictures.images.length === 0)
|
|
58606
|
+
return shownEarlier ? { content, shown: [] } : textAlone;
|
|
58607
|
+
return {
|
|
58608
|
+
content: [{ type: "text", text: content }, ...imageBlocks(pictures.images)],
|
|
58609
|
+
shown: pictures.shown
|
|
58610
|
+
};
|
|
58611
|
+
}
|
|
58612
|
+
|
|
58613
|
+
// src/user-turn-router.ts
|
|
58614
|
+
var ACCEPT_TIMEOUT_MS = 1e4;
|
|
58615
|
+
var PICTURES_TIMEOUT_MS = 20000;
|
|
58616
|
+
var COMMAND_WAITING_MS = 1e4;
|
|
58617
|
+
var AWAITED_ROWS_MAX = 16;
|
|
58618
|
+
var ANSWERED_DELIVERIES_MAX = 64;
|
|
58619
|
+
function commandStillRunning(command) {
|
|
58620
|
+
return `${command} is still running: it waits for the turn this session is in, or for an answer at the session's own terminal. Its output follows in another reply.`;
|
|
58621
|
+
}
|
|
58622
|
+
function commandPrintedNothing(command) {
|
|
58623
|
+
return `${command} ran in this session and printed nothing.`;
|
|
58624
|
+
}
|
|
58625
|
+
function modelPicked(model) {
|
|
58626
|
+
return `The session's model is now ${model}.`;
|
|
58627
|
+
}
|
|
58628
|
+
function thinkingPicked(effort) {
|
|
58629
|
+
return `The session's thinking level is now ${effort}.`;
|
|
58630
|
+
}
|
|
58631
|
+
function rowText(content) {
|
|
58632
|
+
const pieces = [];
|
|
58633
|
+
for (const block of content) {
|
|
58634
|
+
const text = block["text"];
|
|
58635
|
+
if (block.type === "text" && typeof text === "string")
|
|
58636
|
+
pieces.push(text);
|
|
58637
|
+
}
|
|
58638
|
+
return pieces.join(`
|
|
58639
|
+
`);
|
|
58640
|
+
}
|
|
58641
|
+
function claudeContentBlocks(content) {
|
|
58642
|
+
if (typeof content === "string")
|
|
58643
|
+
return [{ type: "text", text: content }];
|
|
58644
|
+
return content.map((block) => block.type === "text" ? { type: "text", text: block.text } : {
|
|
58645
|
+
type: "image",
|
|
58646
|
+
source: { type: "base64", media_type: block.mimeType, data: block.data }
|
|
58647
|
+
});
|
|
58648
|
+
}
|
|
58649
|
+
function pickedEffort(value) {
|
|
58650
|
+
const named = CLAUDE_EFFORT_LEVELS.find((level) => level === value);
|
|
58651
|
+
if (named !== undefined)
|
|
58652
|
+
return named;
|
|
58653
|
+
const number = Number(value);
|
|
58654
|
+
return value !== "" && Number.isFinite(number) ? number : undefined;
|
|
58655
|
+
}
|
|
58656
|
+
function createUserTurnRouter(options) {
|
|
58657
|
+
const { reply, session, state, warn } = options;
|
|
58658
|
+
const inPane = options.inbox !== undefined;
|
|
58659
|
+
const waitingMs = options.commandWaitingMs ?? COMMAND_WAITING_MS;
|
|
58660
|
+
const shownPictures = new Set;
|
|
58661
|
+
const awaited = [];
|
|
58662
|
+
const answered = new Set;
|
|
58663
|
+
const expectRow = (body, messageId) => {
|
|
58664
|
+
awaited.push({ body, messageId });
|
|
58665
|
+
while (awaited.length > AWAITED_ROWS_MAX)
|
|
58666
|
+
awaited.shift();
|
|
58667
|
+
};
|
|
58668
|
+
const record = (row, messageId) => {
|
|
58669
|
+
state.delivered(row, messageId).catch((error) => {
|
|
58670
|
+
warn(`could not record which Dispatch message a row of this session is \u2014 ${messageFor(error)}`);
|
|
58671
|
+
});
|
|
58672
|
+
};
|
|
58673
|
+
const accepted = (rendered) => acceptUserTurn({
|
|
58674
|
+
actorId: options.sessionId(),
|
|
58675
|
+
dispatch: () => options.dispatch(ACCEPT_TIMEOUT_MS),
|
|
58676
|
+
handle: state.handle,
|
|
58677
|
+
handled: state.handled,
|
|
58678
|
+
rendered,
|
|
58679
|
+
warn: (refusal) => {
|
|
58680
|
+
const { delivery } = refusal;
|
|
58681
|
+
if (refusal.reason === "handled") {
|
|
58682
|
+
warn(`a frame names the Dispatch attempt ${delivery.attempt} of message ${delivery.id}, which this session has already delivered; it arrives as a card`);
|
|
58683
|
+
return;
|
|
58684
|
+
}
|
|
58685
|
+
if (refusal.reason === "no-body") {
|
|
58686
|
+
warn(`Dispatch accepted message ${delivery.id} without the stored body to inject; it arrives as a card`);
|
|
58687
|
+
return;
|
|
58688
|
+
}
|
|
58689
|
+
warn(`Dispatch did not accept message ${delivery.id} as this session's turn (${messageFor(refusal.error)}); it arrives as a card`);
|
|
58690
|
+
}
|
|
58691
|
+
});
|
|
58692
|
+
const inject = async (delivery, turn, eventId) => {
|
|
58693
|
+
const pictures = await withDeliveredPictures(turn.body, pictureAddresses(turn.body), () => options.dispatch(PICTURES_TIMEOUT_MS), false, shownPictures);
|
|
58694
|
+
const blocks = claudeContentBlocks(pictures.content);
|
|
58695
|
+
expectRow(turn.body, turn.messageId);
|
|
58696
|
+
if (!inPane) {
|
|
58697
|
+
const row = await session.deliver({
|
|
58698
|
+
content: blocks,
|
|
58699
|
+
...eventId === undefined ? {} : { eventId },
|
|
58700
|
+
messageId: turn.messageId,
|
|
58701
|
+
text: turn.body,
|
|
58702
|
+
failed: (error) => {
|
|
58703
|
+
answer(delivery, typedInputReply(turn.body, { error: messageFor(error) }));
|
|
58704
|
+
}
|
|
58705
|
+
});
|
|
58706
|
+
if (row !== undefined)
|
|
58707
|
+
record(row, turn.messageId);
|
|
58708
|
+
} else {
|
|
58709
|
+
await options.inbox({
|
|
58710
|
+
content: turn.body,
|
|
58711
|
+
eventId: turn.messageId,
|
|
58712
|
+
...blocks.length > 1 ? { blocks } : {}
|
|
58713
|
+
});
|
|
58714
|
+
}
|
|
58715
|
+
for (const address of pictures.shown)
|
|
58716
|
+
shownPictures.add(address);
|
|
58717
|
+
};
|
|
58718
|
+
const post = async (delivery, result) => {
|
|
58719
|
+
const key = handledAttemptKey(delivery.id, delivery.attempt);
|
|
58720
|
+
const followUp = answered.has(key);
|
|
58721
|
+
if (!followUp) {
|
|
58722
|
+
rememberBounded(answered, key, ANSWERED_DELIVERIES_MAX);
|
|
58723
|
+
}
|
|
58724
|
+
await reply(delivery, { ...result, followUp });
|
|
58725
|
+
};
|
|
58726
|
+
const answer = async (delivery, body) => {
|
|
58727
|
+
if (body === undefined)
|
|
58728
|
+
return;
|
|
58729
|
+
await post(delivery, { body }).catch((error) => {
|
|
58730
|
+
warn(`a reply to message ${delivery.id} did not reach Dispatch \u2014 ${messageFor(error)}`);
|
|
58731
|
+
});
|
|
58732
|
+
};
|
|
58733
|
+
const pickModel = async (value) => {
|
|
58734
|
+
const model = value.startsWith(`${CLAUDE_PROVIDER}/`) ? value.slice(CLAUDE_PROVIDER.length + 1) : value;
|
|
58735
|
+
if (inPane) {
|
|
58736
|
+
await session.runCommand("model", model);
|
|
58737
|
+
return modelPicked(`${CLAUDE_PROVIDER}/${model}`);
|
|
58738
|
+
}
|
|
58739
|
+
const stored = await session.setModel(model);
|
|
58740
|
+
return modelPicked(`${CLAUDE_PROVIDER}/${String(stored)}`);
|
|
58741
|
+
};
|
|
58742
|
+
const runTyped = async (delivery, body) => {
|
|
58743
|
+
const typed = /^\/(\S+)/.exec(body);
|
|
58744
|
+
const name = typed?.[1] ?? "";
|
|
58745
|
+
const value = body.slice(typed?.[0].length ?? 0).trim();
|
|
58746
|
+
const effort = name === "effort" ? pickedEffort(value) : undefined;
|
|
58747
|
+
if (!session.connected()) {
|
|
58748
|
+
await answer(delivery, typedInputReply(body, { error: "this session has no link to Claude Code" }));
|
|
58749
|
+
return;
|
|
58750
|
+
}
|
|
58751
|
+
let running = true;
|
|
58752
|
+
const waiting = setTimeout(() => {
|
|
58753
|
+
if (!running)
|
|
58754
|
+
return;
|
|
58755
|
+
answer(delivery, typedInputReply(body, {
|
|
58756
|
+
handled: "command",
|
|
58757
|
+
output: commandStillRunning(`/${name}`)
|
|
58758
|
+
}));
|
|
58759
|
+
}, waitingMs);
|
|
58760
|
+
waiting.unref();
|
|
58761
|
+
let said;
|
|
58762
|
+
try {
|
|
58763
|
+
if (name === "model" && value !== "")
|
|
58764
|
+
said = await pickModel(value);
|
|
58765
|
+
else if (effort !== undefined) {
|
|
58766
|
+
const inForce = await session.setEffort(effort) ?? effort;
|
|
58767
|
+
options.effortPicked(inForce);
|
|
58768
|
+
said = thinkingPicked(inForce);
|
|
58769
|
+
} else {
|
|
58770
|
+
const output = await session.runCommand(name, value);
|
|
58771
|
+
const printed = typedInputReply(body, { handled: "command", output });
|
|
58772
|
+
said = printed ?? commandPrintedNothing(`/${name}`);
|
|
58773
|
+
}
|
|
58774
|
+
} catch (error) {
|
|
58775
|
+
said = typedInputReply(body, { error: messageFor(error) });
|
|
58776
|
+
} finally {
|
|
58777
|
+
running = false;
|
|
58778
|
+
clearTimeout(waiting);
|
|
58779
|
+
}
|
|
58780
|
+
options.refreshSettings();
|
|
58781
|
+
await answer(delivery, said);
|
|
58782
|
+
};
|
|
58783
|
+
const runBtw = (delivery) => {
|
|
58784
|
+
(async () => {
|
|
58785
|
+
if (!session.connected()) {
|
|
58786
|
+
await reply(delivery, { error: "this session has no link to Claude Code" });
|
|
58787
|
+
return;
|
|
58788
|
+
}
|
|
58789
|
+
try {
|
|
58790
|
+
await reply(delivery, { body: await session.fork(delivery.body) });
|
|
58791
|
+
} catch (error) {
|
|
58792
|
+
await reply(delivery, { error: messageFor(error) });
|
|
58793
|
+
}
|
|
58794
|
+
})().catch((error) => {
|
|
58795
|
+
warn(`a BTW answer did not reach Dispatch \u2014 ${messageFor(error)}`);
|
|
58796
|
+
});
|
|
58797
|
+
};
|
|
58798
|
+
return {
|
|
58799
|
+
async route(rendered) {
|
|
58800
|
+
const btw = rendered.delivery;
|
|
58801
|
+
if (btw?.mode === "btw") {
|
|
58802
|
+
runBtw(btw);
|
|
58803
|
+
return true;
|
|
58804
|
+
}
|
|
58805
|
+
const accept = await accepted(rendered);
|
|
58806
|
+
if (accept === undefined)
|
|
58807
|
+
return false;
|
|
58808
|
+
const { delivery, turn } = accept;
|
|
58809
|
+
if (turn.body.startsWith("/")) {
|
|
58810
|
+
runTyped(delivery, turn.body).catch((error) => {
|
|
58811
|
+
warn(`a command from Dispatch failed \u2014 ${messageFor(error)}`);
|
|
58812
|
+
});
|
|
58813
|
+
return true;
|
|
58814
|
+
}
|
|
58815
|
+
await inject(delivery, turn, rendered.envelope?.event_id);
|
|
58816
|
+
return true;
|
|
58817
|
+
},
|
|
58818
|
+
observe(event) {
|
|
58819
|
+
if (event.kind !== "append" || awaited.length === 0)
|
|
58820
|
+
return;
|
|
58821
|
+
const { message } = event;
|
|
58822
|
+
if (message.type !== "user" && message.role !== "user")
|
|
58823
|
+
return;
|
|
58824
|
+
const said = comparableLines(rowText(message.content)).join(`
|
|
58825
|
+
`);
|
|
58826
|
+
const index = awaited.findIndex((row) => comparableLines(row.body).join(`
|
|
58827
|
+
`) === said);
|
|
58828
|
+
const row = awaited[index];
|
|
58829
|
+
if (row === undefined)
|
|
58830
|
+
return;
|
|
58831
|
+
awaited.splice(index, 1);
|
|
58832
|
+
record(event.uuid, row.messageId);
|
|
58833
|
+
}
|
|
58834
|
+
};
|
|
58835
|
+
}
|
|
58836
|
+
|
|
58837
|
+
// src/user-turn-state.ts
|
|
58838
|
+
import { join as join11 } from "path";
|
|
58839
|
+
var TURNS_DIRECTORY = "turns";
|
|
58840
|
+
var HANDLED_ATTEMPTS_MAX = 1000;
|
|
58841
|
+
var DELIVERED_ROWS_MAX = 1000;
|
|
58842
|
+
var DeliveredRowSchema = object({ row: string2(), message_id: string2() });
|
|
58843
|
+
var UserTurnStateSchema = object({
|
|
58844
|
+
handled: array(string2()).default([]),
|
|
58845
|
+
rows: array(DeliveredRowSchema).default([])
|
|
58846
|
+
});
|
|
58847
|
+
function userTurnStateFile(stateDirectory, sessionId) {
|
|
58848
|
+
return join11(stateDirectory, TURNS_DIRECTORY, `${encodeURIComponent(sessionId)}.json`);
|
|
58849
|
+
}
|
|
58850
|
+
async function readUserTurnState(file) {
|
|
58851
|
+
return await readStateFile(file, UserTurnStateSchema, { handled: [], rows: [] }, "holds no record of this session's turns");
|
|
58852
|
+
}
|
|
58853
|
+
function userTurnState(file, restored) {
|
|
58854
|
+
const handled = new Set(restored.handled);
|
|
58855
|
+
const rows = new Map(restored.rows.map(({ row, message_id }) => [row, message_id]));
|
|
58856
|
+
let writing = Promise.resolve();
|
|
58857
|
+
const write = () => {
|
|
58858
|
+
const state = {
|
|
58859
|
+
handled: [...handled].slice(-HANDLED_ATTEMPTS_MAX),
|
|
58860
|
+
rows: [...rows].slice(-DELIVERED_ROWS_MAX).map(([row, message_id]) => ({ row, message_id }))
|
|
58861
|
+
};
|
|
58862
|
+
const run = writing.then(() => writeAtomicStateFile(file, `${JSON.stringify(state)}
|
|
58863
|
+
`));
|
|
58864
|
+
writing = run.catch(() => {
|
|
58865
|
+
return;
|
|
58866
|
+
});
|
|
58867
|
+
return run;
|
|
58868
|
+
};
|
|
58869
|
+
return {
|
|
58870
|
+
handled: (key) => handled.has(key),
|
|
58871
|
+
handle(key) {
|
|
58872
|
+
rememberBounded(handled, key, HANDLED_ATTEMPTS_MAX);
|
|
58873
|
+
return write();
|
|
58874
|
+
},
|
|
58875
|
+
messageOf: (row) => rows.get(row),
|
|
58876
|
+
delivered(row, messageId) {
|
|
58877
|
+
if (rows.get(row) === messageId)
|
|
58878
|
+
return writing;
|
|
58879
|
+
rememberBoundedEntry(rows, row, messageId, DELIVERED_ROWS_MAX);
|
|
58880
|
+
return write();
|
|
58881
|
+
},
|
|
58882
|
+
idle: () => writing
|
|
58883
|
+
};
|
|
58884
|
+
}
|
|
58885
|
+
async function pruneStaleSessionTurns(stateDirectory) {
|
|
58886
|
+
await pruneStateDirectory(join11(stateDirectory, TURNS_DIRECTORY), writtenLongAgo);
|
|
58887
|
+
}
|
|
58888
|
+
|
|
57468
58889
|
// src/envoy-channel-server.ts
|
|
57469
|
-
var
|
|
57470
|
-
var CHANNEL_INSTRUCTIONS = "Envoy delivers trusted, internal session and Dispatch events as <channel> messages. The content is rendered Envoy state; producer identifies its Envoy producer (source is the channel name), topic is the NATS subject, event_id identifies this delivery, dedupe_key is the dedupe identity (a repeat of one already delivered is not shown again), urgency is how soon the sender asks you to act (it does not change when or in what order deliveries arrive), from_session identifies the origin session, and reply metadata names any correlation. Use the Envoy tools for Envoy actions, and reach Dispatch through the `dispatch` command in Bash (`dispatch --help` lists its commands; the `dispatch` skill teaches them). Dispatch asks stay on Dispatch. This channel advertises Aside only: it does not support targeted BTW delivery or permission relay.";
|
|
58890
|
+
var CHANNEL_INSTRUCTIONS = "Envoy delivers trusted, internal session and Dispatch events into this conversation. Use the Envoy tools for Envoy actions, and reach Dispatch through the `dispatch` command in Bash (`dispatch --help` lists its commands; the `dispatch` skill teaches them). Dispatch asks stay on Dispatch.";
|
|
57471
58891
|
var CHANNEL_INBOX_LIMIT = 50;
|
|
57472
58892
|
var EMPTY_RECEIPT = new Uint8Array;
|
|
57473
|
-
var
|
|
57474
|
-
var ChannelMetaKey = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
58893
|
+
var encoder6 = new TextEncoder;
|
|
57475
58894
|
var HANDOFF_POLL_MS = 250;
|
|
57476
58895
|
var HANDOFF_FLUSH_TIMEOUT_MS = 1e4;
|
|
57477
58896
|
async function withTimeout(promise, ms, what) {
|
|
@@ -57491,7 +58910,11 @@ function undefinedOnNotFound(error) {
|
|
|
57491
58910
|
throw error;
|
|
57492
58911
|
}
|
|
57493
58912
|
var MCP_SERVER_INFO = { name: "envoy", version: version2 };
|
|
57494
|
-
|
|
58913
|
+
function sessionCapabilities(session) {
|
|
58914
|
+
if (!session.pane)
|
|
58915
|
+
return session.mod ? DELIVERY_CAPABILITIES : [];
|
|
58916
|
+
return session.mod ? [...INBOX_CAPABILITIES, "btw"] : INBOX_CAPABILITIES;
|
|
58917
|
+
}
|
|
57495
58918
|
var argumentSchemas = new WeakMap;
|
|
57496
58919
|
function argumentsSchema(spec) {
|
|
57497
58920
|
let schema = argumentSchemas.get(spec);
|
|
@@ -57526,14 +58949,6 @@ class UnsupportedEnvoyToolError extends Error {
|
|
|
57526
58949
|
this.toolName = toolName;
|
|
57527
58950
|
}
|
|
57528
58951
|
}
|
|
57529
|
-
function sanitizeChannelMetadata(metadata) {
|
|
57530
|
-
const sanitized = {};
|
|
57531
|
-
for (const [key, value] of Object.entries(metadata)) {
|
|
57532
|
-
if (value !== undefined && ChannelMetaKey.test(key))
|
|
57533
|
-
sanitized[key] = value;
|
|
57534
|
-
}
|
|
57535
|
-
return sanitized;
|
|
57536
|
-
}
|
|
57537
58952
|
async function postDispatchReply(identity, configFile, delivery, result) {
|
|
57538
58953
|
const config = sessionDispatchSettings(configFile, process.env, {
|
|
57539
58954
|
cwd: identity.directory,
|
|
@@ -57546,47 +58961,45 @@ async function postDispatchReply(identity, configFile, delivery, result) {
|
|
|
57546
58961
|
function createChannelDelivery(input) {
|
|
57547
58962
|
const inbox = [];
|
|
57548
58963
|
let tail = Promise.resolve();
|
|
57549
|
-
const
|
|
57550
|
-
const
|
|
57551
|
-
tail =
|
|
57552
|
-
|
|
58964
|
+
const queued = (work) => {
|
|
58965
|
+
const run = tail.then(work);
|
|
58966
|
+
tail = run.then(() => {
|
|
58967
|
+
return;
|
|
58968
|
+
}, (error) => {
|
|
58969
|
+
process.stderr.write(`envoy-channel: a delivery failed \u2014 ${messageFor(error)}
|
|
57553
58970
|
`);
|
|
57554
58971
|
});
|
|
57555
|
-
return
|
|
58972
|
+
return run;
|
|
57556
58973
|
};
|
|
57557
|
-
const
|
|
58974
|
+
const write = (notification) => queued(() => input.notifier.notification(notification));
|
|
58975
|
+
const list = (rendered) => {
|
|
57558
58976
|
const envelope = rendered.envelope;
|
|
57559
|
-
if (envelope
|
|
57560
|
-
|
|
57561
|
-
|
|
57562
|
-
|
|
57563
|
-
|
|
57564
|
-
|
|
57565
|
-
|
|
57566
|
-
|
|
57567
|
-
|
|
57568
|
-
|
|
57569
|
-
return queue({
|
|
57570
|
-
method: CHANNEL_NOTIFICATION_METHOD,
|
|
57571
|
-
params: {
|
|
57572
|
-
content,
|
|
57573
|
-
meta: sanitizeChannelMetadata({
|
|
57574
|
-
producer: envelope?.source ?? "unknown",
|
|
57575
|
-
topic: envelope?.topic || subject,
|
|
57576
|
-
event_id: envelope?.event_id,
|
|
57577
|
-
dedupe_key: envelope?.dedupe_key,
|
|
57578
|
-
urgency: envelope?.urgency,
|
|
57579
|
-
from_session: envelope?.source_session,
|
|
57580
|
-
expects_reply: envelope?.expects_reply?.toString(),
|
|
57581
|
-
in_reply_to: envelope?.in_reply_to
|
|
57582
|
-
})
|
|
57583
|
-
}
|
|
58977
|
+
if (envelope === undefined)
|
|
58978
|
+
return;
|
|
58979
|
+
const eventId = envelope.event_id;
|
|
58980
|
+
if (eventId !== undefined && inbox.some((entry) => entry.event_id === eventId))
|
|
58981
|
+
return;
|
|
58982
|
+
inbox.unshift({
|
|
58983
|
+
event_id: eventId ?? "unknown",
|
|
58984
|
+
at: inboundTimestamp(envelope.issued_at),
|
|
58985
|
+
from: envelope.source_session ?? envelope.source,
|
|
58986
|
+
summary: envelope.payload_summary ?? "unknown"
|
|
57584
58987
|
});
|
|
58988
|
+
if (inbox.length > CHANNEL_INBOX_LIMIT)
|
|
58989
|
+
inbox.pop();
|
|
58990
|
+
};
|
|
58991
|
+
const notification = (rendered, content) => ({
|
|
58992
|
+
content,
|
|
58993
|
+
...rendered.envelope?.event_id === undefined ? {} : { eventId: rendered.envelope.event_id }
|
|
58994
|
+
});
|
|
58995
|
+
const show = (rendered, content) => {
|
|
58996
|
+
list(rendered);
|
|
58997
|
+
return write(notification(rendered, content));
|
|
57585
58998
|
};
|
|
57586
58999
|
const passOverBtw = (rendered, how) => {
|
|
57587
59000
|
if (rendered.delivery?.mode !== "btw")
|
|
57588
59001
|
return false;
|
|
57589
|
-
process.stderr.write(`envoy-channel: passing over ${how} BTW ${rendered.envelope?.event_id ?? "unknown"}:
|
|
59002
|
+
process.stderr.write(`envoy-channel: passing over ${how} BTW ${rendered.envelope?.event_id ?? "unknown"}: it was asked while this session was not taking messages
|
|
57590
59003
|
`);
|
|
57591
59004
|
return true;
|
|
57592
59005
|
};
|
|
@@ -57614,7 +59027,16 @@ function createChannelDelivery(input) {
|
|
|
57614
59027
|
}
|
|
57615
59028
|
if (late === true && passOverBtw(rendered, "late"))
|
|
57616
59029
|
return tail.then(() => false);
|
|
57617
|
-
return
|
|
59030
|
+
return queued(async () => {
|
|
59031
|
+
const routed = await input.router?.()?.route(rendered);
|
|
59032
|
+
if (routed === true) {
|
|
59033
|
+
list(rendered);
|
|
59034
|
+
return true;
|
|
59035
|
+
}
|
|
59036
|
+
await input.notifier.notification(notification(rendered, rendered.content));
|
|
59037
|
+
list(rendered);
|
|
59038
|
+
return true;
|
|
59039
|
+
});
|
|
57618
59040
|
},
|
|
57619
59041
|
async replay(frame) {
|
|
57620
59042
|
const rendered = renderInbound(frame.data, input.identity.id, frame.subject);
|
|
@@ -57622,14 +59044,11 @@ function createChannelDelivery(input) {
|
|
|
57622
59044
|
return "suppressed";
|
|
57623
59045
|
if (passOverBtw(rendered, "replayed"))
|
|
57624
59046
|
return "suppressed";
|
|
57625
|
-
await show(rendered,
|
|
59047
|
+
await show(rendered, replayedText(rendered.content, frame.time));
|
|
57626
59048
|
return "shown";
|
|
57627
59049
|
},
|
|
57628
59050
|
notice(content) {
|
|
57629
|
-
return
|
|
57630
|
-
method: CHANNEL_NOTIFICATION_METHOD,
|
|
57631
|
-
params: { content, meta: { producer: "envoy" } }
|
|
57632
|
-
});
|
|
59051
|
+
return write({ content });
|
|
57633
59052
|
},
|
|
57634
59053
|
inbox() {
|
|
57635
59054
|
return [...inbox];
|
|
@@ -57651,10 +59070,13 @@ async function enqueueChannelMessage(delivery, connection, directSubject, messag
|
|
|
57651
59070
|
async function startChannelSession(options) {
|
|
57652
59071
|
const { identity } = options;
|
|
57653
59072
|
let directSubject = agentSubject(identity.id);
|
|
59073
|
+
let router;
|
|
59074
|
+
let turns;
|
|
57654
59075
|
const delivery = createChannelDelivery({
|
|
57655
59076
|
identity,
|
|
57656
59077
|
notifier: options.notifier,
|
|
57657
|
-
configFile: options.configFile
|
|
59078
|
+
configFile: options.configFile,
|
|
59079
|
+
router: () => router
|
|
57658
59080
|
});
|
|
57659
59081
|
const userTopics = new Set;
|
|
57660
59082
|
let heldRole;
|
|
@@ -57764,6 +59186,22 @@ async function startChannelSession(options) {
|
|
|
57764
59186
|
replayBound = sessionId;
|
|
57765
59187
|
return rememberedTopics(snapshot.state);
|
|
57766
59188
|
};
|
|
59189
|
+
const restoreTurns = async () => {
|
|
59190
|
+
router = undefined;
|
|
59191
|
+
turns = undefined;
|
|
59192
|
+
if (options.router === undefined)
|
|
59193
|
+
return;
|
|
59194
|
+
const file = userTurnStateFile(options.stateDirectory, identity.id);
|
|
59195
|
+
const restored = await readUserTurnState(file).catch((error) => {
|
|
59196
|
+
process.stderr.write(`envoy-channel: could not read this session's record of the messages it took (${messageFor(error)}); its messages arrive as cards
|
|
59197
|
+
`);
|
|
59198
|
+
return;
|
|
59199
|
+
});
|
|
59200
|
+
if (restored === undefined)
|
|
59201
|
+
return;
|
|
59202
|
+
turns = userTurnState(file, restored);
|
|
59203
|
+
router = options.router(turns);
|
|
59204
|
+
};
|
|
57767
59205
|
const triggerReplay = () => {
|
|
57768
59206
|
if (!shuttingDown)
|
|
57769
59207
|
replay.sweep();
|
|
@@ -57823,7 +59261,7 @@ async function startChannelSession(options) {
|
|
|
57823
59261
|
`);
|
|
57824
59262
|
}
|
|
57825
59263
|
},
|
|
57826
|
-
publish: (reply, body) => options.connection.publish(reply,
|
|
59264
|
+
publish: (reply, body) => options.connection.publish(reply, encoder6.encode(body)),
|
|
57827
59265
|
persist: (snapshot) => {
|
|
57828
59266
|
if (relay !== owner)
|
|
57829
59267
|
return false;
|
|
@@ -57952,7 +59390,7 @@ async function startChannelSession(options) {
|
|
|
57952
59390
|
topics,
|
|
57953
59391
|
port: 0,
|
|
57954
59392
|
title,
|
|
57955
|
-
capabilities: options.capabilities,
|
|
59393
|
+
capabilities: options.capabilities(),
|
|
57956
59394
|
driving: false,
|
|
57957
59395
|
selfSubscribed: true,
|
|
57958
59396
|
clientInstanceID: options.clientInstanceID,
|
|
@@ -58097,6 +59535,7 @@ async function startChannelSession(options) {
|
|
|
58097
59535
|
directSubject = nextSubject;
|
|
58098
59536
|
forwarder.unfollow([previousSubject]);
|
|
58099
59537
|
await restoreReplay();
|
|
59538
|
+
await restoreTurns();
|
|
58100
59539
|
roleTransferFrom ??= previous;
|
|
58101
59540
|
await withTimeout(options.connection.flush(), options.handoffFlushTimeoutMs ?? HANDOFF_FLUSH_TIMEOUT_MS, "flushing NATS").catch((error) => {
|
|
58102
59541
|
process.stderr.write(`envoy-channel: could not confirm the relay subscription of ${next} \u2014 ${messageFor(error)}
|
|
@@ -58211,6 +59650,7 @@ async function startChannelSession(options) {
|
|
|
58211
59650
|
heartbeat();
|
|
58212
59651
|
};
|
|
58213
59652
|
const remembered = await restoreReplay();
|
|
59653
|
+
await restoreTurns();
|
|
58214
59654
|
forwarder.follow(directSubject);
|
|
58215
59655
|
for (const topic of remembered) {
|
|
58216
59656
|
userTopics.add(topic);
|
|
@@ -58241,6 +59681,7 @@ async function startChannelSession(options) {
|
|
|
58241
59681
|
for (const prune of [
|
|
58242
59682
|
() => pruneStaleSessionHandoffs(options.stateDirectory),
|
|
58243
59683
|
() => pruneStaleSessionTitles(options.stateDirectory),
|
|
59684
|
+
() => pruneStaleSessionTurns(options.stateDirectory),
|
|
58244
59685
|
() => pruneStaleSessionRoles(options.stateDirectory, identity.id)
|
|
58245
59686
|
]) {
|
|
58246
59687
|
await prune().catch((error) => {
|
|
@@ -58308,6 +59749,20 @@ async function startChannelSession(options) {
|
|
|
58308
59749
|
onRoleRegained(hook) {
|
|
58309
59750
|
regained = hook;
|
|
58310
59751
|
},
|
|
59752
|
+
capabilitiesChanged() {
|
|
59753
|
+
if (shuttingDown)
|
|
59754
|
+
return;
|
|
59755
|
+
serialized(register).catch((error) => {
|
|
59756
|
+
process.stderr.write(`envoy-channel: could not advertise this session's delivery modes (${messageFor(error)}); the next heartbeat retries
|
|
59757
|
+
`);
|
|
59758
|
+
});
|
|
59759
|
+
},
|
|
59760
|
+
observe(event) {
|
|
59761
|
+
router?.observe(event);
|
|
59762
|
+
},
|
|
59763
|
+
messageOf(row) {
|
|
59764
|
+
return turns?.messageOf(row);
|
|
59765
|
+
},
|
|
58311
59766
|
async rememberRole(role) {
|
|
58312
59767
|
heldRole = role;
|
|
58313
59768
|
await writeRoleRecord(roleStateFile(options.stateDirectory, identity.id), role);
|
|
@@ -58329,6 +59784,7 @@ async function startChannelSession(options) {
|
|
|
58329
59784
|
`);
|
|
58330
59785
|
});
|
|
58331
59786
|
await stateWriter?.idle();
|
|
59787
|
+
await turns?.idle();
|
|
58332
59788
|
}
|
|
58333
59789
|
};
|
|
58334
59790
|
}
|
|
@@ -58502,18 +59958,9 @@ async function runEnvoyChannelServer() {
|
|
|
58502
59958
|
const inboxSocket = configuredValue(process.env[INBOX_VARIABLE]);
|
|
58503
59959
|
const bundlePath = process.argv[1] ?? "";
|
|
58504
59960
|
const inbox = inboxSocket === undefined ? undefined : createInboxNotifier(inboxSocket);
|
|
58505
|
-
const channel = inboxSocket === undefined ? detectClaudeChannel({
|
|
58506
|
-
claudePid: process.ppid,
|
|
58507
|
-
bundlePath,
|
|
58508
|
-
environment: process.env
|
|
58509
|
-
}) : { loaded: true };
|
|
58510
|
-
if (!channel.loaded) {
|
|
58511
|
-
process.stderr.write(`envoy-channel: Claude Code did not load this server as a channel: ${channel.why}; the session advertises no delivery mode, so Dispatch refuses messages to it instead of losing them
|
|
58512
|
-
`);
|
|
58513
|
-
}
|
|
58514
59961
|
const server = new Server(MCP_SERVER_INFO, {
|
|
58515
|
-
capabilities: { tools: {}
|
|
58516
|
-
instructions:
|
|
59962
|
+
capabilities: { tools: {} },
|
|
59963
|
+
instructions: CHANNEL_INSTRUCTIONS
|
|
58517
59964
|
});
|
|
58518
59965
|
const client = createEnvoyClient({
|
|
58519
59966
|
baseUrl: envoy.url,
|
|
@@ -58532,10 +59979,7 @@ async function runEnvoyChannelServer() {
|
|
|
58532
59979
|
booted: claimBooted.promise,
|
|
58533
59980
|
readState: (signal) => daemon.state({ signal }),
|
|
58534
59981
|
reportActivity: (input) => daemon.activity(input),
|
|
58535
|
-
inbox: inbox === undefined ? undefined : (text) => inbox.notification({
|
|
58536
|
-
method: CHANNEL_NOTIFICATION_METHOD,
|
|
58537
|
-
params: { content: text, meta: {} }
|
|
58538
|
-
}),
|
|
59982
|
+
inbox: inbox === undefined ? undefined : (text) => inbox.notification({ content: text }),
|
|
58539
59983
|
publish: client.publish,
|
|
58540
59984
|
env: process.env,
|
|
58541
59985
|
log: phaseLog
|
|
@@ -58568,13 +60012,39 @@ async function runEnvoyChannelServer() {
|
|
|
58568
60012
|
throw new Error("Envoy channel server is still starting");
|
|
58569
60013
|
return mcpResult(await executeEnvoyTool(runtime, request.params.name, request.params.arguments));
|
|
58570
60014
|
});
|
|
58571
|
-
|
|
60015
|
+
let modLink;
|
|
60016
|
+
const linked = Promise.withResolvers();
|
|
60017
|
+
let agentStream;
|
|
60018
|
+
const claude = modSession({
|
|
60019
|
+
link: {
|
|
60020
|
+
request: (call, args, requestOptions) => {
|
|
60021
|
+
const link = modLink;
|
|
60022
|
+
if (link === undefined) {
|
|
60023
|
+
return Promise.reject(new Error("this session has no link to Claude Code"));
|
|
60024
|
+
}
|
|
60025
|
+
return link.request(call, args, requestOptions);
|
|
60026
|
+
},
|
|
60027
|
+
connected: () => modLink?.connected() === true
|
|
60028
|
+
},
|
|
60029
|
+
warn: (text) => process.stderr.write(`envoy-channel: ${text}
|
|
60030
|
+
`)
|
|
60031
|
+
});
|
|
60032
|
+
const writer = inbox ?? {
|
|
60033
|
+
notification: async ({ content, blocks, eventId }) => {
|
|
60034
|
+
await claude.show({
|
|
60035
|
+
content: blocks ?? [{ type: "text", text: content }],
|
|
60036
|
+
text: content,
|
|
60037
|
+
...eventId === undefined ? {} : { eventId }
|
|
60038
|
+
});
|
|
60039
|
+
}
|
|
60040
|
+
};
|
|
60041
|
+
const notifier = phaseHook === undefined ? writer : {
|
|
58572
60042
|
notification: async (notification) => {
|
|
58573
|
-
phaseHook.deliveryWritten(notification.
|
|
58574
|
-
await
|
|
60043
|
+
phaseHook.deliveryWritten(notification.content);
|
|
60044
|
+
await writer.notification(notification);
|
|
58575
60045
|
}
|
|
58576
60046
|
};
|
|
58577
|
-
const capabilities = inboxSocket !== undefined
|
|
60047
|
+
const capabilities = () => sessionCapabilities({ pane: inboxSocket !== undefined, mod: modLink?.connected() === true });
|
|
58578
60048
|
const booting = new AbortController;
|
|
58579
60049
|
const stopped = Promise.withResolvers();
|
|
58580
60050
|
let stopping = false;
|
|
@@ -58585,6 +60055,10 @@ async function runEnvoyChannelServer() {
|
|
|
58585
60055
|
stopping = true;
|
|
58586
60056
|
booting.abort();
|
|
58587
60057
|
phaseHook?.stop();
|
|
60058
|
+
modLink?.close().catch(() => {
|
|
60059
|
+
return;
|
|
60060
|
+
});
|
|
60061
|
+
agentStream?.stop();
|
|
58588
60062
|
if (session === undefined) {
|
|
58589
60063
|
stopped.resolve();
|
|
58590
60064
|
return;
|
|
@@ -58596,6 +60070,41 @@ async function runEnvoyChannelServer() {
|
|
|
58596
60070
|
process.once("SIGINT", stop);
|
|
58597
60071
|
process.once("SIGHUP", stop);
|
|
58598
60072
|
await server.connect(new StdioServerTransport);
|
|
60073
|
+
startModLink({
|
|
60074
|
+
sessionId: claudeSessionId({ CLAUDE_CODE_SESSION_ID: process.env["CLAUDE_CODE_SESSION_ID"] }),
|
|
60075
|
+
env: process.env,
|
|
60076
|
+
warn: (text) => process.stderr.write(`envoy-channel: ${text}
|
|
60077
|
+
`)
|
|
60078
|
+
}).then((link) => {
|
|
60079
|
+
modLink = link;
|
|
60080
|
+
if (stopping)
|
|
60081
|
+
link.close().catch(() => {
|
|
60082
|
+
return;
|
|
60083
|
+
});
|
|
60084
|
+
else
|
|
60085
|
+
linked.resolve(link);
|
|
60086
|
+
}, (error) => {
|
|
60087
|
+
process.stderr.write(`envoy-channel: no link to this session's Claude Code mod \u2014 ${messageFor(error)}; the session runs on without one
|
|
60088
|
+
`);
|
|
60089
|
+
});
|
|
60090
|
+
const router = (state) => {
|
|
60091
|
+
const options = {
|
|
60092
|
+
dispatch: (ms) => {
|
|
60093
|
+
const active = activeDispatchSettings(sessionDispatchSettings(configFile, process.env, { cwd: directory }).dispatch);
|
|
60094
|
+
return active === null ? undefined : new DispatchClient(active.url, active.credential, fetch, AbortSignal.timeout(ms));
|
|
60095
|
+
},
|
|
60096
|
+
effortPicked: (effort) => agentStream?.pickedEffort(effort),
|
|
60097
|
+
refreshSettings: () => agentStream?.refresh(),
|
|
60098
|
+
reply: (delivery, result) => postDispatchReply(identity, configFile, delivery, result),
|
|
60099
|
+
session: claude,
|
|
60100
|
+
sessionId: () => identity.id,
|
|
60101
|
+
state,
|
|
60102
|
+
warn: (text) => process.stderr.write(`envoy-channel: ${text}
|
|
60103
|
+
`),
|
|
60104
|
+
...inboxSocket === undefined ? {} : { inbox: (input) => notifier.notification(input) }
|
|
60105
|
+
};
|
|
60106
|
+
return createUserTurnRouter(options);
|
|
60107
|
+
};
|
|
58599
60108
|
let connection;
|
|
58600
60109
|
try {
|
|
58601
60110
|
const { inbound } = envoy;
|
|
@@ -58616,6 +60125,7 @@ async function runEnvoyChannelServer() {
|
|
|
58616
60125
|
connection,
|
|
58617
60126
|
notifier,
|
|
58618
60127
|
capabilities,
|
|
60128
|
+
router,
|
|
58619
60129
|
client,
|
|
58620
60130
|
heartbeatMs: envoy.heartbeatMs,
|
|
58621
60131
|
configFile,
|
|
@@ -58626,6 +60136,31 @@ async function runEnvoyChannelServer() {
|
|
|
58626
60136
|
...stamping ? { stampTitle } : {},
|
|
58627
60137
|
...configuredValue(overrideSessionId) === undefined ? { handoffPid: process.ppid } : {}
|
|
58628
60138
|
});
|
|
60139
|
+
const bus = connection;
|
|
60140
|
+
const live = session;
|
|
60141
|
+
linked.promise.then((link) => {
|
|
60142
|
+
if (stopping)
|
|
60143
|
+
return;
|
|
60144
|
+
agentStream = startAgentStream({
|
|
60145
|
+
connection: bus,
|
|
60146
|
+
followsSessionId: configuredValue(overrideSessionId) === undefined,
|
|
60147
|
+
isPane: inbox !== undefined,
|
|
60148
|
+
link,
|
|
60149
|
+
sessionId: identity.id,
|
|
60150
|
+
observe: (event) => {
|
|
60151
|
+
claude.observe(event);
|
|
60152
|
+
live.observe(event);
|
|
60153
|
+
},
|
|
60154
|
+
appendedRow: (row) => claude.appendedRow(row),
|
|
60155
|
+
runsCommandRow: (row) => claude.runsCommandRow(row),
|
|
60156
|
+
runsUnreadPrompt: (row) => claude.runsUnreadPrompt(row),
|
|
60157
|
+
tagFor: (row) => live.messageOf(row)
|
|
60158
|
+
});
|
|
60159
|
+
link.onSessionId(() => live.capabilitiesChanged());
|
|
60160
|
+
link.onConnected(() => live.capabilitiesChanged());
|
|
60161
|
+
if (link.connected())
|
|
60162
|
+
live.capabilitiesChanged();
|
|
60163
|
+
});
|
|
58629
60164
|
}
|
|
58630
60165
|
runtime = { identity, client, session, clientInstanceID };
|
|
58631
60166
|
if (pane !== undefined && daemon !== undefined && !stopping) {
|
|
@@ -58646,6 +60181,9 @@ async function runEnvoyChannelServer() {
|
|
|
58646
60181
|
await session.shutdown();
|
|
58647
60182
|
await stopped.promise;
|
|
58648
60183
|
} finally {
|
|
60184
|
+
await modLink?.close().catch(() => {
|
|
60185
|
+
return;
|
|
60186
|
+
});
|
|
58649
60187
|
if (session === undefined && connection !== undefined) {
|
|
58650
60188
|
await connection.drain().catch(() => {
|
|
58651
60189
|
return;
|