@sjawhar/claude-envoy 1.17.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 +59 -8
- package/dist/envoy-channel.js +2418 -877
- package/dist/session-hook.js +59 -8
- 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/legion/skills/legion-architect/references/acceptance.md +7 -7
- package/legion/skills/legion-controller/references/daily-report.md +1 -1
- package/legion/skills/legion-worker/references/merge-gate.md +5 -4
- 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,
|
|
@@ -36760,7 +36798,7 @@ var commentValidation = {
|
|
|
36760
36798
|
refusal: (value) => commentProblem(value) ?? `${commentOwner.message} ${COMMENT_TURN_RULE}`
|
|
36761
36799
|
};
|
|
36762
36800
|
var ISSUE_UPDATE_FIELDS_RULE = "Issue update requires at least one field besides issue: status, title, labels, priority, external_links, route, parent, blocked_by, components, repo, before, or after.";
|
|
36763
|
-
var ISSUE_UPDATE_DONE_RULE = "status done requires reason, a non-empty note saying why the issue is closing, posted on the issue before it closes
|
|
36801
|
+
var ISSUE_UPDATE_DONE_RULE = "status done requires reason, a non-empty note saying why the issue is closing, posted on the issue before it closes so the close carries its note";
|
|
36764
36802
|
var ISSUE_UPDATE_REASON_RULE = "reason goes only with status done";
|
|
36765
36803
|
var ISSUE_UPDATE_RULE = `${ISSUE_UPDATE_FIELDS_RULE} ${ISSUE_UPDATE_DONE_RULE}; ${ISSUE_UPDATE_REASON_RULE}.`;
|
|
36766
36804
|
function issueUpdateProblem(value) {
|
|
@@ -36905,7 +36943,7 @@ var dispatchToolSpecs = [
|
|
|
36905
36943
|
priority: 1,
|
|
36906
36944
|
before: "DSP-2"
|
|
36907
36945
|
},
|
|
36908
|
-
description: "Update an existing issue: move its lifecycle status, retitle it, replace its labels, set " + "its priority, link a URL (the pull request that delivers it, a run, a document), set its " + "route, set or clear its parent, replace the issues it is blocked by, attach it to " + "architecture components, set the repository it targets, or move it on its project's " + "board. Status is one of " + `${ISSUE_STATUSES.join(", ")}; outside Legion, move it yourself as the work advances; inside ` + "Legion the daemon moves it. A non-empty external_links is " + "merged into the issue's existing links by URL, so linking the pull request you just opened " + "keeps every earlier link; an empty external_links removes every link instead. components " + "replaces the issue's own attachment. Closing an issue " + "(status done) requires reason, the note that says why: it is posted on the issue as a " + "message, then the issue closes,
|
|
36946
|
+
description: "Update an existing issue: move its lifecycle status, retitle it, replace its labels, set " + "its priority, link a URL (the pull request that delivers it, a run, a document), set its " + "route, set or clear its parent, replace the issues it is blocked by, attach it to " + "architecture components, set the repository it targets, or move it on its project's " + "board. Status is one of " + `${ISSUE_STATUSES.join(", ")}; outside Legion, move it yourself as the work advances; inside ` + "Legion the daemon moves it. A non-empty external_links is " + "merged into the issue's existing links by URL, so linking the pull request you just opened " + "keeps every earlier link; an empty external_links removes every link instead. components " + "replaces the issue's own attachment. Closing an issue " + "(status done) requires reason, the note that says why: it is posted on the issue as a " + "message, then the issue closes, so the close carries its note; closing an issue that is " + "already closed is refused (ISSUE_CLOSED) before anything is posted. Reason goes only " + "with status done. Closing an issue withdraws every open ask " + "it holds (resolution kind withdrawn, naming the closer), and reopening it brings exactly " + "those asks back. " + "A closed issue still takes messages and comments, though not asks or artifacts; of this " + "tool's fields it takes only a board move " + "(before, after), components, " + "a reopening status (any status but done), and an external_links write that adds no new " + "link (so a link can move to the issue that replaced it); everything else, priority " + "included, waits for the reopen. " + "priority is yours to set and a human overrides it. before and after move the issue on " + "its project's board: before puts it above that issue, after below, and both together " + "land it between them. A project's board order is one agent's to keep; in a Legion " + "deployment that is the controller. Never reassign an issue a person holds to get a " + "faster answer: who holds it is their call. At least one " + `field besides issue is required. ${ISSUE_REFERENCE}`,
|
|
36909
36947
|
arguments: (z) => ({
|
|
36910
36948
|
issue: z.string().describe(ISSUE_REFERENCE),
|
|
36911
36949
|
status: z.enum(ISSUE_STATUSES).describe("New lifecycle status.").optional(),
|
|
@@ -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";
|
|
@@ -50103,6 +50292,9 @@ async function executeDispatchTool(input) {
|
|
|
50103
50292
|
} catch (error) {
|
|
50104
50293
|
throw refusalWithCode(error);
|
|
50105
50294
|
}
|
|
50295
|
+
if (status === "done" && typeof before.closed_at === "string") {
|
|
50296
|
+
throw refusalWithCode(new DispatchServiceError("ISSUE_CLOSED", 409, "issue is closed"), `${issueKey} closed at ${before.closed_at}, so the reason was not posted and the close was not sent`);
|
|
50297
|
+
}
|
|
50106
50298
|
let closingNote;
|
|
50107
50299
|
if (reason !== undefined) {
|
|
50108
50300
|
try {
|
|
@@ -51307,164 +51499,23 @@ function natsConnectionOptions(input) {
|
|
|
51307
51499
|
};
|
|
51308
51500
|
}
|
|
51309
51501
|
|
|
51310
|
-
// ../envoy-client/src/
|
|
51311
|
-
|
|
51312
|
-
|
|
51313
|
-
|
|
51314
|
-
|
|
51315
|
-
|
|
51316
|
-
return url.slice(0, end);
|
|
51317
|
-
}
|
|
51318
|
-
function checkoutPair(checkout, serverUrl) {
|
|
51319
|
-
const { token } = checkout.dispatch;
|
|
51320
|
-
if (token === undefined) {
|
|
51321
|
-
throw new Error(`${checkout.path}: dispatch.serverUrl requires dispatch.token in the same file`);
|
|
51322
|
-
}
|
|
51323
|
-
return {
|
|
51324
|
-
kind: "file",
|
|
51325
|
-
token,
|
|
51326
|
-
field: "dispatch.token",
|
|
51327
|
-
path: checkout.path,
|
|
51328
|
-
server: serverUrl,
|
|
51329
|
-
checkout: true
|
|
51330
|
-
};
|
|
51331
|
-
}
|
|
51332
|
-
function writtenToken(config, checkout) {
|
|
51333
|
-
if (checkout?.dispatch.token !== undefined) {
|
|
51334
|
-
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) };
|
|
51335
51508
|
}
|
|
51336
|
-
const own = config.dispatch;
|
|
51337
|
-
if (own.token === undefined)
|
|
51338
|
-
return;
|
|
51339
|
-
const server = own.serverUrl ?? DEFAULT_SERVER_URL;
|
|
51340
|
-
return {
|
|
51341
|
-
token: own.token,
|
|
51342
|
-
field: "dispatch.token",
|
|
51343
|
-
path: config.path,
|
|
51344
|
-
server,
|
|
51345
|
-
checkout: false
|
|
51346
|
-
};
|
|
51347
|
-
}
|
|
51348
|
-
function lostDispatchUrlNotice(env, config) {
|
|
51349
|
-
if (nonEmpty(env.DISPATCH_TOKEN_FILE) === undefined)
|
|
51350
|
-
return;
|
|
51351
|
-
const link = config.missing?.link ?? null;
|
|
51352
|
-
const old = config.missing?.oldUserFileNotice;
|
|
51353
|
-
return [
|
|
51354
|
-
"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",
|
|
51355
|
-
...link === null ? [] : [`${config.path} is a link to ${link}, which does not exist`],
|
|
51356
|
-
...old === undefined ? [] : [old]
|
|
51357
|
-
].join("; ");
|
|
51358
51509
|
}
|
|
51359
|
-
function
|
|
51360
|
-
const target = dispatchTarget(env, config);
|
|
51361
|
-
if (!target.enabled)
|
|
51362
|
-
return target;
|
|
51363
|
-
const { url, source } = target;
|
|
51364
|
-
let credential;
|
|
51510
|
+
function handedConfigFile(env) {
|
|
51365
51511
|
try {
|
|
51366
|
-
|
|
51512
|
+
const handed = handedConfigPath(env);
|
|
51513
|
+
return handed === undefined ? null : { path: handed, error: null };
|
|
51367
51514
|
} catch (error) {
|
|
51368
|
-
return
|
|
51369
|
-
}
|
|
51370
|
-
const publicUrl = nonEmpty(env.DISPATCH_PUBLIC_URL);
|
|
51371
|
-
const publicParsed = publicUrl === undefined ? undefined : httpUrl2(publicUrl);
|
|
51372
|
-
if (publicUrl !== undefined && (publicParsed === undefined || publicParsed.port === "0")) {
|
|
51373
|
-
return offWithError("DISPATCH_PUBLIC_URL must be an http or https URL");
|
|
51515
|
+
return { path: null, error: messageFor(error) };
|
|
51374
51516
|
}
|
|
51375
|
-
return {
|
|
51376
|
-
enabled: true,
|
|
51377
|
-
url,
|
|
51378
|
-
credential,
|
|
51379
|
-
...publicUrl === undefined ? {} : { publicUrl: publicUrl.replace(/\/+$/, "") }
|
|
51380
|
-
};
|
|
51381
|
-
}
|
|
51382
|
-
function dispatchUrl(env, config) {
|
|
51383
|
-
const target = dispatchTarget(env, config);
|
|
51384
|
-
return target.enabled ? target.url : undefined;
|
|
51385
|
-
}
|
|
51386
|
-
function brokerRouteForced(env) {
|
|
51387
|
-
const written = (env.DISPATCH_TOKEN_BROKER ?? "").trim();
|
|
51388
|
-
if (written === "")
|
|
51389
|
-
return false;
|
|
51390
|
-
if (written === "1")
|
|
51391
|
-
return true;
|
|
51392
|
-
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`);
|
|
51393
51517
|
}
|
|
51394
|
-
function
|
|
51395
|
-
if (config.kind === "failed")
|
|
51396
|
-
return offWithError(config.error);
|
|
51397
|
-
const forced = brokerRouteForced(env);
|
|
51398
|
-
if (typeof forced !== "boolean")
|
|
51399
|
-
return forced;
|
|
51400
|
-
const explicitUrl = nonEmpty(env.DISPATCH_URL);
|
|
51401
|
-
const checkoutFile = explicitUrl === undefined ? config.checkout : null;
|
|
51402
|
-
if (checkoutFile?.kind === "invalid")
|
|
51403
|
-
return offWithError(checkoutFile.error);
|
|
51404
|
-
const checkout = checkoutFile?.kind === "valid" ? checkoutFile : null;
|
|
51405
|
-
const own = config.dispatch;
|
|
51406
|
-
if (own.enabled === false) {
|
|
51407
|
-
return offWithNotice(`${config.path} sets dispatch.enabled to false: Dispatch is off`);
|
|
51408
|
-
}
|
|
51409
|
-
if (checkout?.dispatch.enabled === false) {
|
|
51410
|
-
return offWithNotice(`${checkout.path} sets dispatch.enabled to false: Dispatch is off`);
|
|
51411
|
-
}
|
|
51412
|
-
const checkoutServer = checkout?.dispatch.serverUrl;
|
|
51413
|
-
const addressed = explicitUrl !== undefined || checkoutServer !== undefined || own.serverUrl !== undefined || own.enabled === true;
|
|
51414
|
-
if (!addressed) {
|
|
51415
|
-
const lost = lostDispatchUrlNotice(env, config);
|
|
51416
|
-
if (lost !== undefined)
|
|
51417
|
-
return offWithNotice(lost);
|
|
51418
|
-
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"));
|
|
51419
|
-
}
|
|
51420
|
-
if (explicitUrl !== undefined && httpUrl2(explicitUrl) === undefined) {
|
|
51421
|
-
return offWithError("DISPATCH_URL must be an http or https URL");
|
|
51422
|
-
}
|
|
51423
|
-
let source = { kind: "none" };
|
|
51424
|
-
if (!forced) {
|
|
51425
|
-
try {
|
|
51426
|
-
source = checkout !== null && checkoutServer !== undefined ? checkoutPair(checkout, checkoutServer) : bearerSource(env, "DISPATCH", () => writtenToken(config, checkout));
|
|
51427
|
-
} catch (error) {
|
|
51428
|
-
return offWithError(messageFor(error));
|
|
51429
|
-
}
|
|
51430
|
-
}
|
|
51431
|
-
if (source.kind === "none") {
|
|
51432
|
-
let route;
|
|
51433
|
-
try {
|
|
51434
|
-
route = brokerRouteConfigured(env);
|
|
51435
|
-
} catch (error) {
|
|
51436
|
-
return offWithError(messageFor(error));
|
|
51437
|
-
}
|
|
51438
|
-
if (route.traded) {
|
|
51439
|
-
const brokered = withoutTrailingSlashes2(explicitUrl ?? own.serverUrl ?? DEFAULT_SERVER_URL);
|
|
51440
|
-
return { enabled: true, url: brokered, source: { kind: "broker" } };
|
|
51441
|
-
}
|
|
51442
|
-
if (forced) {
|
|
51443
|
-
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`);
|
|
51444
|
-
}
|
|
51445
|
-
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}`);
|
|
51446
|
-
}
|
|
51447
|
-
const url = (explicitUrl ?? (source.kind === "file" ? source.server : own.serverUrl ?? DEFAULT_SERVER_URL)).replace(/\/+$/, "");
|
|
51448
|
-
return { enabled: true, url, source };
|
|
51449
|
-
}
|
|
51450
|
-
|
|
51451
|
-
// ../envoy-client/src/session-settings.ts
|
|
51452
|
-
function sessionConfigFile(harness, env) {
|
|
51453
|
-
try {
|
|
51454
|
-
return { path: envoyConfigPath(harness, env), error: null };
|
|
51455
|
-
} catch (error) {
|
|
51456
|
-
return { path: null, error: messageFor(error) };
|
|
51457
|
-
}
|
|
51458
|
-
}
|
|
51459
|
-
function handedConfigFile(env) {
|
|
51460
|
-
try {
|
|
51461
|
-
const handed = handedConfigPath(env);
|
|
51462
|
-
return handed === undefined ? null : { path: handed, error: null };
|
|
51463
|
-
} catch (error) {
|
|
51464
|
-
return { path: null, error: messageFor(error) };
|
|
51465
|
-
}
|
|
51466
|
-
}
|
|
51467
|
-
function startNotice(config, dispatch, envoy, nats, lost) {
|
|
51518
|
+
function startNotice(config, dispatch, envoy, nats, lost) {
|
|
51468
51519
|
if (config.kind === "failed")
|
|
51469
51520
|
return `${config.error}: Dispatch and Envoy are off`;
|
|
51470
51521
|
const { missing, checkout } = config;
|
|
@@ -55211,604 +55262,953 @@ var import_nats2 = __toESM(require_mod4(), 1);
|
|
|
55211
55262
|
|
|
55212
55263
|
// package.json
|
|
55213
55264
|
var name = "@sjawhar/claude-envoy";
|
|
55214
|
-
var version2 = "1.
|
|
55265
|
+
var version2 = "1.19.0";
|
|
55215
55266
|
var legion = {
|
|
55216
55267
|
daemonApiVersion: 56
|
|
55217
55268
|
};
|
|
55218
55269
|
|
|
55219
|
-
// src/
|
|
55220
|
-
var
|
|
55221
|
-
|
|
55222
|
-
|
|
55223
|
-
|
|
55224
|
-
|
|
55225
|
-
|
|
55226
|
-
|
|
55227
|
-
|
|
55228
|
-
var DEFAULT_DRAIN_TIMEOUT_MS = 1000;
|
|
55229
|
-
function deliveryIdentity(raw) {
|
|
55230
|
-
let parsed;
|
|
55231
|
-
try {
|
|
55232
|
-
parsed = JSON.parse(raw);
|
|
55233
|
-
} 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 === "")
|
|
55234
55279
|
return;
|
|
55235
|
-
|
|
55236
|
-
|
|
55237
|
-
return
|
|
55280
|
+
if (typeof provider !== "string" || provider === "")
|
|
55281
|
+
return;
|
|
55282
|
+
return `${provider}/${id}`;
|
|
55238
55283
|
}
|
|
55239
|
-
function
|
|
55240
|
-
|
|
55241
|
-
|
|
55284
|
+
function readModel(message) {
|
|
55285
|
+
const model = modelReference(message.provider, message.model);
|
|
55286
|
+
return model === undefined ? undefined : capAgentStreamText(model, AGENT_STREAM_LIMITS.modelChars);
|
|
55242
55287
|
}
|
|
55243
|
-
function
|
|
55244
|
-
|
|
55245
|
-
|
|
55246
|
-
const
|
|
55247
|
-
|
|
55248
|
-
|
|
55249
|
-
for await (const { message, receivedAt } of readArrivals(subscription)) {
|
|
55250
|
-
const raw = decoder5.decode(message.data);
|
|
55251
|
-
const identity = deliveryIdentity(raw);
|
|
55252
|
-
let claim = options.relayed?.(topic) === true ? undefined : dedupe.claim(identity);
|
|
55253
|
-
if (claim !== undefined && identity !== undefined && options.ledger?.delivered(identity)) {
|
|
55254
|
-
claim.settle();
|
|
55255
|
-
claim = undefined;
|
|
55256
|
-
}
|
|
55257
|
-
const inbound = {
|
|
55258
|
-
subject: message.subject,
|
|
55259
|
-
data: message.data,
|
|
55260
|
-
raw,
|
|
55261
|
-
...message.reply ? { reply: message.reply } : {},
|
|
55262
|
-
...identity?.topic === undefined ? {} : { envelopeTopic: identity.topic },
|
|
55263
|
-
...claim === undefined ? { duplicate: true } : {}
|
|
55264
|
-
};
|
|
55265
|
-
let handed;
|
|
55266
|
-
try {
|
|
55267
|
-
handed = await options.deliver(inbound);
|
|
55268
|
-
} catch (error) {
|
|
55269
|
-
claim?.release();
|
|
55270
|
-
report(`could not deliver a message on ${message.subject}`, error);
|
|
55271
|
-
if (claim !== undefined)
|
|
55272
|
-
options.failed?.(inbound, topic, error, receivedAt);
|
|
55273
|
-
continue;
|
|
55274
|
-
}
|
|
55275
|
-
if (claim === undefined)
|
|
55276
|
-
continue;
|
|
55277
|
-
if (!handed) {
|
|
55278
|
-
claim.release();
|
|
55279
|
-
continue;
|
|
55280
|
-
}
|
|
55281
|
-
const taken = claim;
|
|
55282
|
-
const settle = () => taken.settle();
|
|
55283
|
-
if (identity === undefined || options.ledger === undefined)
|
|
55284
|
-
settle();
|
|
55285
|
-
else
|
|
55286
|
-
options.ledger.recordDelivered(identity).then(settle, settle);
|
|
55287
|
-
}
|
|
55288
|
-
} catch (error) {
|
|
55289
|
-
report(`delivery from ${topic} stopped`, error);
|
|
55290
|
-
}
|
|
55291
|
-
};
|
|
55292
|
-
const stop = async (topics) => {
|
|
55293
|
-
const stopping = topics.flatMap((topic) => {
|
|
55294
|
-
const entry = following.get(topic);
|
|
55295
|
-
if (entry === undefined)
|
|
55296
|
-
return [];
|
|
55297
|
-
following.delete(topic);
|
|
55298
|
-
entry.subscription.unsubscribe();
|
|
55299
|
-
return [entry.done];
|
|
55300
|
-
});
|
|
55301
|
-
await Promise.all(stopping);
|
|
55302
|
-
};
|
|
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;
|
|
55303
55294
|
return {
|
|
55304
|
-
|
|
55305
|
-
|
|
55306
|
-
|
|
55307
|
-
|
|
55308
|
-
|
|
55309
|
-
|
|
55310
|
-
|
|
55311
|
-
|
|
55312
|
-
|
|
55313
|
-
|
|
55314
|
-
|
|
55315
|
-
|
|
55316
|
-
|
|
55317
|
-
|
|
55318
|
-
|
|
55319
|
-
|
|
55320
|
-
|
|
55321
|
-
|
|
55322
|
-
|
|
55323
|
-
if (connection.isClosed())
|
|
55324
|
-
return;
|
|
55325
|
-
const timeout = Promise.withResolvers();
|
|
55326
|
-
const timer = setTimeout(timeout.resolve, drainTimeoutMs);
|
|
55327
|
-
try {
|
|
55328
|
-
await Promise.race([connection.drain(), timeout.promise]);
|
|
55329
|
-
} catch (error) {
|
|
55330
|
-
report("draining the broker connection failed", error);
|
|
55331
|
-
}
|
|
55332
|
-
clearTimeout(timer);
|
|
55333
|
-
if (connection.isClosed())
|
|
55334
|
-
return;
|
|
55335
|
-
try {
|
|
55336
|
-
await connection.close();
|
|
55337
|
-
} catch (error) {
|
|
55338
|
-
report("closing the broker connection failed", error);
|
|
55339
|
-
}
|
|
55340
|
-
}
|
|
55341
|
-
};
|
|
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}`;
|
|
55342
55314
|
}
|
|
55343
|
-
|
|
55344
|
-
|
|
55345
|
-
|
|
55346
|
-
|
|
55347
|
-
|
|
55348
|
-
|
|
55349
|
-
|
|
55350
|
-
|
|
55351
|
-
|
|
55352
|
-
|
|
55353
|
-
|
|
55354
|
-
|
|
55355
|
-
|
|
55356
|
-
|
|
55357
|
-
const at = spec.indexOf("@");
|
|
55358
|
-
if (at <= 0 || at === spec.length - 1)
|
|
55359
|
-
return;
|
|
55360
|
-
return { kind: "plugin", name: spec.slice(0, at), marketplace: spec.slice(at + 1), dev };
|
|
55361
|
-
}
|
|
55362
|
-
if (value.startsWith("server:") && value.length > "server:".length) {
|
|
55363
|
-
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));
|
|
55364
55329
|
}
|
|
55365
|
-
return
|
|
55330
|
+
return pieces.join(`
|
|
55331
|
+
`);
|
|
55366
55332
|
}
|
|
55367
|
-
|
|
55368
|
-
"
|
|
55369
|
-
"--prefill-b64",
|
|
55370
|
-
"--deep-link-repo",
|
|
55371
|
-
"--deep-link-last-fetch",
|
|
55372
|
-
"--deep-link-cwd-b64",
|
|
55373
|
-
"--handle-uri",
|
|
55374
|
-
"--settings",
|
|
55375
|
-
"--managed-settings",
|
|
55376
|
-
"--setting-sources",
|
|
55377
|
-
"--client-data-url",
|
|
55378
|
-
"--watch-artifact",
|
|
55379
|
-
"--watch-artifact-no-autoreact",
|
|
55380
|
-
"--team-name",
|
|
55381
|
-
"--agent-id",
|
|
55382
|
-
"--agent-name",
|
|
55383
|
-
"--agent-color",
|
|
55384
|
-
"--parent-session-id",
|
|
55385
|
-
"--agent-type",
|
|
55386
|
-
"--model",
|
|
55387
|
-
"--agent",
|
|
55388
|
-
"--routine",
|
|
55389
|
-
"--effort",
|
|
55390
|
-
"--fast-mode",
|
|
55391
|
-
"--permission-mode",
|
|
55392
|
-
"--inherit-permission-mode",
|
|
55393
|
-
"--proactivity",
|
|
55394
|
-
"--debug-file",
|
|
55395
|
-
"--system-prompt",
|
|
55396
|
-
"--system-prompt-file",
|
|
55397
|
-
"--append-system-prompt",
|
|
55398
|
-
"--append-system-prompt-file",
|
|
55399
|
-
"--system-prompt-snapshot",
|
|
55400
|
-
"--append-subagent-system-prompt",
|
|
55401
|
-
"--append-subagent-system-prompt-file",
|
|
55402
|
-
"--plan-mode-instructions",
|
|
55403
|
-
"--permission-prompt-tool",
|
|
55404
|
-
"--permission-prompts",
|
|
55405
|
-
"--json-schema",
|
|
55406
|
-
"--fallback-model",
|
|
55407
|
-
"--advisor",
|
|
55408
|
-
"--agents",
|
|
55409
|
-
"--name",
|
|
55410
|
-
"-n",
|
|
55411
|
-
"--allowedTools",
|
|
55412
|
-
"--allowed-tools",
|
|
55413
|
-
"--disallowedTools",
|
|
55414
|
-
"--disallowed-tools",
|
|
55415
|
-
"--tools",
|
|
55416
|
-
"--add-dir",
|
|
55417
|
-
"--mcp-config",
|
|
55418
|
-
"--betas",
|
|
55419
|
-
"--file",
|
|
55420
|
-
"--channels",
|
|
55421
|
-
"--dangerously-load-development-channels",
|
|
55422
|
-
"--plugin-dir",
|
|
55423
|
-
"--plugin-dir-no-mcp",
|
|
55424
|
-
"--plugin-url",
|
|
55425
|
-
"--remote-control-session-name-prefix",
|
|
55426
|
-
"--exec",
|
|
55427
|
-
"-m",
|
|
55428
|
-
"--sdk-url",
|
|
55429
|
-
"--thinking",
|
|
55430
|
-
"--thinking-display",
|
|
55431
|
-
"--max-thinking-tokens",
|
|
55432
|
-
"--max-turns",
|
|
55433
|
-
"--max-budget-usd",
|
|
55434
|
-
"--task-budget",
|
|
55435
|
-
"--autocompact",
|
|
55436
|
-
"--rewind-files",
|
|
55437
|
-
"--resume-session-at",
|
|
55438
|
-
"--resume-drops-turn",
|
|
55439
|
-
"--workload",
|
|
55440
|
-
"--output-format",
|
|
55441
|
-
"--input-format",
|
|
55442
|
-
"--teammate-mode",
|
|
55443
|
-
"--messaging-socket-path",
|
|
55444
|
-
"--session-id",
|
|
55445
|
-
"--environment",
|
|
55446
|
-
"--pool",
|
|
55447
|
-
"--ref",
|
|
55448
|
-
"--on-branch",
|
|
55449
|
-
"--correlation-id",
|
|
55450
|
-
"--forward-home-settings",
|
|
55451
|
-
"--project-config-root",
|
|
55452
|
-
"--attach-serve"
|
|
55453
|
-
]);
|
|
55454
|
-
var OPTIONAL_VALUE_OPTIONS = new Set([
|
|
55455
|
-
"-d",
|
|
55456
|
-
"--debug",
|
|
55457
|
-
"-r",
|
|
55458
|
-
"--resume",
|
|
55459
|
-
"--from-pr",
|
|
55460
|
-
"-w",
|
|
55461
|
-
"--worktree",
|
|
55462
|
-
"--teleport",
|
|
55463
|
-
"--cloud",
|
|
55464
|
-
"--remote",
|
|
55465
|
-
"--project",
|
|
55466
|
-
"--remote-control",
|
|
55467
|
-
"--rc",
|
|
55468
|
-
"--prompt-suggestions"
|
|
55469
|
-
]);
|
|
55470
|
-
var MULTI_VALUE_OPTIONS = new Set([
|
|
55471
|
-
"--allowedTools",
|
|
55472
|
-
"--allowed-tools",
|
|
55473
|
-
"--disallowedTools",
|
|
55474
|
-
"--disallowed-tools",
|
|
55475
|
-
"--tools",
|
|
55476
|
-
"--add-dir",
|
|
55477
|
-
"--mcp-config",
|
|
55478
|
-
"--betas",
|
|
55479
|
-
"--file",
|
|
55480
|
-
"--channels",
|
|
55481
|
-
"--dangerously-load-development-channels"
|
|
55482
|
-
]);
|
|
55483
|
-
function isOption(argument) {
|
|
55484
|
-
return argument.length > 1 && argument.startsWith("-");
|
|
55485
|
-
}
|
|
55486
|
-
function lastValueIndex(argv, index) {
|
|
55487
|
-
const argument = argv[index] ?? "";
|
|
55488
|
-
if (OPTIONAL_VALUE_OPTIONS.has(argument)) {
|
|
55489
|
-
const next = argv[index + 1];
|
|
55490
|
-
return next !== undefined && !isOption(next) ? index + 1 : index;
|
|
55491
|
-
}
|
|
55492
|
-
if (!VALUE_OPTIONS.has(argument))
|
|
55493
|
-
return index;
|
|
55494
|
-
let last = index + 1;
|
|
55495
|
-
if (MULTI_VALUE_OPTIONS.has(argument)) {
|
|
55496
|
-
while (last + 1 < argv.length && !isOption(argv[last + 1] ?? ""))
|
|
55497
|
-
last++;
|
|
55498
|
-
}
|
|
55499
|
-
return last;
|
|
55500
|
-
}
|
|
55501
|
-
function asksForPrintMode(argv) {
|
|
55502
|
-
for (let index = 1;index < argv.length; index++) {
|
|
55503
|
-
const argument = argv[index] ?? "";
|
|
55504
|
-
if (argument === "--")
|
|
55505
|
-
return false;
|
|
55506
|
-
if (argument === "-p" || argument === "--print" || argument === "--init-only" || argument.startsWith("--sdk-url")) {
|
|
55507
|
-
return true;
|
|
55508
|
-
}
|
|
55509
|
-
index = lastValueIndex(argv, index);
|
|
55510
|
-
}
|
|
55511
|
-
return false;
|
|
55333
|
+
function boundedStrings(_key, value) {
|
|
55334
|
+
return typeof value === "string" ? capAgentStreamText(value, AGENT_STREAM_LIMITS.toolChars) : value;
|
|
55512
55335
|
}
|
|
55513
|
-
function
|
|
55514
|
-
|
|
55515
|
-
|
|
55516
|
-
|
|
55517
|
-
|
|
55518
|
-
|
|
55519
|
-
|
|
55520
|
-
break;
|
|
55521
|
-
const equals = argument.indexOf("=");
|
|
55522
|
-
const flag = equals === -1 ? argument : argument.slice(0, equals);
|
|
55523
|
-
const dev = CHANNEL_FLAGS[flag];
|
|
55524
|
-
if (dev === undefined) {
|
|
55525
|
-
if (exact)
|
|
55526
|
-
index = lastValueIndex(argv, index);
|
|
55527
|
-
continue;
|
|
55528
|
-
}
|
|
55529
|
-
const values = [];
|
|
55530
|
-
if (equals !== -1)
|
|
55531
|
-
values.push(argument.slice(equals + 1));
|
|
55532
|
-
else {
|
|
55533
|
-
while (index + 1 < argv.length && !isOption(argv[index + 1] ?? "-")) {
|
|
55534
|
-
values.push(argv[index + 1] ?? "");
|
|
55535
|
-
index++;
|
|
55536
|
-
}
|
|
55537
|
-
}
|
|
55538
|
-
if (dev && !interactive)
|
|
55539
|
-
continue;
|
|
55540
|
-
for (const value of values) {
|
|
55541
|
-
const entry = parseEntry(value, dev);
|
|
55542
|
-
if (entry !== undefined)
|
|
55543
|
-
(dev ? development : approved).push(entry);
|
|
55544
|
-
}
|
|
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]";
|
|
55545
55343
|
}
|
|
55546
|
-
return [...approved, ...development];
|
|
55547
55344
|
}
|
|
55548
|
-
function
|
|
55549
|
-
const {
|
|
55550
|
-
if (
|
|
55551
|
-
return {
|
|
55552
|
-
const ours = plugin === undefined ? "server:<name>" : `plugin:${plugin.name}@${plugin.marketplace ?? "<marketplace>"}`;
|
|
55553
|
-
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}`);
|
|
55554
|
-
if (entry === undefined) {
|
|
55555
|
-
return {
|
|
55556
|
-
loaded: false,
|
|
55557
|
-
why: `claude was started without --channels ${ours} (or --dangerously-load-development-channels ${ours})`
|
|
55558
|
-
};
|
|
55559
|
-
}
|
|
55560
|
-
if (entry.kind === "server") {
|
|
55561
|
-
return entry.dev ? { loaded: true } : {
|
|
55562
|
-
loaded: false,
|
|
55563
|
-
why: `server:${entry.name} is not on Claude Code's approved channels allowlist (use --dangerously-load-development-channels server:${entry.name})`
|
|
55564
|
-
};
|
|
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" }];
|
|
55565
55349
|
}
|
|
55566
|
-
|
|
55567
|
-
|
|
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) {
|
|
55568
55377
|
return {
|
|
55569
|
-
|
|
55570
|
-
|
|
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
|
|
55571
55388
|
};
|
|
55572
55389
|
}
|
|
55573
|
-
|
|
55574
|
-
|
|
55575
|
-
|
|
55576
|
-
|
|
55577
|
-
|
|
55578
|
-
|
|
55579
|
-
|
|
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);
|
|
55580
55472
|
}
|
|
55581
|
-
|
|
55582
|
-
|
|
55583
|
-
|
|
55584
|
-
|
|
55585
|
-
|
|
55586
|
-
|
|
55587
|
-
|
|
55588
|
-
|
|
55589
|
-
|
|
55590
|
-
function processExecutable(pid) {
|
|
55591
|
-
if (process.platform === "linux") {
|
|
55592
|
-
try {
|
|
55593
|
-
return readlinkSync3(`/proc/${pid}/exe`);
|
|
55594
|
-
} catch {
|
|
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)
|
|
55595
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)
|
|
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);
|
|
55596
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)));
|
|
55597
55513
|
}
|
|
55598
|
-
|
|
55599
|
-
return;
|
|
55600
|
-
const result = spawnSync("lsof", ["-a", "-p", String(pid), "-d", "txt", "-Fn"], {
|
|
55601
|
-
encoding: "utf8",
|
|
55602
|
-
timeout: 2000
|
|
55603
|
-
});
|
|
55604
|
-
const first = (result.stdout ?? "").split(`
|
|
55605
|
-
`).find((line) => line.startsWith("n"));
|
|
55606
|
-
return first === undefined ? undefined : first.slice(1);
|
|
55607
|
-
}
|
|
55608
|
-
function stdoutIsTerminal(pid) {
|
|
55609
|
-
if (process.platform !== "linux")
|
|
55610
|
-
return;
|
|
55611
|
-
try {
|
|
55612
|
-
const target = readlinkSync3(`/proc/${pid}/fd/1`);
|
|
55613
|
-
return target.startsWith("/dev/pts/") || /^\/dev\/tty[^S]/.test(target) || target === "/dev/tty";
|
|
55614
|
-
} catch {
|
|
55615
|
-
return;
|
|
55514
|
+
#commandsFrame(commands, seq = this.#commandsSeq) {
|
|
55515
|
+
return { commands, kind: "commands", seq, v: AGENT_STREAM_PROTOCOL };
|
|
55616
55516
|
}
|
|
55617
|
-
|
|
55618
|
-
|
|
55619
|
-
try {
|
|
55620
|
-
return readlinkSync3(`/proc/${pid}/cwd`);
|
|
55621
|
-
} catch {
|
|
55622
|
-
return;
|
|
55517
|
+
#settingsFrame(settings) {
|
|
55518
|
+
return { ...settings, kind: "settings", seq: this.#settingsSeq, v: AGENT_STREAM_PROTOCOL };
|
|
55623
55519
|
}
|
|
55624
|
-
|
|
55625
|
-
|
|
55626
|
-
|
|
55627
|
-
|
|
55628
|
-
|
|
55629
|
-
|
|
55630
|
-
|
|
55631
|
-
|
|
55632
|
-
|
|
55633
|
-
|
|
55634
|
-
|
|
55635
|
-
|
|
55636
|
-
|
|
55637
|
-
|
|
55638
|
-
|
|
55639
|
-
|
|
55640
|
-
|
|
55641
|
-
|
|
55642
|
-
|
|
55643
|
-
|
|
55644
|
-
|
|
55645
|
-
|
|
55646
|
-
|
|
55647
|
-
|
|
55648
|
-
|
|
55649
|
-
|
|
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);
|
|
55650
55549
|
}
|
|
55651
|
-
|
|
55652
|
-
|
|
55653
|
-
|
|
55654
|
-
|
|
55655
|
-
|
|
55656
|
-
|
|
55657
|
-
|
|
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 };
|
|
55658
55586
|
}
|
|
55659
|
-
if (commandLine === undefined || commandLine.argv.length > 1)
|
|
55660
|
-
return commandLine;
|
|
55661
|
-
return basename2(executable(pid) ?? "").startsWith("node") ? undefined : commandLine;
|
|
55662
55587
|
}
|
|
55663
|
-
|
|
55664
|
-
|
|
55665
|
-
|
|
55666
|
-
|
|
55667
|
-
|
|
55668
|
-
|
|
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;
|
|
55669
55606
|
}
|
|
55670
|
-
if (
|
|
55671
|
-
|
|
55672
|
-
const
|
|
55673
|
-
|
|
55674
|
-
|
|
55675
|
-
|
|
55676
|
-
|
|
55677
|
-
|
|
55678
|
-
|
|
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)
|
|
55679
55624
|
return;
|
|
55680
|
-
}
|
|
55625
|
+
const to = text.indexOf(`</${tag}>`, from + open.length);
|
|
55626
|
+
if (to === -1)
|
|
55627
|
+
return;
|
|
55628
|
+
return text.slice(from + open.length, to).trim();
|
|
55681
55629
|
}
|
|
55682
|
-
|
|
55683
|
-
|
|
55684
|
-
|
|
55685
|
-
|
|
55686
|
-
|
|
55687
|
-
|
|
55688
|
-
|
|
55689
|
-
|
|
55690
|
-
|
|
55691
|
-
|
|
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;
|
|
55692
55644
|
}
|
|
55693
55645
|
return;
|
|
55694
55646
|
}
|
|
55695
|
-
function
|
|
55696
|
-
const
|
|
55697
|
-
|
|
55698
|
-
|
|
55699
|
-
|
|
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
|
+
};
|
|
55700
55700
|
}
|
|
55701
|
-
return;
|
|
55701
|
+
return { type: block.type };
|
|
55702
|
+
}
|
|
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;
|
|
55702
55709
|
}
|
|
55703
|
-
function
|
|
55704
|
-
|
|
55710
|
+
function roleOf(message) {
|
|
55711
|
+
if (message.role !== undefined)
|
|
55712
|
+
return message.role;
|
|
55713
|
+
return message.type === "assistant" ? "assistant" : "user";
|
|
55705
55714
|
}
|
|
55706
|
-
|
|
55707
|
-
|
|
55715
|
+
var PANE_SEEDED_COMMAND = "/rename";
|
|
55716
|
+
function isPaneSeededRename(line) {
|
|
55717
|
+
return line === PANE_SEEDED_COMMAND || line.startsWith(`${PANE_SEEDED_COMMAND} `);
|
|
55708
55718
|
}
|
|
55709
|
-
|
|
55710
|
-
|
|
55711
|
-
|
|
55712
|
-
|
|
55713
|
-
|
|
55714
|
-
|
|
55715
|
-
|
|
55716
|
-
|
|
55717
|
-
|
|
55718
|
-
|
|
55719
|
-
|
|
55720
|
-
|
|
55721
|
-
|
|
55722
|
-
|
|
55723
|
-
|
|
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
|
+
});
|
|
55724
55792
|
}
|
|
55725
|
-
if (equals === -1)
|
|
55726
|
-
index = lastValueIndex(argv, index);
|
|
55727
55793
|
}
|
|
55728
|
-
|
|
55729
|
-
|
|
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
|
+
};
|
|
55730
55882
|
}
|
|
55731
|
-
|
|
55732
|
-
|
|
55733
|
-
|
|
55734
|
-
|
|
55735
|
-
|
|
55736
|
-
|
|
55737
|
-
|
|
55738
|
-
|
|
55739
|
-
|
|
55740
|
-
|
|
55741
|
-
return value.flatMap((entry) => {
|
|
55742
|
-
const parsed = AllowedChannelEntry.safeParse(entry);
|
|
55743
|
-
return parsed.success ? [parsed.data] : [];
|
|
55744
|
-
});
|
|
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
|
+
}
|
|
55745
55893
|
}
|
|
55746
|
-
function
|
|
55747
|
-
|
|
55748
|
-
|
|
55749
|
-
|
|
55750
|
-
|
|
55751
|
-
|
|
55752
|
-
|
|
55753
|
-
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);
|
|
55754
55901
|
}
|
|
55755
55902
|
}
|
|
55756
|
-
|
|
55757
|
-
|
|
55758
|
-
|
|
55759
|
-
|
|
55760
|
-
|
|
55761
|
-
|
|
55762
|
-
|
|
55763
|
-
|
|
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())
|
|
55764
55956
|
return;
|
|
55765
|
-
|
|
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())
|
|
55766
55974
|
return;
|
|
55767
|
-
|
|
55768
|
-
|
|
55769
|
-
|
|
55770
|
-
|
|
55771
|
-
|
|
55772
|
-
}
|
|
55773
|
-
|
|
55774
|
-
|
|
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);
|
|
55775
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
|
+
};
|
|
55776
56087
|
}
|
|
55777
|
-
|
|
55778
|
-
|
|
55779
|
-
|
|
55780
|
-
|
|
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;
|
|
55781
56101
|
try {
|
|
55782
|
-
|
|
56102
|
+
parsed = JSON.parse(raw);
|
|
55783
56103
|
} catch {
|
|
55784
|
-
|
|
55785
|
-
}
|
|
55786
|
-
const
|
|
55787
|
-
|
|
55788
|
-
|
|
55789
|
-
|
|
55790
|
-
|
|
55791
|
-
|
|
55792
|
-
|
|
55793
|
-
|
|
55794
|
-
|
|
55795
|
-
const
|
|
55796
|
-
|
|
55797
|
-
|
|
55798
|
-
|
|
55799
|
-
|
|
55800
|
-
|
|
55801
|
-
|
|
55802
|
-
|
|
55803
|
-
|
|
55804
|
-
|
|
55805
|
-
|
|
55806
|
-
|
|
55807
|
-
|
|
55808
|
-
|
|
55809
|
-
|
|
55810
|
-
|
|
55811
|
-
|
|
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
|
+
};
|
|
55812
56212
|
}
|
|
55813
56213
|
|
|
55814
56214
|
// src/claude-session.ts
|
|
@@ -55830,12 +56230,12 @@ function claudeProjectDirectory(environment, fallback) {
|
|
|
55830
56230
|
|
|
55831
56231
|
// src/session-identity.ts
|
|
55832
56232
|
import { readFile as readFile2 } from "fs/promises";
|
|
55833
|
-
import { basename as
|
|
56233
|
+
import { basename as basename2, join as join7 } from "path";
|
|
55834
56234
|
|
|
55835
56235
|
// src/session-state.ts
|
|
55836
|
-
import { readFileSync as
|
|
56236
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
55837
56237
|
import { mkdir, readdir, rename, rm, stat, utimes, writeFile } from "fs/promises";
|
|
55838
|
-
import { dirname as dirname2, join as
|
|
56238
|
+
import { dirname as dirname2, join as join6 } from "path";
|
|
55839
56239
|
async function writeAtomicStateFile(file, contents) {
|
|
55840
56240
|
await mkdir(dirname2(file), { recursive: true, mode: 448 });
|
|
55841
56241
|
const temporary = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
@@ -55853,7 +56253,7 @@ async function pruneStateDirectory(directory, isStale) {
|
|
|
55853
56253
|
throw error;
|
|
55854
56254
|
});
|
|
55855
56255
|
for (const entry of entries) {
|
|
55856
|
-
const file =
|
|
56256
|
+
const file = join6(directory, entry);
|
|
55857
56257
|
if (!await isStale(file))
|
|
55858
56258
|
continue;
|
|
55859
56259
|
await rm(file, { recursive: true, force: true });
|
|
@@ -55868,7 +56268,7 @@ async function writtenLongAgo(file) {
|
|
|
55868
56268
|
}
|
|
55869
56269
|
var TITLES_DIRECTORY = "titles";
|
|
55870
56270
|
function sessionTitleFile(stateDirectory, sessionId) {
|
|
55871
|
-
return
|
|
56271
|
+
return join6(stateDirectory, TITLES_DIRECTORY, `${encodeURIComponent(sessionId)}.txt`);
|
|
55872
56272
|
}
|
|
55873
56273
|
async function writeChosenTitle(file, title) {
|
|
55874
56274
|
await writeAtomicStateFile(file, `${title}
|
|
@@ -55879,7 +56279,7 @@ async function removeChosenTitle(file) {
|
|
|
55879
56279
|
}
|
|
55880
56280
|
function readChosenTitleSync(file) {
|
|
55881
56281
|
try {
|
|
55882
|
-
return
|
|
56282
|
+
return readFileSync6(file, "utf8");
|
|
55883
56283
|
} catch {
|
|
55884
56284
|
return;
|
|
55885
56285
|
}
|
|
@@ -55896,7 +56296,7 @@ async function recordChosenTitle(sessionId, title) {
|
|
|
55896
56296
|
await writeChosenTitle(file, chosen);
|
|
55897
56297
|
}
|
|
55898
56298
|
async function pruneStaleSessionTitles(stateDirectory) {
|
|
55899
|
-
await pruneStateDirectory(
|
|
56299
|
+
await pruneStateDirectory(join6(stateDirectory, TITLES_DIRECTORY), writtenLongAgo);
|
|
55900
56300
|
}
|
|
55901
56301
|
|
|
55902
56302
|
// src/session-identity.ts
|
|
@@ -55921,14 +56321,14 @@ class SessionIdentity {
|
|
|
55921
56321
|
}
|
|
55922
56322
|
}
|
|
55923
56323
|
function sessionHandoffDirectory(stateDirectory, claudePid) {
|
|
55924
|
-
return
|
|
56324
|
+
return join7(stateDirectory, "sessions", String(claudePid));
|
|
55925
56325
|
}
|
|
55926
56326
|
function sessionHandoffFile(stateDirectory, claudePid) {
|
|
55927
|
-
return
|
|
56327
|
+
return join7(sessionHandoffDirectory(stateDirectory, claudePid), "session-id");
|
|
55928
56328
|
}
|
|
55929
56329
|
var ROLES_DIRECTORY = "roles";
|
|
55930
56330
|
function roleStateFile(stateDirectory, sessionId) {
|
|
55931
|
-
return
|
|
56331
|
+
return join7(stateDirectory, ROLES_DIRECTORY, `${encodeURIComponent(sessionId)}.json`);
|
|
55932
56332
|
}
|
|
55933
56333
|
|
|
55934
56334
|
class UnparseableRoleRecordError extends Error {
|
|
@@ -55979,14 +56379,14 @@ function processExists(pid) {
|
|
|
55979
56379
|
}
|
|
55980
56380
|
}
|
|
55981
56381
|
async function pruneStaleSessionHandoffs(stateDirectory) {
|
|
55982
|
-
await pruneStateDirectory(
|
|
55983
|
-
const pid =
|
|
56382
|
+
await pruneStateDirectory(join7(stateDirectory, "sessions"), (file) => {
|
|
56383
|
+
const pid = basename2(file);
|
|
55984
56384
|
return /^\d+$/.test(pid) && !processExists(Number(pid));
|
|
55985
56385
|
});
|
|
55986
56386
|
}
|
|
55987
56387
|
async function pruneStaleSessionRoles(stateDirectory, currentSessionId) {
|
|
55988
56388
|
const own = roleStateFile(stateDirectory, currentSessionId);
|
|
55989
|
-
await pruneStateDirectory(
|
|
56389
|
+
await pruneStateDirectory(join7(stateDirectory, ROLES_DIRECTORY), async (file) => file !== own && await writtenLongAgo(file));
|
|
55990
56390
|
}
|
|
55991
56391
|
|
|
55992
56392
|
// src/envoy-off.ts
|
|
@@ -56002,6 +56402,15 @@ function inboundOffChannelSession(options) {
|
|
|
56002
56402
|
onRoleRegained: () => {
|
|
56003
56403
|
return;
|
|
56004
56404
|
},
|
|
56405
|
+
capabilitiesChanged: () => {
|
|
56406
|
+
return;
|
|
56407
|
+
},
|
|
56408
|
+
observe: () => {
|
|
56409
|
+
return;
|
|
56410
|
+
},
|
|
56411
|
+
messageOf: () => {
|
|
56412
|
+
return;
|
|
56413
|
+
},
|
|
56005
56414
|
async rememberRole(role) {
|
|
56006
56415
|
await writePersistedRole(roleStateFile(stateDirectory, identity.id), {
|
|
56007
56416
|
session_id: identity.id,
|
|
@@ -56034,9 +56443,23 @@ async function serveEnvoyOff(serverInfo, refusal) {
|
|
|
56034
56443
|
}
|
|
56035
56444
|
}
|
|
56036
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
|
+
|
|
56037
56460
|
// src/legion-claim.ts
|
|
56038
56461
|
import { readdir as readdir2, stat as stat2 } from "fs/promises";
|
|
56039
|
-
import { join as
|
|
56462
|
+
import { join as join8 } from "path";
|
|
56040
56463
|
import { setTimeout as delay } from "timers/promises";
|
|
56041
56464
|
|
|
56042
56465
|
// ../pi-shared/src/legion-claim-core.ts
|
|
@@ -56122,12 +56545,12 @@ function stopPane(why) {
|
|
|
56122
56545
|
endPane();
|
|
56123
56546
|
}
|
|
56124
56547
|
async function waitForTranscript(configDir, sessionID, signal, pollMs = 100) {
|
|
56125
|
-
const projects =
|
|
56548
|
+
const projects = join8(configDir, "projects");
|
|
56126
56549
|
const name = `${sessionID}.jsonl`;
|
|
56127
56550
|
for (;; ) {
|
|
56128
56551
|
signal.throwIfAborted();
|
|
56129
56552
|
for (const directory of await readdir2(projects).catch(() => [])) {
|
|
56130
|
-
const file =
|
|
56553
|
+
const file = join8(projects, directory, name);
|
|
56131
56554
|
if (await isFile(file))
|
|
56132
56555
|
return file;
|
|
56133
56556
|
}
|
|
@@ -56275,10 +56698,14 @@ function createInboxNotifier(socketPath) {
|
|
|
56275
56698
|
};
|
|
56276
56699
|
return {
|
|
56277
56700
|
notification(notification) {
|
|
56278
|
-
const id = notification.
|
|
56701
|
+
const id = notification.eventId ?? randomUUID2();
|
|
56279
56702
|
const settle = Promise.withResolvers();
|
|
56280
56703
|
waiting.push({ id, settle });
|
|
56281
|
-
connected().write(`${JSON.stringify({
|
|
56704
|
+
connected().write(`${JSON.stringify({
|
|
56705
|
+
id,
|
|
56706
|
+
content: notification.content,
|
|
56707
|
+
...notification.blocks === undefined ? {} : { blocks: notification.blocks }
|
|
56708
|
+
})}
|
|
56282
56709
|
`);
|
|
56283
56710
|
return settle.promise;
|
|
56284
56711
|
}
|
|
@@ -56287,8 +56714,8 @@ function createInboxNotifier(socketPath) {
|
|
|
56287
56714
|
|
|
56288
56715
|
// src/legion-phase-hook.ts
|
|
56289
56716
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
56290
|
-
import { readFileSync as
|
|
56291
|
-
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";
|
|
56292
56719
|
|
|
56293
56720
|
// ../pi-shared/src/phase-reminder.ts
|
|
56294
56721
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
@@ -56753,7 +57180,7 @@ function phaseHookToolDefinition() {
|
|
|
56753
57180
|
};
|
|
56754
57181
|
}
|
|
56755
57182
|
function phaseStallFile(configDir, sessionID) {
|
|
56756
|
-
return
|
|
57183
|
+
return join9(configDir, `legion-phase-stall-${sessionID}.json`);
|
|
56757
57184
|
}
|
|
56758
57185
|
function paneReminderLog(write) {
|
|
56759
57186
|
const line = (level, message, context) => {
|
|
@@ -56768,7 +57195,7 @@ var NO_RECORD = { entry: undefined, recreatedNoticeSent: false };
|
|
|
56768
57195
|
function readPhaseStallRecord(file, log) {
|
|
56769
57196
|
let raw;
|
|
56770
57197
|
try {
|
|
56771
|
-
raw =
|
|
57198
|
+
raw = readFileSync7(file, "utf8");
|
|
56772
57199
|
} catch (error) {
|
|
56773
57200
|
const missing = typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
56774
57201
|
if (!missing) {
|
|
@@ -56806,7 +57233,7 @@ function readPhaseStallRecord(file, log) {
|
|
|
56806
57233
|
};
|
|
56807
57234
|
}
|
|
56808
57235
|
function writePhaseStallRecord(file, entry, recreatedNoticeSent, log) {
|
|
56809
|
-
const temporary =
|
|
57236
|
+
const temporary = join9(dirname3(file), `.legion-phase-stall-${randomUUID4()}`);
|
|
56810
57237
|
try {
|
|
56811
57238
|
writeFileSync4(temporary, JSON.stringify({ ...entry, ...recreatedNoticeSent ? { recreatedNoticeSent } : {} }), { mode: 384 });
|
|
56812
57239
|
renameSync3(temporary, file);
|
|
@@ -57201,84 +57628,699 @@ async function completeHandoff(runtime, args) {
|
|
|
57201
57628
|
if (args.ready === true) {
|
|
57202
57629
|
throw new Error("handoff_complete on a production check is no READY: leave ready out");
|
|
57203
57630
|
}
|
|
57204
|
-
if (claim.role !== check.role) {
|
|
57205
|
-
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;
|
|
57206
58273
|
}
|
|
57207
|
-
|
|
57208
|
-
|
|
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;
|
|
57209
58288
|
}
|
|
57210
|
-
|
|
57211
|
-
|
|
57212
|
-
|
|
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}`);
|
|
57213
58302
|
}
|
|
58303
|
+
return result.value;
|
|
58304
|
+
},
|
|
58305
|
+
async setEffort(effort) {
|
|
58306
|
+
const result = await link.request("set-effort", { effort });
|
|
58307
|
+
return result.effort;
|
|
57214
58308
|
}
|
|
57215
|
-
return daemon.handoffComplete({
|
|
57216
|
-
grantId: await mintClaimGrant(daemon, claim),
|
|
57217
|
-
summary: args.summary,
|
|
57218
|
-
verdict: args.verdict ?? "",
|
|
57219
|
-
ready: false,
|
|
57220
|
-
...args.wake_at === undefined ? {} : { wakeAt: args.wake_at }
|
|
57221
|
-
});
|
|
57222
|
-
}
|
|
57223
|
-
if (args.verdict === "pending") {
|
|
57224
|
-
throw new Error("handoff_complete's verdict is pass or fail on a phase completion; pending is a production check's");
|
|
57225
|
-
}
|
|
57226
|
-
if (args.wake_at !== undefined) {
|
|
57227
|
-
throw new Error("handoff_complete's wake_at is a production check's pending report alone; a phase completion carries none");
|
|
57228
|
-
}
|
|
57229
|
-
const report = await runtime.handoffReport(record.phase, claim.role);
|
|
57230
|
-
return daemon.handoffComplete({
|
|
57231
|
-
grantId: await mintClaimGrant(daemon, claim),
|
|
57232
|
-
summary: args.summary,
|
|
57233
|
-
verdict: args.verdict ?? "",
|
|
57234
|
-
ready: args.ready ?? false,
|
|
57235
|
-
...report
|
|
57236
|
-
});
|
|
57237
|
-
}
|
|
57238
|
-
|
|
57239
|
-
// src/legion-tool.ts
|
|
57240
|
-
var LEGION_TOOL_NAME = "legion";
|
|
57241
|
-
function legionToolDefinition() {
|
|
57242
|
-
return {
|
|
57243
|
-
name: LEGION_TOOL_NAME,
|
|
57244
|
-
description: legionToolDescription({
|
|
57245
|
-
envoyPublish: "mcp__legion__envoy_publish",
|
|
57246
|
-
before: PHASE_OP_SENTENCES.readRecord + PHASE_OP_SENTENCES.readReleaseRecords,
|
|
57247
|
-
after: PHASE_OP_SENTENCES.requestBackwardMove
|
|
57248
|
-
}),
|
|
57249
|
-
inputSchema: toJSONSchema(object({ op: _enum2(PHASE_OPS), ...phaseOpParameters(exports_external) }))
|
|
57250
58309
|
};
|
|
57251
58310
|
}
|
|
57252
|
-
async function executeLegionTool(runtime, input, signal) {
|
|
57253
|
-
const claim = runtime.claim();
|
|
57254
|
-
if (claim === undefined) {
|
|
57255
|
-
throw new Error("this session has no Legion claim yet: the pane registers with the daemon once its first turn starts");
|
|
57256
|
-
}
|
|
57257
|
-
return executePhaseOp({
|
|
57258
|
-
daemon: runtime.daemon,
|
|
57259
|
-
claim,
|
|
57260
|
-
handoffReport: runtime.handoffReport,
|
|
57261
|
-
branchHead: runtime.branchHead,
|
|
57262
|
-
...signal === undefined ? {} : { restart: { signal } }
|
|
57263
|
-
}, input);
|
|
57264
|
-
}
|
|
57265
58311
|
|
|
57266
58312
|
// src/replay-state.ts
|
|
58313
|
+
import { join as join10 } from "path";
|
|
58314
|
+
|
|
58315
|
+
// src/state-file.ts
|
|
57267
58316
|
import { readFile as readFile3 } from "fs/promises";
|
|
57268
|
-
|
|
57269
|
-
var ReplayStateFileSchema = ReplaySnapshotSchema.extend({
|
|
57270
|
-
consumer: ConsumerDeliverySnapshotSchema.optional()
|
|
57271
|
-
});
|
|
57272
|
-
function replayStateFile(stateDirectory, sessionId) {
|
|
57273
|
-
return join11(stateDirectory, `replay-${encodeURIComponent(sessionId)}.json`);
|
|
57274
|
-
}
|
|
57275
|
-
async function readReplayState(file) {
|
|
58317
|
+
async function readStateFile(file, schema, empty, refusal) {
|
|
57276
58318
|
let text;
|
|
57277
58319
|
try {
|
|
57278
58320
|
text = await readFile3(file, "utf8");
|
|
57279
58321
|
} catch (error) {
|
|
57280
58322
|
if (hasErrnoCode(error, "ENOENT"))
|
|
57281
|
-
return
|
|
58323
|
+
return empty;
|
|
57282
58324
|
throw error;
|
|
57283
58325
|
}
|
|
57284
58326
|
let parsed;
|
|
@@ -57287,10 +58329,21 @@ async function readReplayState(file) {
|
|
|
57287
58329
|
} catch {
|
|
57288
58330
|
throw new Error(`${file} is not JSON`);
|
|
57289
58331
|
}
|
|
57290
|
-
const
|
|
57291
|
-
if (!
|
|
57292
|
-
throw new Error(`${file}
|
|
57293
|
-
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");
|
|
57294
58347
|
}
|
|
57295
58348
|
async function writeReplayState(file, state) {
|
|
57296
58349
|
await writeAtomicStateFile(file, `${JSON.stringify(state)}
|
|
@@ -57315,16 +58368,16 @@ function replayStateWriter(file, restored) {
|
|
|
57315
58368
|
}
|
|
57316
58369
|
|
|
57317
58370
|
// src/session-title.ts
|
|
57318
|
-
import { basename as
|
|
58371
|
+
import { basename as basename3 } from "path";
|
|
57319
58372
|
|
|
57320
58373
|
// ../envoy-client/src/process-ancestry.ts
|
|
57321
58374
|
import { execFileSync } from "child_process";
|
|
57322
|
-
import { readFileSync as
|
|
58375
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
57323
58376
|
var MAX_ANCESTORS = 64;
|
|
57324
58377
|
var PARENT_LOOKUP_MS = 1000;
|
|
57325
58378
|
function procParentPid(pid) {
|
|
57326
58379
|
try {
|
|
57327
|
-
const stat =
|
|
58380
|
+
const stat = readFileSync8(`/proc/${pid}/stat`, "utf8");
|
|
57328
58381
|
const parent = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
|
|
57329
58382
|
return Number.isInteger(parent) ? parent : undefined;
|
|
57330
58383
|
} catch {
|
|
@@ -57454,7 +58507,7 @@ async function sessionTitle(environment, directory, sources = {}) {
|
|
|
57454
58507
|
return () => explicit;
|
|
57455
58508
|
const { serverPid, socket } = tmuxServer(configuredValue(environment.TMUX));
|
|
57456
58509
|
const pane = configuredValue(environment.TMUX_PANE)?.trim();
|
|
57457
|
-
const directoryName = oneLine(
|
|
58510
|
+
const directoryName = oneLine(basename3(directory) || directory);
|
|
57458
58511
|
const name = (socket === "" || serverPid === "" || pane === undefined ? undefined : await tmuxPaneTitle(socket, serverPid, pane, ownPane, query)) ?? (visible(directoryName) ? directoryName : "session");
|
|
57459
58512
|
return (sessionId) => {
|
|
57460
58513
|
const own = oneLine(chosen(sessionId) ?? "");
|
|
@@ -57462,13 +58515,382 @@ async function sessionTitle(environment, directory, sources = {}) {
|
|
|
57462
58515
|
};
|
|
57463
58516
|
}
|
|
57464
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
|
+
|
|
57465
58889
|
// src/envoy-channel-server.ts
|
|
57466
|
-
var
|
|
57467
|
-
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.";
|
|
57468
58891
|
var CHANNEL_INBOX_LIMIT = 50;
|
|
57469
58892
|
var EMPTY_RECEIPT = new Uint8Array;
|
|
57470
|
-
var
|
|
57471
|
-
var ChannelMetaKey = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
58893
|
+
var encoder6 = new TextEncoder;
|
|
57472
58894
|
var HANDOFF_POLL_MS = 250;
|
|
57473
58895
|
var HANDOFF_FLUSH_TIMEOUT_MS = 1e4;
|
|
57474
58896
|
async function withTimeout(promise, ms, what) {
|
|
@@ -57488,7 +58910,11 @@ function undefinedOnNotFound(error) {
|
|
|
57488
58910
|
throw error;
|
|
57489
58911
|
}
|
|
57490
58912
|
var MCP_SERVER_INFO = { name: "envoy", version: version2 };
|
|
57491
|
-
|
|
58913
|
+
function sessionCapabilities(session) {
|
|
58914
|
+
if (!session.pane)
|
|
58915
|
+
return session.mod ? DELIVERY_CAPABILITIES : [];
|
|
58916
|
+
return session.mod ? [...INBOX_CAPABILITIES, "btw"] : INBOX_CAPABILITIES;
|
|
58917
|
+
}
|
|
57492
58918
|
var argumentSchemas = new WeakMap;
|
|
57493
58919
|
function argumentsSchema(spec) {
|
|
57494
58920
|
let schema = argumentSchemas.get(spec);
|
|
@@ -57523,14 +58949,6 @@ class UnsupportedEnvoyToolError extends Error {
|
|
|
57523
58949
|
this.toolName = toolName;
|
|
57524
58950
|
}
|
|
57525
58951
|
}
|
|
57526
|
-
function sanitizeChannelMetadata(metadata) {
|
|
57527
|
-
const sanitized = {};
|
|
57528
|
-
for (const [key, value] of Object.entries(metadata)) {
|
|
57529
|
-
if (value !== undefined && ChannelMetaKey.test(key))
|
|
57530
|
-
sanitized[key] = value;
|
|
57531
|
-
}
|
|
57532
|
-
return sanitized;
|
|
57533
|
-
}
|
|
57534
58952
|
async function postDispatchReply(identity, configFile, delivery, result) {
|
|
57535
58953
|
const config = sessionDispatchSettings(configFile, process.env, {
|
|
57536
58954
|
cwd: identity.directory,
|
|
@@ -57543,47 +58961,45 @@ async function postDispatchReply(identity, configFile, delivery, result) {
|
|
|
57543
58961
|
function createChannelDelivery(input) {
|
|
57544
58962
|
const inbox = [];
|
|
57545
58963
|
let tail = Promise.resolve();
|
|
57546
|
-
const
|
|
57547
|
-
const
|
|
57548
|
-
tail =
|
|
57549
|
-
|
|
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)}
|
|
57550
58970
|
`);
|
|
57551
58971
|
});
|
|
57552
|
-
return
|
|
58972
|
+
return run;
|
|
57553
58973
|
};
|
|
57554
|
-
const
|
|
58974
|
+
const write = (notification) => queued(() => input.notifier.notification(notification));
|
|
58975
|
+
const list = (rendered) => {
|
|
57555
58976
|
const envelope = rendered.envelope;
|
|
57556
|
-
if (envelope
|
|
57557
|
-
|
|
57558
|
-
|
|
57559
|
-
|
|
57560
|
-
|
|
57561
|
-
|
|
57562
|
-
|
|
57563
|
-
|
|
57564
|
-
|
|
57565
|
-
|
|
57566
|
-
return queue({
|
|
57567
|
-
method: CHANNEL_NOTIFICATION_METHOD,
|
|
57568
|
-
params: {
|
|
57569
|
-
content,
|
|
57570
|
-
meta: sanitizeChannelMetadata({
|
|
57571
|
-
producer: envelope?.source ?? "unknown",
|
|
57572
|
-
topic: envelope?.topic || subject,
|
|
57573
|
-
event_id: envelope?.event_id,
|
|
57574
|
-
dedupe_key: envelope?.dedupe_key,
|
|
57575
|
-
urgency: envelope?.urgency,
|
|
57576
|
-
from_session: envelope?.source_session,
|
|
57577
|
-
expects_reply: envelope?.expects_reply?.toString(),
|
|
57578
|
-
in_reply_to: envelope?.in_reply_to
|
|
57579
|
-
})
|
|
57580
|
-
}
|
|
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"
|
|
57581
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));
|
|
57582
58998
|
};
|
|
57583
58999
|
const passOverBtw = (rendered, how) => {
|
|
57584
59000
|
if (rendered.delivery?.mode !== "btw")
|
|
57585
59001
|
return false;
|
|
57586
|
-
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
|
|
57587
59003
|
`);
|
|
57588
59004
|
return true;
|
|
57589
59005
|
};
|
|
@@ -57611,7 +59027,16 @@ function createChannelDelivery(input) {
|
|
|
57611
59027
|
}
|
|
57612
59028
|
if (late === true && passOverBtw(rendered, "late"))
|
|
57613
59029
|
return tail.then(() => false);
|
|
57614
|
-
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
|
+
});
|
|
57615
59040
|
},
|
|
57616
59041
|
async replay(frame) {
|
|
57617
59042
|
const rendered = renderInbound(frame.data, input.identity.id, frame.subject);
|
|
@@ -57619,14 +59044,11 @@ function createChannelDelivery(input) {
|
|
|
57619
59044
|
return "suppressed";
|
|
57620
59045
|
if (passOverBtw(rendered, "replayed"))
|
|
57621
59046
|
return "suppressed";
|
|
57622
|
-
await show(rendered,
|
|
59047
|
+
await show(rendered, replayedText(rendered.content, frame.time));
|
|
57623
59048
|
return "shown";
|
|
57624
59049
|
},
|
|
57625
59050
|
notice(content) {
|
|
57626
|
-
return
|
|
57627
|
-
method: CHANNEL_NOTIFICATION_METHOD,
|
|
57628
|
-
params: { content, meta: { producer: "envoy" } }
|
|
57629
|
-
});
|
|
59051
|
+
return write({ content });
|
|
57630
59052
|
},
|
|
57631
59053
|
inbox() {
|
|
57632
59054
|
return [...inbox];
|
|
@@ -57648,10 +59070,13 @@ async function enqueueChannelMessage(delivery, connection, directSubject, messag
|
|
|
57648
59070
|
async function startChannelSession(options) {
|
|
57649
59071
|
const { identity } = options;
|
|
57650
59072
|
let directSubject = agentSubject(identity.id);
|
|
59073
|
+
let router;
|
|
59074
|
+
let turns;
|
|
57651
59075
|
const delivery = createChannelDelivery({
|
|
57652
59076
|
identity,
|
|
57653
59077
|
notifier: options.notifier,
|
|
57654
|
-
configFile: options.configFile
|
|
59078
|
+
configFile: options.configFile,
|
|
59079
|
+
router: () => router
|
|
57655
59080
|
});
|
|
57656
59081
|
const userTopics = new Set;
|
|
57657
59082
|
let heldRole;
|
|
@@ -57761,6 +59186,22 @@ async function startChannelSession(options) {
|
|
|
57761
59186
|
replayBound = sessionId;
|
|
57762
59187
|
return rememberedTopics(snapshot.state);
|
|
57763
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
|
+
};
|
|
57764
59205
|
const triggerReplay = () => {
|
|
57765
59206
|
if (!shuttingDown)
|
|
57766
59207
|
replay.sweep();
|
|
@@ -57820,7 +59261,7 @@ async function startChannelSession(options) {
|
|
|
57820
59261
|
`);
|
|
57821
59262
|
}
|
|
57822
59263
|
},
|
|
57823
|
-
publish: (reply, body) => options.connection.publish(reply,
|
|
59264
|
+
publish: (reply, body) => options.connection.publish(reply, encoder6.encode(body)),
|
|
57824
59265
|
persist: (snapshot) => {
|
|
57825
59266
|
if (relay !== owner)
|
|
57826
59267
|
return false;
|
|
@@ -57949,7 +59390,7 @@ async function startChannelSession(options) {
|
|
|
57949
59390
|
topics,
|
|
57950
59391
|
port: 0,
|
|
57951
59392
|
title,
|
|
57952
|
-
capabilities: options.capabilities,
|
|
59393
|
+
capabilities: options.capabilities(),
|
|
57953
59394
|
driving: false,
|
|
57954
59395
|
selfSubscribed: true,
|
|
57955
59396
|
clientInstanceID: options.clientInstanceID,
|
|
@@ -58094,6 +59535,7 @@ async function startChannelSession(options) {
|
|
|
58094
59535
|
directSubject = nextSubject;
|
|
58095
59536
|
forwarder.unfollow([previousSubject]);
|
|
58096
59537
|
await restoreReplay();
|
|
59538
|
+
await restoreTurns();
|
|
58097
59539
|
roleTransferFrom ??= previous;
|
|
58098
59540
|
await withTimeout(options.connection.flush(), options.handoffFlushTimeoutMs ?? HANDOFF_FLUSH_TIMEOUT_MS, "flushing NATS").catch((error) => {
|
|
58099
59541
|
process.stderr.write(`envoy-channel: could not confirm the relay subscription of ${next} \u2014 ${messageFor(error)}
|
|
@@ -58208,6 +59650,7 @@ async function startChannelSession(options) {
|
|
|
58208
59650
|
heartbeat();
|
|
58209
59651
|
};
|
|
58210
59652
|
const remembered = await restoreReplay();
|
|
59653
|
+
await restoreTurns();
|
|
58211
59654
|
forwarder.follow(directSubject);
|
|
58212
59655
|
for (const topic of remembered) {
|
|
58213
59656
|
userTopics.add(topic);
|
|
@@ -58238,6 +59681,7 @@ async function startChannelSession(options) {
|
|
|
58238
59681
|
for (const prune of [
|
|
58239
59682
|
() => pruneStaleSessionHandoffs(options.stateDirectory),
|
|
58240
59683
|
() => pruneStaleSessionTitles(options.stateDirectory),
|
|
59684
|
+
() => pruneStaleSessionTurns(options.stateDirectory),
|
|
58241
59685
|
() => pruneStaleSessionRoles(options.stateDirectory, identity.id)
|
|
58242
59686
|
]) {
|
|
58243
59687
|
await prune().catch((error) => {
|
|
@@ -58305,6 +59749,20 @@ async function startChannelSession(options) {
|
|
|
58305
59749
|
onRoleRegained(hook) {
|
|
58306
59750
|
regained = hook;
|
|
58307
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
|
+
},
|
|
58308
59766
|
async rememberRole(role) {
|
|
58309
59767
|
heldRole = role;
|
|
58310
59768
|
await writeRoleRecord(roleStateFile(options.stateDirectory, identity.id), role);
|
|
@@ -58326,6 +59784,7 @@ async function startChannelSession(options) {
|
|
|
58326
59784
|
`);
|
|
58327
59785
|
});
|
|
58328
59786
|
await stateWriter?.idle();
|
|
59787
|
+
await turns?.idle();
|
|
58329
59788
|
}
|
|
58330
59789
|
};
|
|
58331
59790
|
}
|
|
@@ -58499,18 +59958,9 @@ async function runEnvoyChannelServer() {
|
|
|
58499
59958
|
const inboxSocket = configuredValue(process.env[INBOX_VARIABLE]);
|
|
58500
59959
|
const bundlePath = process.argv[1] ?? "";
|
|
58501
59960
|
const inbox = inboxSocket === undefined ? undefined : createInboxNotifier(inboxSocket);
|
|
58502
|
-
const channel = inboxSocket === undefined ? detectClaudeChannel({
|
|
58503
|
-
claudePid: process.ppid,
|
|
58504
|
-
bundlePath,
|
|
58505
|
-
environment: process.env
|
|
58506
|
-
}) : { loaded: true };
|
|
58507
|
-
if (!channel.loaded) {
|
|
58508
|
-
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
|
|
58509
|
-
`);
|
|
58510
|
-
}
|
|
58511
59961
|
const server = new Server(MCP_SERVER_INFO, {
|
|
58512
|
-
capabilities: { tools: {}
|
|
58513
|
-
instructions:
|
|
59962
|
+
capabilities: { tools: {} },
|
|
59963
|
+
instructions: CHANNEL_INSTRUCTIONS
|
|
58514
59964
|
});
|
|
58515
59965
|
const client = createEnvoyClient({
|
|
58516
59966
|
baseUrl: envoy.url,
|
|
@@ -58529,10 +59979,7 @@ async function runEnvoyChannelServer() {
|
|
|
58529
59979
|
booted: claimBooted.promise,
|
|
58530
59980
|
readState: (signal) => daemon.state({ signal }),
|
|
58531
59981
|
reportActivity: (input) => daemon.activity(input),
|
|
58532
|
-
inbox: inbox === undefined ? undefined : (text) => inbox.notification({
|
|
58533
|
-
method: CHANNEL_NOTIFICATION_METHOD,
|
|
58534
|
-
params: { content: text, meta: {} }
|
|
58535
|
-
}),
|
|
59982
|
+
inbox: inbox === undefined ? undefined : (text) => inbox.notification({ content: text }),
|
|
58536
59983
|
publish: client.publish,
|
|
58537
59984
|
env: process.env,
|
|
58538
59985
|
log: phaseLog
|
|
@@ -58565,13 +60012,39 @@ async function runEnvoyChannelServer() {
|
|
|
58565
60012
|
throw new Error("Envoy channel server is still starting");
|
|
58566
60013
|
return mcpResult(await executeEnvoyTool(runtime, request.params.name, request.params.arguments));
|
|
58567
60014
|
});
|
|
58568
|
-
|
|
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 : {
|
|
58569
60042
|
notification: async (notification) => {
|
|
58570
|
-
phaseHook.deliveryWritten(notification.
|
|
58571
|
-
await
|
|
60043
|
+
phaseHook.deliveryWritten(notification.content);
|
|
60044
|
+
await writer.notification(notification);
|
|
58572
60045
|
}
|
|
58573
60046
|
};
|
|
58574
|
-
const capabilities = inboxSocket !== undefined
|
|
60047
|
+
const capabilities = () => sessionCapabilities({ pane: inboxSocket !== undefined, mod: modLink?.connected() === true });
|
|
58575
60048
|
const booting = new AbortController;
|
|
58576
60049
|
const stopped = Promise.withResolvers();
|
|
58577
60050
|
let stopping = false;
|
|
@@ -58582,6 +60055,10 @@ async function runEnvoyChannelServer() {
|
|
|
58582
60055
|
stopping = true;
|
|
58583
60056
|
booting.abort();
|
|
58584
60057
|
phaseHook?.stop();
|
|
60058
|
+
modLink?.close().catch(() => {
|
|
60059
|
+
return;
|
|
60060
|
+
});
|
|
60061
|
+
agentStream?.stop();
|
|
58585
60062
|
if (session === undefined) {
|
|
58586
60063
|
stopped.resolve();
|
|
58587
60064
|
return;
|
|
@@ -58593,6 +60070,41 @@ async function runEnvoyChannelServer() {
|
|
|
58593
60070
|
process.once("SIGINT", stop);
|
|
58594
60071
|
process.once("SIGHUP", stop);
|
|
58595
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
|
+
};
|
|
58596
60108
|
let connection;
|
|
58597
60109
|
try {
|
|
58598
60110
|
const { inbound } = envoy;
|
|
@@ -58613,6 +60125,7 @@ async function runEnvoyChannelServer() {
|
|
|
58613
60125
|
connection,
|
|
58614
60126
|
notifier,
|
|
58615
60127
|
capabilities,
|
|
60128
|
+
router,
|
|
58616
60129
|
client,
|
|
58617
60130
|
heartbeatMs: envoy.heartbeatMs,
|
|
58618
60131
|
configFile,
|
|
@@ -58623,6 +60136,31 @@ async function runEnvoyChannelServer() {
|
|
|
58623
60136
|
...stamping ? { stampTitle } : {},
|
|
58624
60137
|
...configuredValue(overrideSessionId) === undefined ? { handoffPid: process.ppid } : {}
|
|
58625
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
|
+
});
|
|
58626
60164
|
}
|
|
58627
60165
|
runtime = { identity, client, session, clientInstanceID };
|
|
58628
60166
|
if (pane !== undefined && daemon !== undefined && !stopping) {
|
|
@@ -58643,6 +60181,9 @@ async function runEnvoyChannelServer() {
|
|
|
58643
60181
|
await session.shutdown();
|
|
58644
60182
|
await stopped.promise;
|
|
58645
60183
|
} finally {
|
|
60184
|
+
await modLink?.close().catch(() => {
|
|
60185
|
+
return;
|
|
60186
|
+
});
|
|
58646
60187
|
if (session === undefined && connection !== undefined) {
|
|
58647
60188
|
await connection.drain().catch(() => {
|
|
58648
60189
|
return;
|