@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.
@@ -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 because a closed issue refuses messages, comments, and artifacts";
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, because a closed issue refuses messages, comments, and " + "artifacts; 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 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}`,
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 `![${pictureCaption(name)}](${address})`;
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(PICTURE_SYNTAX)].map((match) => match[1]);
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
- ...result
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/dispatch-config.ts
51311
- var DEFAULT_SERVER_URL = "http://localhost:8766";
51312
- function withoutTrailingSlashes2(url) {
51313
- let end = url.length;
51314
- while (end > 0 && url[end - 1] === "/")
51315
- end -= 1;
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 dispatchSettings(env, config, options = {}) {
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
- credential = source.kind === "broker" ? brokerCredential(env, options.session) : bearerCredential(source.kind === "variable" ? source.token : fileBearer(source, url, "DISPATCH", env, options.commands));
51512
+ const handed = handedConfigPath(env);
51513
+ return handed === undefined ? null : { path: handed, error: null };
51367
51514
  } catch (error) {
51368
- return offWithError(messageFor(error));
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 dispatchTarget(env, config) {
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.17.0";
55265
+ var version2 = "1.19.0";
55215
55266
  var legion = {
55216
55267
  daemonApiVersion: 56
55217
55268
  };
55218
55269
 
55219
- // src/channel-forwarder.ts
55220
- var DeliveryIdentity = DedupeIdentitySchema.extend({
55221
- event_id: DedupeIdentitySchema.shape.event_id.catch(undefined),
55222
- dedupe_key: DedupeIdentitySchema.shape.dedupe_key.catch(undefined),
55223
- source: DedupeIdentitySchema.shape.source.catch(undefined),
55224
- source_event_id: DedupeIdentitySchema.shape.source_event_id.catch(undefined),
55225
- topic: string2().min(1).optional().catch(undefined)
55226
- });
55227
- var decoder5 = new TextDecoder;
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
- const identity = DeliveryIdentity.safeParse(parsed);
55237
- return identity.success ? identity.data : undefined;
55280
+ if (typeof provider !== "string" || provider === "")
55281
+ return;
55282
+ return `${provider}/${id}`;
55238
55283
  }
55239
- function report(what, error) {
55240
- process.stderr.write(`envoy-channel: ${what} \u2014 ${messageFor(error)}
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 createChannelForwarder(connection, options) {
55244
- const following = new Map;
55245
- const dedupe = options.dedupe ?? createDeliveryDedupe();
55246
- const drainTimeoutMs = options.drainTimeoutMs ?? DEFAULT_DRAIN_TIMEOUT_MS;
55247
- const deliver = async (topic, subscription) => {
55248
- try {
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
- follow(topic) {
55305
- for (const subject of expandSubscriptionTopics([topic])) {
55306
- if (following.has(subject))
55307
- continue;
55308
- const subscription = connection.subscribe(subject);
55309
- following.set(subject, { subscription, done: deliver(subject, subscription) });
55310
- }
55311
- },
55312
- unfollow(topics) {
55313
- return stop(topics.length === 0 ? [...following.keys()] : expandSubscriptionTopics(topics));
55314
- },
55315
- topics() {
55316
- return [...following.keys()];
55317
- },
55318
- isClosed() {
55319
- return connection.isClosed();
55320
- },
55321
- async close() {
55322
- await stop([...following.keys()]);
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
- // src/claude-channel.ts
55345
- import { spawnSync } from "child_process";
55346
- import { readdirSync as readdirSync3, readFileSync as readFileSync6, readlinkSync as readlinkSync3 } from "fs";
55347
- import { homedir as homedir3 } from "os";
55348
- import { basename as basename2, join as join6, resolve as resolve3, sep } from "path";
55349
- var PLUGIN_SERVER = "envoy";
55350
- var CHANNEL_FLAGS = {
55351
- "--channels": false,
55352
- "--dangerously-load-development-channels": true
55353
- };
55354
- function parseEntry(value, dev) {
55355
- if (value.startsWith("plugin:")) {
55356
- const spec = value.slice("plugin:".length);
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
- var VALUE_OPTIONS = new Set([
55368
- "--prefill",
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 claudeChannelEntries(argv, exact = true, stdoutIsTerminal = undefined) {
55514
- const interactive = !exact || !asksForPrintMode(argv) && stdoutIsTerminal !== false;
55515
- const approved = [];
55516
- const development = [];
55517
- for (let index = 1;index < argv.length; index++) {
55518
- const argument = argv[index] ?? "";
55519
- if (exact && argument === "--")
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 claudeChannelGate(input) {
55549
- const { commandLine, plugin, allowlist } = input;
55550
- if (commandLine === undefined)
55551
- return { loaded: true };
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
- const asked = `plugin:${entry.name}@${entry.marketplace}`;
55567
- if (plugin?.marketplace !== undefined && cacheDirectoryName(entry.marketplace ?? "") !== cacheDirectoryName(plugin.marketplace)) {
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
- loaded: false,
55570
- why: `claude asked for ${asked} but this plugin is installed from ${plugin.marketplace}`
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
- if (entry.dev)
55574
- return { loaded: true };
55575
- if (allowlist === undefined) {
55576
- return {
55577
- loaded: false,
55578
- why: `${asked} is not on Claude Code's approved channels allowlist (add it to allowedChannelPlugins in ${MANAGED_SETTINGS_HINT}; for one session use --dangerously-load-development-channels ${asked})`
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
- return allowlist.some((allowed) => allowed.marketplace === entry.marketplace && allowed.plugin === entry.name) ? { loaded: true } : {
55582
- loaded: false,
55583
- why: `${asked} is not on Claude Code's approved channels allowlist (allowedChannelPlugins in ${MANAGED_SETTINGS_HINT})`
55584
- };
55585
- }
55586
- var MANAGED_SETTINGS_HINT = `${join6(managedSettingsDirectory(), "managed-settings.json")}, or in the server-managed settings when the account receives them`;
55587
- function cacheDirectoryName(marketplace) {
55588
- return marketplace.replace(/[^a-zA-Z0-9\-_]/g, "-");
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
- if (process.platform === "win32")
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
- function workingDirectory(pid) {
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
- function processCommandLine(pid, executable = processExecutable) {
55626
- let commandLine;
55627
- if (process.platform === "linux") {
55628
- try {
55629
- const pieces = readFileSync6(`/proc/${pid}/cmdline`, "utf8").split("\x00");
55630
- let trailing = 0;
55631
- while (pieces.length > 0 && pieces.at(-1) === "") {
55632
- pieces.pop();
55633
- trailing++;
55634
- }
55635
- if (trailing > 1 && pieces.length === 1)
55636
- return;
55637
- for (let blank = 1;blank < trailing; blank++)
55638
- pieces.push("");
55639
- const argv = pieces;
55640
- const terminal = stdoutIsTerminal(pid);
55641
- const cwd = workingDirectory(pid);
55642
- commandLine = argv.length === 0 ? undefined : {
55643
- argv,
55644
- exact: true,
55645
- ...terminal === undefined ? {} : { stdoutIsTerminal: terminal },
55646
- ...cwd === undefined ? {} : { cwd }
55647
- };
55648
- } catch {
55649
- commandLine = undefined;
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
- } else if (process.platform !== "win32") {
55652
- const result = spawnSync("ps", ["-ww", "-o", "args=", "-p", String(pid)], {
55653
- encoding: "utf8",
55654
- timeout: 1000
55655
- });
55656
- const line = result.status === 0 ? result.stdout.trim() : "";
55657
- commandLine = line === "" ? undefined : { argv: line.split(/\s+/), exact: false };
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
- function parentProcess(pid) {
55664
- try {
55665
- if (process.platform === "linux") {
55666
- const stat = readFileSync6(`/proc/${pid}/stat`, "utf8");
55667
- const parent = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
55668
- return Number.isInteger(parent) && parent > 0 ? parent : undefined;
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 (process.platform === "win32")
55671
- return;
55672
- const result = spawnSync("ps", ["-o", "ppid=", "-p", String(pid)], {
55673
- encoding: "utf8",
55674
- timeout: 1000
55675
- });
55676
- const parent = Number((result.stdout ?? "").trim());
55677
- return result.status === 0 && Number.isInteger(parent) && parent > 0 ? parent : undefined;
55678
- } catch {
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
- var MAX_CLAUDE_DEPTH = 8;
55683
- function claudeCommandLine(pid, read = processCommandLine, parent = parentProcess) {
55684
- let current = pid;
55685
- for (let depth = 0;current !== undefined && current > 1 && depth < MAX_CLAUDE_DEPTH; depth++) {
55686
- const commandLine = read(current);
55687
- if (commandLine === undefined)
55688
- return;
55689
- if (basename2(commandLine.argv[0] ?? "") === "claude")
55690
- return commandLine;
55691
- current = parent(current);
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 pluginCacheSegments(bundlePath) {
55696
- const parts = bundlePath.split(sep);
55697
- for (let index = 0;index + 1 < parts.length; index++) {
55698
- if (parts[index] === "plugins" && parts[index + 1] === "cache")
55699
- return parts.slice(index + 2);
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 installedMarketplace(bundlePath) {
55704
- return pluginCacheSegments(bundlePath)?.[0];
55710
+ function roleOf(message) {
55711
+ if (message.role !== undefined)
55712
+ return message.role;
55713
+ return message.type === "assistant" ? "assistant" : "user";
55705
55714
  }
55706
- function installedPluginVersion(bundlePath) {
55707
- return pluginCacheSegments(bundlePath)?.[2];
55715
+ var PANE_SEEDED_COMMAND = "/rename";
55716
+ function isPaneSeededRename(line) {
55717
+ return line === PANE_SEEDED_COMMAND || line.startsWith(`${PANE_SEEDED_COMMAND} `);
55708
55718
  }
55709
- var INLINE_MARKETPLACE = "inline";
55710
- function loadedMarketplace(pluginRoot, bundlePath, commandLine) {
55711
- if (commandLine !== undefined) {
55712
- const { argv, cwd } = commandLine;
55713
- const root = resolve3(pluginRoot);
55714
- for (let index = 1;index < argv.length; index++) {
55715
- const argument = argv[index] ?? "";
55716
- if (argument === "--")
55717
- break;
55718
- const equals = argument.indexOf("=");
55719
- const flag = equals === -1 ? argument : argument.slice(0, equals);
55720
- if (flag === "--plugin-dir" || flag === "--plugin-dir-no-mcp") {
55721
- const value = equals === -1 ? argv[index + 1] : argument.slice(equals + 1);
55722
- if (value !== undefined && resolve3(cwd, value) === root)
55723
- return INLINE_MARKETPLACE;
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
- return installedMarketplace(bundlePath);
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
- var AllowedChannelEntry = union([
55732
- object({ marketplace: string2(), plugin: string2() }),
55733
- string2().regex(/^[^@]+@[^@]+$/).transform((value) => {
55734
- const at = value.indexOf("@");
55735
- return { plugin: value.slice(0, at), marketplace: value.slice(at + 1) };
55736
- })
55737
- ]);
55738
- function allowedEntries(value) {
55739
- if (!Array.isArray(value))
55740
- return;
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 managedSettingsDirectory() {
55747
- switch (process.platform) {
55748
- case "darwin":
55749
- return "/Library/Application Support/ClaudeCode";
55750
- case "win32":
55751
- return "C:\\Program Files\\ClaudeCode";
55752
- default:
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
- var COMPOSITION_KEYS = new Set([
55757
- "managedSourcesBehavior",
55758
- "wslInheritsWindowsSettings"
55759
- ]);
55760
- function readChannelSettings(file, asSource = false) {
55761
- try {
55762
- const value = JSON.parse(readFileSync6(file, "utf8"));
55763
- if (typeof value !== "object" || value === null || Array.isArray(value))
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
- if (asSource && Object.keys(value).every((key) => COMPOSITION_KEYS.has(key)))
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
- const record = value;
55768
- const behavior = record["managedSourcesBehavior"];
55769
- return {
55770
- list: allowedEntries(record["allowedChannelPlugins"]),
55771
- behavior: typeof behavior === "string" ? behavior : undefined
55772
- };
55773
- } catch {
55774
- return;
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
- function channelAllowlist(directories) {
55778
- const remote = readChannelSettings(join6(directories.configDirectory, "remote-settings.json"), true);
55779
- const dropIns = join6(directories.managedDirectory, "managed-settings.d");
55780
- let dropInFiles = [];
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
- dropInFiles = readdirSync3(dropIns).filter((name) => name.endsWith(".json") && !name.startsWith(".")).sort().map((name) => join6(dropIns, name));
56102
+ parsed = JSON.parse(raw);
55783
56103
  } catch {
55784
- dropInFiles = [];
55785
- }
55786
- const files = [join6(directories.managedDirectory, "managed-settings.json"), ...dropInFiles].map((file) => readChannelSettings(file)).filter((settings) => settings !== undefined);
55787
- const lists = files.map((settings) => settings.list).filter((list) => list !== undefined);
55788
- const fileList = lists.length === 0 ? undefined : lists.flat();
55789
- const behavior = remote?.behavior ?? files.find((settings) => settings.behavior)?.behavior;
55790
- if (remote === undefined)
55791
- return fileList;
55792
- return behavior === "merge" ? remote.list ?? fileList : remote.list;
55793
- }
55794
- function detectClaudeChannel(input) {
55795
- const override = input.environment["ENVOY_CHANNEL_LOADED"]?.trim();
55796
- if (override === "1")
55797
- return { loaded: true };
55798
- if (override === "0")
55799
- return { loaded: false, why: "ENVOY_CHANNEL_LOADED=0" };
55800
- const configDirectory = input.environment["CLAUDE_CONFIG_DIR"]?.trim() || join6(homedir3(), ".claude");
55801
- const commandLine = claudeCommandLine(input.claudePid);
55802
- const pluginRoot = input.environment["CLAUDE_PLUGIN_ROOT"]?.trim();
55803
- const plugin = pluginRoot ? {
55804
- name: "claude-envoy",
55805
- marketplace: loadedMarketplace(pluginRoot, input.bundlePath, commandLine?.exact && commandLine.cwd !== undefined ? { argv: commandLine.argv, cwd: commandLine.cwd } : undefined)
55806
- } : undefined;
55807
- return claudeChannelGate({
55808
- commandLine,
55809
- plugin,
55810
- allowlist: channelAllowlist({ managedDirectory: managedSettingsDirectory(), configDirectory })
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 basename3, join as join8 } from "path";
56233
+ import { basename as basename2, join as join7 } from "path";
55834
56234
 
55835
56235
  // src/session-state.ts
55836
- import { readFileSync as readFileSync7 } from "fs";
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 join7 } from "path";
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 = join7(directory, entry);
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 join7(stateDirectory, TITLES_DIRECTORY, `${encodeURIComponent(sessionId)}.txt`);
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 readFileSync7(file, "utf8");
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(join7(stateDirectory, TITLES_DIRECTORY), writtenLongAgo);
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 join8(stateDirectory, "sessions", String(claudePid));
56324
+ return join7(stateDirectory, "sessions", String(claudePid));
55925
56325
  }
55926
56326
  function sessionHandoffFile(stateDirectory, claudePid) {
55927
- return join8(sessionHandoffDirectory(stateDirectory, claudePid), "session-id");
56327
+ return join7(sessionHandoffDirectory(stateDirectory, claudePid), "session-id");
55928
56328
  }
55929
56329
  var ROLES_DIRECTORY = "roles";
55930
56330
  function roleStateFile(stateDirectory, sessionId) {
55931
- return join8(stateDirectory, ROLES_DIRECTORY, `${encodeURIComponent(sessionId)}.json`);
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(join8(stateDirectory, "sessions"), (file) => {
55983
- const pid = basename3(file);
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(join8(stateDirectory, ROLES_DIRECTORY), async (file) => file !== own && await writtenLongAgo(file));
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 join9 } from "path";
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 = join9(configDir, "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 = join9(projects, directory, name);
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.params.meta["event_id"] ?? randomUUID2();
56701
+ const id = notification.eventId ?? randomUUID2();
56279
56702
  const settle = Promise.withResolvers();
56280
56703
  waiting.push({ id, settle });
56281
- connected().write(`${JSON.stringify({ id, content: notification.params.content })}
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 readFileSync8, renameSync as renameSync3, rmSync as rmSync5, writeFileSync as writeFileSync4 } from "fs";
56291
- import { dirname as dirname3, join as join10 } from "path";
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 join10(configDir, `legion-phase-stall-${sessionID}.json`);
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 = readFileSync8(file, "utf8");
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 = join10(dirname3(file), `.legion-phase-stall-${randomUUID4()}`);
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
- if (args.verdict === "pending" && args.wake_at === undefined && !check.last) {
57208
- throw new Error(`handoff_complete's pending report names wake_at, when the next look of ${claim.issue}'s production check is due`);
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
- if (args.wake_at !== undefined && (args.verdict !== "pending" || check.last)) {
57211
- 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`;
57212
- throw new Error(`handoff_complete names wake_at and no look follows it: ${why}; send it again without a time`);
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
- import { join as join11 } from "path";
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 { ledger: [] };
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 snapshot = ReplayStateFileSchema.safeParse(parsed);
57291
- if (!snapshot.success)
57292
- throw new Error(`${file} holds no replay state`);
57293
- return snapshot.data;
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 basename4 } from "path";
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 readFileSync9 } from "fs";
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 = readFileSync9(`/proc/${pid}/stat`, "utf8");
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(basename4(directory) || directory);
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 CHANNEL_NOTIFICATION_METHOD = "notifications/claude/channel";
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 encoder4 = new TextEncoder;
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
- var CHANNEL_CAPABILITIES = ["aside"];
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 queue = (notification) => {
57547
- const queued = tail.then(() => input.notifier.notification(notification));
57548
- tail = queued.catch((error) => {
57549
- process.stderr.write(`envoy-channel: channel notification failed \u2014 ${messageFor(error)}
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 queued;
58972
+ return run;
57553
58973
  };
57554
- const show = (rendered, subject, content) => {
58974
+ const write = (notification) => queued(() => input.notifier.notification(notification));
58975
+ const list = (rendered) => {
57555
58976
  const envelope = rendered.envelope;
57556
- if (envelope !== undefined) {
57557
- inbox.unshift({
57558
- event_id: envelope.event_id ?? "unknown",
57559
- at: inboundTimestamp(envelope.issued_at),
57560
- from: envelope.source_session ?? envelope.source,
57561
- summary: envelope.payload_summary ?? "unknown"
57562
- });
57563
- if (inbox.length > CHANNEL_INBOX_LIMIT)
57564
- inbox.pop();
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"}: a BTW never starts a turn
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 show(rendered, subject, rendered.content).then(() => true);
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, frame.subject, replayedText(rendered.content, frame.time));
59047
+ await show(rendered, replayedText(rendered.content, frame.time));
57623
59048
  return "shown";
57624
59049
  },
57625
59050
  notice(content) {
57626
- return queue({
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, encoder4.encode(body)),
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: {}, experimental: { "claude/channel": {} } },
58513
- instructions: channel.loaded ? CHANNEL_INSTRUCTIONS : `${CHANNEL_INSTRUCTIONS} Claude Code did not load this channel for this session (${channel.why}), so no Envoy event or Dispatch message reaches you as a <channel> message, and the session advertises no delivery mode; envoy_inbox still lists what arrived.`
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
- const notifier = inbox === undefined ? server : phaseHook === undefined ? inbox : {
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.params.content);
58571
- await inbox.notification(notification);
60043
+ phaseHook.deliveryWritten(notification.content);
60044
+ await writer.notification(notification);
58572
60045
  }
58573
60046
  };
58574
- const capabilities = inboxSocket !== undefined ? INBOX_CAPABILITIES : channel.loaded ? CHANNEL_CAPABILITIES : [];
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;