@sjawhar/claude-envoy 1.18.0 → 1.19.0

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