@sjawhar/pi-legion-envoy 1.49.0 → 1.49.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/envoy.js CHANGED
@@ -29649,6 +29649,7 @@ function date4(params) {
29649
29649
  // ../../node_modules/.bun/zod@4.3.6/node_modules/zod/v4/classic/external.js
29650
29650
  config(en_default());
29651
29651
  // ../contracts/src/dispatch-api.ts
29652
+ var ASK_TURNS = ["human", "agent"];
29652
29653
  var DELIVERY_CAPABILITIES = ["aside", "btw", "steer"];
29653
29654
  var DispatchEventSchema = object({
29654
29655
  issue_key: string2().nullable(),
@@ -29729,8 +29730,8 @@ var CommentEventPayloadSchema = object({
29729
29730
  ask_id: string2().nullish(),
29730
29731
  ask_question: string2().optional(),
29731
29732
  ask_state: _enum2(["open", "answered", "resolved"]).optional(),
29732
- ask_waiting_on: _enum2(["human", "agent"]).optional(),
29733
- turn: _enum2(["human", "agent"]).nullish(),
29733
+ ask_waiting_on: _enum2(ASK_TURNS).optional(),
29734
+ turn: _enum2(ASK_TURNS).nullish(),
29734
29735
  anchor: object({ block_id: string2().nullable().optional(), quote: string2().optional() }).nullish(),
29735
29736
  suggestion: object({ replace_with: string2().optional() }).nullish(),
29736
29737
  author: object({ kind: string2(), id: string2() }).optional(),
@@ -29853,16 +29854,14 @@ function documentOwnerValidation(requireArtifact, alwaysRequireArtifact = false)
29853
29854
  message: alwaysRequireArtifact ? "Exactly one of issue and project is required; artifact or ref must name the document." : "Exactly one of issue and project is required; with project, artifact names the document."
29854
29855
  };
29855
29856
  }
29856
- var commentValidation = (() => {
29857
- const owner = documentOwnerValidation(true);
29858
- return {
29859
- check: (value) => {
29860
- const input = value;
29861
- return owner.check(value) && (input.turn === undefined || typeof input.reply_to_ask === "string");
29862
- },
29863
- message: `${owner.message} turn requires reply_to_ask.`
29864
- };
29865
- })();
29857
+ var commentOwner = documentOwnerValidation(true);
29858
+ var commentValidation = {
29859
+ check: (value) => {
29860
+ const input = value;
29861
+ return commentOwner.check(value) && (input.turn === undefined || typeof input.reply_to_ask === "string");
29862
+ },
29863
+ message: `${commentOwner.message} turn requires reply_to_ask.`
29864
+ };
29866
29865
  var ISSUE_COMPONENTS_MODES = ["inherit", "explicit", "none"];
29867
29866
  function componentsArgument(z) {
29868
29867
  return z.object({
@@ -29884,6 +29883,7 @@ var SPEC_SECTIONS = [
29884
29883
  ];
29885
29884
  var SPEC_WRITING_GUIDANCE = `When writing a spec, use these sections in order: ${SPEC_SECTIONS.join(", ")}. ` + "Write for a reader who has not seen the code: plain sentences, every identifier expanded on " + "first use, no coined shorthand; see skills/dispatch Writing for the human and Writing a spec.";
29886
29885
  var ASK_URGENCIES = ["low", "med", "high", "blocking"];
29886
+ var ASK_QUESTION_MAX = 800;
29887
29887
  var ISSUE_STATUSES = [
29888
29888
  "triage",
29889
29889
  "icebox",
@@ -29895,6 +29895,9 @@ var ISSUE_STATUSES = [
29895
29895
  "retro",
29896
29896
  "done"
29897
29897
  ];
29898
+ function isIssueStatus(value) {
29899
+ return ISSUE_STATUSES.includes(value);
29900
+ }
29898
29901
  var DOC_EDIT_OPS = ["replace", "delete", "insert", "retype", "move"];
29899
29902
  var dispatchToolSpecs = [
29900
29903
  {
@@ -29937,13 +29940,13 @@ var dispatchToolSpecs = [
29937
29940
  },
29938
29941
  {
29939
29942
  name: "dispatch_ask",
29940
- description: "Open a durable, answerable decision on an issue or project document. Do not use it for a status update or discussion; " + "use dispatch_message instead. A to-do a human must complete is a question phrased as that to-do, with the options you want (for example Done / Can't). " + "Anchor a document question, thread reply_to/reply_to_ask, or cite a dispatch:// " + 'reference \u2014 it must be answerable from its own text and anchor alone, never "see above". A quote anchor is pinned to its block. Question is at most 800 ' + `characters and has at most 8 options. ${OWNER_REFERENCE}`,
29943
+ description: "Open a durable, answerable decision on an issue or project document. Do not use it for a status update or discussion; " + "use dispatch_message instead. A to-do a human must complete is a question phrased as that to-do, with the options you want (for example Done / Can't). " + "Anchor a document question, thread reply_to/reply_to_ask, or cite a dispatch:// " + `reference \u2014 it must be answerable from its own text and anchor alone, never "see above". A quote anchor is pinned to its block. Question is at most ${ASK_QUESTION_MAX} ` + `characters and has at most 8 options. ${OWNER_REFERENCE}`,
29941
29944
  arguments: (z) => ({
29942
29945
  issue: z.string().describe(ISSUE_REFERENCE).optional(),
29943
29946
  project: z.string().describe("Project key owning the document.").optional(),
29944
29947
  artifact: z.string().describe("Project document artifact id, slug, or filename.").optional(),
29945
29948
  ref: z.string().describe("Optional dispatch:// reference (issue, document, message, or ask); appended to the question and rendered as a link.").optional(),
29946
- question: z.string({ max: 800 }).describe("Decision question, at most 800 characters."),
29949
+ question: z.string({ max: ASK_QUESTION_MAX }).describe(`Decision question, at most ${ASK_QUESTION_MAX} characters.`),
29947
29950
  options: z.array(z.object({
29948
29951
  label: z.string().describe("Selectable option label."),
29949
29952
  description: z.string().describe("Optional option context.").optional()
@@ -29963,7 +29966,7 @@ var dispatchToolSpecs = [
29963
29966
  description: "Edit an open question in place. Use it to correct or refine the same decision; retract the " + "old ask and open a new one when the decision itself changes. Previous text remains in the " + "event log. Only the asking session can edit it; answered or resolved asks cannot be edited.",
29964
29967
  arguments: (z) => ({
29965
29968
  ask: z.string().describe("Ask id to edit."),
29966
- question: z.string({ max: 800 }).describe("Replacement decision question, at most 800 characters.").optional(),
29969
+ question: z.string({ max: ASK_QUESTION_MAX }).describe(`Replacement decision question, at most ${ASK_QUESTION_MAX} characters.`).optional(),
29967
29970
  options: z.array(z.object({
29968
29971
  label: z.string().describe("Selectable option label."),
29969
29972
  description: z.string().describe("Optional option context.").optional()
@@ -31194,9 +31197,9 @@ function resolveOptions(options) {
31194
31197
 
31195
31198
  // ../envoy-client/src/ask-answer.ts
31196
31199
  var HEAD_LENGTH = 120;
31197
- function textHead(text, limit = HEAD_LENGTH) {
31200
+ function textHead(text) {
31198
31201
  const flat = text.replace(/\s+/g, " ").trim();
31199
- return flat.length > limit ? `${flat.slice(0, limit)}\u2026` : flat;
31202
+ return flat.length > HEAD_LENGTH ? `${flat.slice(0, HEAD_LENGTH)}\u2026` : flat;
31200
31203
  }
31201
31204
  function askAnswerText(answer) {
31202
31205
  if (answer === null || answer === undefined)
@@ -31210,6 +31213,29 @@ function askAnswerText(answer) {
31210
31213
  return `${selected} - ${text}`;
31211
31214
  }
31212
31215
 
31216
+ // ../envoy-client/src/dispatch-owner.ts
31217
+ function documentLabel(project, slug) {
31218
+ return `${project}/${slug}`;
31219
+ }
31220
+ function issueTopic(key) {
31221
+ return { label: key, topic: dispatchIssueSubject(key, ">") };
31222
+ }
31223
+ function documentTopicOf(project, slug) {
31224
+ return {
31225
+ label: documentLabel(project, slug),
31226
+ topic: dispatchDocumentSubject(project, slug, ">")
31227
+ };
31228
+ }
31229
+ function dispatchIssueRef(key) {
31230
+ return `dispatch://${key}`;
31231
+ }
31232
+ function dispatchDocumentRef(owner, slug) {
31233
+ return `dispatch://${owner}/artifact/${slug}`;
31234
+ }
31235
+ function dispatchChildRef(ownerRef, kind, id) {
31236
+ return `${ownerRef}/${kind}/${id}`;
31237
+ }
31238
+
31213
31239
  // ../envoy-client/src/delivery.ts
31214
31240
  var KNOWN_SOURCES = EnvelopeSchema.shape.source.enum;
31215
31241
  var FOREIGN_SESSION_ID = /\b01a0[0-9a-f]{4}-[0-9a-f]{4}-7[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}\b/g;
@@ -31247,6 +31273,31 @@ var TolerantInboundEnvelopeSchema = object({
31247
31273
  source: InboundEnvelopeSchema.shape.source.optional().catch(undefined),
31248
31274
  sender: object(tolerantShape(InboundSenderSchema.shape)).optional().catch(undefined)
31249
31275
  }).passthrough();
31276
+ async function postDeliveryReply(config, sessionId, delivery, result) {
31277
+ const response = await fetch(`${config.url}/api/v1/messages/${delivery.messageID}/reply`, {
31278
+ method: "POST",
31279
+ headers: { Authorization: `Bearer ${config.token}`, "Content-Type": "application/json" },
31280
+ body: JSON.stringify({
31281
+ actor: { kind: "session", id: sessionId },
31282
+ attempt: delivery.attempt,
31283
+ ...result
31284
+ })
31285
+ });
31286
+ if (!response.ok) {
31287
+ throw new Error(`Dispatch reply failed: ${response.status} ${await response.text()}`);
31288
+ }
31289
+ }
31290
+ function expectsLaneReceipt(frame) {
31291
+ return frame.reply !== undefined && frame.reply !== "" && frame.subject === frame.directSubject && frame.envelopeTopic !== undefined && frame.envelopeTopic !== frame.directSubject;
31292
+ }
31293
+ function rememberBounded(seen, key, limit) {
31294
+ seen.add(key);
31295
+ if (seen.size > limit) {
31296
+ const oldest = seen.values().next();
31297
+ if (!oldest.done)
31298
+ seen.delete(oldest.value);
31299
+ }
31300
+ }
31250
31301
  var DispatchDeliveryRequestSchema = object({
31251
31302
  attempt: number2().int().positive(),
31252
31303
  mode: _enum2(DELIVERY_CAPABILITIES)
@@ -31313,23 +31364,20 @@ function dispatchOwner(event, topic) {
31313
31364
  return `${document.project} / ${document.slug}`;
31314
31365
  return event.artifact_id ?? "unknown";
31315
31366
  }
31316
- function dispatchCommentReplyWith(event, topic) {
31317
- if (event.type !== "comment.created")
31318
- return;
31319
- const comment = CommentEventPayloadSchema.safeParse(event.payload);
31320
- if (!comment.success || comment.data.id === undefined)
31367
+ function dispatchCommentReplyWith(event, topic, comment) {
31368
+ if (comment === undefined || comment.id === undefined)
31321
31369
  return;
31322
- const reply = comment.data.ask_id;
31370
+ const reply = comment.ask_id;
31323
31371
  const thread = reply === undefined || reply === null || reply === "" ? "reply_to" : "reply_to_ask";
31324
- const threadID = thread === "reply_to" ? comment.data.id : String(reply);
31372
+ const threadID = thread === "reply_to" ? comment.id : String(reply);
31325
31373
  if (event.issue_key !== null) {
31326
31374
  return {
31327
31375
  tool: "dispatch_comment",
31328
31376
  args: { issue: event.issue_key, [thread]: threadID, body: "..." }
31329
31377
  };
31330
31378
  }
31331
- let project = comment.data.project_key;
31332
- let artifact = comment.data.artifact_slug;
31379
+ let project = comment.project_key;
31380
+ let artifact = comment.artifact_slug;
31333
31381
  if (project === undefined || project === "" || artifact === undefined || artifact === "") {
31334
31382
  [project, artifact] = dispatchOwner(event, topic).split(" / ", 2);
31335
31383
  }
@@ -31358,13 +31406,10 @@ function dispatchPayload(event) {
31358
31406
  const parsed = schema.safeParse(event.payload);
31359
31407
  return parsed.success ? parsed.data : event.payload;
31360
31408
  }
31361
- function dispatchAskQuestion(event) {
31362
- if (event.type !== "comment.created")
31363
- return;
31364
- const parsed = CommentEventPayloadSchema.safeParse(event.payload);
31365
- return parsed.success && parsed.data.ask_question !== undefined && parsed.data.ask_question !== "" ? textHead(parsed.data.ask_question) : undefined;
31409
+ function dispatchAskQuestion(comment) {
31410
+ return comment?.ask_question !== undefined && comment.ask_question !== "" ? textHead(comment.ask_question) : undefined;
31366
31411
  }
31367
- function dispatchReplyRef(event, topic, inReplyTo) {
31412
+ function dispatchReplyRef(event, topic, inReplyTo, comment) {
31368
31413
  let kind;
31369
31414
  let document = topicDocument(topic);
31370
31415
  if (event.type.startsWith("ask.")) {
@@ -31372,23 +31417,23 @@ function dispatchReplyRef(event, topic, inReplyTo) {
31372
31417
  } else if (event.type.startsWith("message.")) {
31373
31418
  kind = "message";
31374
31419
  } else if (event.type === "comment.created") {
31375
- const comment = CommentEventPayloadSchema.safeParse(event.payload);
31376
- if (!comment.success || comment.data.ask_id === undefined || comment.data.ask_id === null) {
31420
+ if (comment === undefined || comment.ask_id === undefined || comment.ask_id === null) {
31377
31421
  return inReplyTo;
31378
31422
  }
31379
31423
  kind = "ask";
31380
- const { project_key: project, artifact_slug: slug } = comment.data;
31424
+ const { project_key: project, artifact_slug: slug } = comment;
31381
31425
  if (project !== undefined && project !== "" && slug !== undefined && slug !== "") {
31382
31426
  document = { project, slug };
31383
31427
  }
31384
31428
  } else {
31385
31429
  return inReplyTo;
31386
31430
  }
31387
- if (event.issue_key !== null)
31388
- return `dispatch://${event.issue_key}/${kind}/${inReplyTo}`;
31431
+ if (event.issue_key !== null) {
31432
+ return dispatchChildRef(dispatchIssueRef(event.issue_key), kind, inReplyTo);
31433
+ }
31389
31434
  if (document === undefined)
31390
31435
  return inReplyTo;
31391
- return `dispatch://${document.project}/artifact/${document.slug}/${kind}/${inReplyTo}`;
31436
+ return dispatchChildRef(dispatchDocumentRef(document.project, document.slug), kind, inReplyTo);
31392
31437
  }
31393
31438
  function isObject2(value) {
31394
31439
  return typeof value === "object" && value !== null;
@@ -31400,16 +31445,16 @@ function parseDispatchFrame(rawPayload) {
31400
31445
  } catch {
31401
31446
  return { raw: rawPayload };
31402
31447
  }
31403
- const targeted = DispatchTargetedFrameSchema.safeParse(value);
31404
- if (targeted.success) {
31405
- const wireFrame = value;
31406
- return {
31407
- event: targeted.data.event,
31408
- notify: wireFrame.event.notify,
31409
- delivery: targeted.data.delivery
31410
- };
31411
- }
31412
31448
  if (isObject2(value) && "delivery" in value) {
31449
+ const targeted = DispatchTargetedFrameSchema.safeParse(value);
31450
+ if (targeted.success) {
31451
+ const wireFrame = value;
31452
+ return {
31453
+ event: targeted.data.event,
31454
+ notify: wireFrame.event.notify,
31455
+ delivery: targeted.data.delivery
31456
+ };
31457
+ }
31413
31458
  const recoverable = RecoverableDispatchDeliveryFailureSchema.safeParse(value);
31414
31459
  if (recoverable.success) {
31415
31460
  return {
@@ -31481,12 +31526,13 @@ function renderInbound(raw, sessionID, subject) {
31481
31526
  let askAuthor;
31482
31527
  const dispatchRendered = envelope.source === "dispatch" && envelope.payload !== undefined;
31483
31528
  if (envelope.source === "dispatch") {
31529
+ const topic = subject ?? envelope.topic;
31484
31530
  if (envelope.payload === undefined) {
31485
31531
  dispatchIssue = "payload";
31486
31532
  } else {
31487
31533
  const frame = parseDispatchFrame(envelope.payload);
31488
31534
  if ("event" in frame) {
31489
- if (frame.notify === false && (subject ?? envelope.topic)?.startsWith(DISPATCH_TOPIC_PREFIX) === true) {
31535
+ if (frame.notify === false && topic?.startsWith(DISPATCH_TOPIC_PREFIX) === true) {
31490
31536
  return { skip: true, content: "", envelope };
31491
31537
  }
31492
31538
  if (frame.event.actor.kind === "session" && frame.event.actor.id === sessionID) {
@@ -31500,7 +31546,7 @@ function renderInbound(raw, sessionID, subject) {
31500
31546
  const who = removed.data.by?.id ?? "someone";
31501
31547
  return {
31502
31548
  skip: false,
31503
- content: `Unsubscribed from ${dispatchOwner(frame.event, subject ?? envelope.topic)} by ${who}`,
31549
+ content: `Unsubscribed from ${dispatchOwner(frame.event, topic)} by ${who}`,
31504
31550
  envelope
31505
31551
  };
31506
31552
  }
@@ -31510,7 +31556,7 @@ function renderInbound(raw, sessionID, subject) {
31510
31556
  return { skip: true, content: "", envelope };
31511
31557
  }
31512
31558
  const who = follower.data.by?.id ?? "someone";
31513
- const owner = dispatchOwner(frame.event, subject ?? envelope.topic);
31559
+ const owner = dispatchOwner(frame.event, topic);
31514
31560
  const ask = follower.data.ask_id ?? "?";
31515
31561
  return {
31516
31562
  skip: false,
@@ -31518,8 +31564,10 @@ function renderInbound(raw, sessionID, subject) {
31518
31564
  envelope
31519
31565
  };
31520
31566
  }
31567
+ const comment = frame.event.type === "comment.created" ? CommentEventPayloadSchema.safeParse(frame.event.payload) : undefined;
31568
+ const commentPayload = comment?.success === true ? comment.data : undefined;
31521
31569
  const answered = dispatchAskAnswer(frame.event);
31522
- const question = answered?.question ?? dispatchAskQuestion(frame.event);
31570
+ const question = answered?.question ?? dispatchAskQuestion(commentPayload);
31523
31571
  if (frame.event.type.startsWith("ask.")) {
31524
31572
  const asked = AskAuthorPayloadSchema.safeParse(frame.event.payload);
31525
31573
  if (asked.success && asked.data.author.kind === "session") {
@@ -31527,9 +31575,9 @@ function renderInbound(raw, sessionID, subject) {
31527
31575
  }
31528
31576
  }
31529
31577
  if (inReplyTo !== undefined) {
31530
- inReplyTo = dispatchReplyRef(frame.event, subject ?? envelope.topic, inReplyTo);
31578
+ inReplyTo = dispatchReplyRef(frame.event, topic, inReplyTo, commentPayload);
31531
31579
  }
31532
- dispatchReply = dispatchCommentReplyWith(frame.event, subject ?? envelope.topic);
31580
+ dispatchReply = dispatchCommentReplyWith(frame.event, topic, commentPayload);
31533
31581
  if (frame.event.type === "message.created") {
31534
31582
  const message = MessageEventPayloadSchema.safeParse(frame.event.payload);
31535
31583
  const requested = DispatchDeliveryRequestSchema.safeParse(frame.delivery);
@@ -31550,11 +31598,9 @@ function renderInbound(raw, sessionID, subject) {
31550
31598
  }
31551
31599
  }
31552
31600
  dispatchEvent = {
31553
- owner: dispatchOwner(frame.event, subject ?? envelope.topic),
31601
+ owner: dispatchOwner(frame.event, topic),
31554
31602
  ...frame.event.issue_key === null ? {
31555
- ...(subject ?? envelope.topic)?.startsWith(DISPATCH_DOCUMENT_TOPIC_PREFIX) === true ? {
31556
- document: dispatchOwner(frame.event, subject ?? envelope.topic).replace(" / ", "/")
31557
- } : {},
31603
+ ...topic?.startsWith(DISPATCH_DOCUMENT_TOPIC_PREFIX) === true ? { document: dispatchOwner(frame.event, topic).replace(" / ", "/") } : {},
31558
31604
  ...frame.event.artifact_id === undefined || frame.event.artifact_id === null ? {} : { artifact_id: frame.event.artifact_id }
31559
31605
  } : { issue_key: frame.event.issue_key },
31560
31606
  type: frame.event.type,
@@ -31582,14 +31628,16 @@ function renderInbound(raw, sessionID, subject) {
31582
31628
  const payloadSummary = envelope.payload_summary ?? "unknown";
31583
31629
  const message = !dispatchRendered && envelope.payload !== undefined && envelope.payload !== envelope.payload_summary ? parsedPayload : undefined;
31584
31630
  const summaryIsHead = typeof message === "string" && envelope.payload_summary !== undefined && message.startsWith(envelope.payload_summary.endsWith("\u2026") ? envelope.payload_summary.slice(0, -1) : envelope.payload_summary);
31585
- const body = `${envelope.payload_summary ?? ""}
31586
- ${envelope.payload ?? ""}`;
31587
31631
  let foreignSession;
31588
- for (const match of body.matchAll(FOREIGN_SESSION_ID)) {
31589
- if (match[0] !== envelope.source_session && match[0] !== sessionID && match[0] !== askAuthor) {
31590
- foreignSession = match[0];
31591
- break;
31632
+ for (const part of [envelope.payload_summary ?? "", envelope.payload ?? ""]) {
31633
+ for (const match of part.matchAll(FOREIGN_SESSION_ID)) {
31634
+ if (match[0] !== envelope.source_session && match[0] !== sessionID && match[0] !== askAuthor) {
31635
+ foreignSession = match[0];
31636
+ break;
31637
+ }
31592
31638
  }
31639
+ if (foreignSession !== undefined)
31640
+ break;
31593
31641
  }
31594
31642
  const role = envelope.sender?.roles?.[0];
31595
31643
  const reply = dispatchReply ?? replyWith(envelope);
@@ -32300,14 +32348,8 @@ function formatZodIssues(issues, schema) {
32300
32348
  }
32301
32349
 
32302
32350
  // ../envoy-client/src/dispatch-execute.ts
32303
- function issueTopic(key) {
32304
- return { label: key, topic: dispatchIssueSubject(key, ">") };
32305
- }
32306
32351
  function documentTopic(artifact) {
32307
- return {
32308
- label: `${artifact.project}/${artifact.slug}`,
32309
- topic: dispatchDocumentSubject(artifact.project, artifact.slug, ">")
32310
- };
32352
+ return documentTopicOf(artifact.project, artifact.slug);
32311
32353
  }
32312
32354
  function resolvedTopic(resolved) {
32313
32355
  if (resolved.owner.kind === "project")
@@ -32326,7 +32368,7 @@ function documentResultDetails(artifact) {
32326
32368
  return {
32327
32369
  project: artifact.project,
32328
32370
  artifact: artifact.id,
32329
- document: `${artifact.project}/${artifact.slug}`
32371
+ document: documentLabel(artifact.project, artifact.slug)
32330
32372
  };
32331
32373
  }
32332
32374
  function writeResultDetails(resolved, fields) {
@@ -32337,15 +32379,15 @@ function writeResultDetails(resolved, fields) {
32337
32379
  throw new Error("issue document is missing its issue");
32338
32380
  return { issue: resolved.issue.key, ...fields };
32339
32381
  }
32340
- async function askOwnerDetails(client, ask, resolved) {
32382
+ async function askOwnerDetails(client, ask, artifact) {
32341
32383
  if (ask.issue_key !== null) {
32342
32384
  return { issue: ask.issue_key, ask: ask.id };
32343
32385
  }
32344
32386
  if (ask.artifact_id === undefined || ask.artifact_id === null) {
32345
32387
  throw new Error("document ask is missing its artifact ID");
32346
32388
  }
32347
- const artifact = resolved?.artifact ?? await client.getArtifact(ask.artifact_id);
32348
- return { ...documentResultDetails(artifact), ask: ask.id };
32389
+ const owner = artifact ?? await client.getArtifact(ask.artifact_id);
32390
+ return { ...documentResultDetails(owner), ask: ask.id };
32349
32391
  }
32350
32392
  async function commentOwnerResult(client, comment, artifact) {
32351
32393
  if (comment.issue_key !== null) {
@@ -32356,12 +32398,12 @@ async function commentOwnerResult(client, comment, artifact) {
32356
32398
  }
32357
32399
  const owner = artifact ?? await client.getArtifact(comment.artifact_id);
32358
32400
  return {
32359
- label: documentTopic(owner).label,
32401
+ label: documentLabel(owner.project, owner.slug),
32360
32402
  details: { ...documentResultDetails(owner), comment: comment.id }
32361
32403
  };
32362
32404
  }
32363
- async function followedAskDetails(client, ask, resolved) {
32364
- return { ...await askOwnerDetails(client, ask, resolved), follows: { ask: ask.id } };
32405
+ async function followedAskDetails(client, ask, artifact) {
32406
+ return { ...await askOwnerDetails(client, ask, artifact), follows: { ask: ask.id } };
32365
32407
  }
32366
32408
  var nativeIssueKeyPattern = /^[A-Z][A-Z0-9]{1,9}-[0-9]+$/;
32367
32409
  var externalIssueRefPattern = /^([^/\s]+)\/([^/\s#]+)#([1-9][0-9]*)$/;
@@ -32436,7 +32478,7 @@ function searchResultLine(result, baseUrl) {
32436
32478
  const href = new URL(result.href, baseUrl).toString();
32437
32479
  const { owner } = result;
32438
32480
  if (owner.kind === "document") {
32439
- const reference = `dispatch://${owner.project}/artifact/${owner.slug}`;
32481
+ const reference = dispatchDocumentRef(owner.project, owner.slug);
32440
32482
  return `${reference} [document] ${owner.name} - ${result.kind}: ${snippetText(result.snippet)} -> ${href}`;
32441
32483
  }
32442
32484
  const artifactName = result.artifact ? ` ${result.artifact.name}` : "";
@@ -32447,7 +32489,6 @@ function askUrgency(args) {
32447
32489
  const value = args.urgency;
32448
32490
  return ASK_URGENCIES.find((urgency) => urgency === value);
32449
32491
  }
32450
- var maxAskQuestion16 = 800;
32451
32492
  function questionWithRef(question, ref) {
32452
32493
  return ref === undefined || question.includes(ref) ? question : `${question}
32453
32494
 
@@ -32457,7 +32498,7 @@ function askQuestionWithRef(args) {
32457
32498
  return questionWithRef(stringArg(args, "question"), optionalString(args, "ref"));
32458
32499
  }
32459
32500
  function askQuestionProblem(withRef) {
32460
- return withRef.length > maxAskQuestion16 ? `question plus ref ${overCapMessage(withRef.length, maxAskQuestion16)}; shorten the question or drop the ref` : undefined;
32501
+ return withRef.length > ASK_QUESTION_MAX ? `question plus ref ${overCapMessage(withRef.length, ASK_QUESTION_MAX)}; shorten the question or drop the ref` : undefined;
32461
32502
  }
32462
32503
  function parseDispatchRef(ref) {
32463
32504
  const projectDocument = ref.match(/^dispatch:\/\/([A-Z][A-Z0-9]{1,9})\/artifact\/([^/@]+)(?:@v(\d+))?(?:\/(ask|comment)\/([^/]+))?$/);
@@ -32512,6 +32553,10 @@ function parseDispatchRef(ref) {
32512
32553
  return { owner, kind: "message", id: message };
32513
32554
  return { owner, kind: "issue", id: issue };
32514
32555
  }
32556
+ function refTarget(ref, kind, id) {
32557
+ const ownerRef = ref.owner.kind === "issue" ? dispatchIssueRef(ref.owner.issue) : dispatchDocumentRef(ref.owner.project, `${ref.artifact}`);
32558
+ return dispatchChildRef(ownerRef, kind, id);
32559
+ }
32515
32560
  var uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
32516
32561
  var askIdProblem = "ask must be a bare ask id or a dispatch://.../ask/<id> reference";
32517
32562
  var commentIdProblem = "comment must be a bare comment id or a dispatch://.../comment/<id> reference";
@@ -32555,7 +32600,7 @@ function argumentProblems(tool, args) {
32555
32600
  case "dispatch_ask": {
32556
32601
  const question = optionalString(args, "question");
32557
32602
  const ref = optionalString(args, "ref");
32558
- if (question !== undefined && ref !== undefined && question.length <= maxAskQuestion16) {
32603
+ if (question !== undefined && ref !== undefined && question.length <= ASK_QUESTION_MAX) {
32559
32604
  const problem = askQuestionProblem(questionWithRef(question, ref));
32560
32605
  if (problem !== undefined)
32561
32606
  problems.push(problem);
@@ -32645,11 +32690,17 @@ function dispatchRefFromUrl(value, serverUrl) {
32645
32690
  }
32646
32691
  var refGrammarProblem = "ref must be a valid dispatch:// reference such as dispatch://KEY-1, " + "dispatch://KEY-1/ask/<uuid>, dispatch://KEY-1/comment/<uuid>, " + "dispatch://KEY-1/message/<uuid>, dispatch://KEY-1/artifact/<slug>, or " + "dispatch://PROJECT/artifact/<document-ref> (an artifact id, slug, or filename); " + "a dashboard URL on this Dispatch server is accepted too";
32647
32692
  var ownerRequiredProblem = "issue is required; supply issue or set LEGION_ISSUE";
32693
+ var toolSchemas = new Map;
32648
32694
  function toolSchema(tool) {
32695
+ const cached = toolSchemas.get(tool);
32696
+ if (cached !== undefined)
32697
+ return cached;
32649
32698
  const spec = dispatchToolSpecs.find((candidate) => candidate.name === tool);
32650
32699
  if (!spec)
32651
32700
  throw new Error(`Unknown Dispatch tool: ${tool}`);
32652
- return dispatchToolSchema(spec, zodSchemaApi(exports_external), { strict: true });
32701
+ const schema = dispatchToolSchema(spec, zodSchemaApi(exports_external), { strict: true });
32702
+ toolSchemas.set(tool, schema);
32703
+ return schema;
32653
32704
  }
32654
32705
  async function resolveOwnerArguments(tool, input, cwd, env, exec, serverUrl, problems) {
32655
32706
  if (issueFreeTools[tool] === true)
@@ -32857,11 +32908,21 @@ function referenceLines(edges) {
32857
32908
  return `- ${edge.kind} ${edge.node.kind} ${edge.node.ref ?? edge.node.id} (${excerpt}${edge.created_at})`;
32858
32909
  });
32859
32910
  }
32911
+ function unavailableReason(error) {
32912
+ return error instanceof DispatchServiceError && error.status === 404 ? "unavailable" : `unavailable: ${messageFor(error)}`;
32913
+ }
32860
32914
  async function graphEdges(client, query) {
32861
32915
  try {
32862
32916
  return (await client.getReferences(query)).edges;
32863
32917
  } catch (error) {
32864
- return error instanceof DispatchServiceError && error.status === 404 ? "unavailable" : `unavailable: ${error instanceof Error ? error.message : String(error)}`;
32918
+ return unavailableReason(error);
32919
+ }
32920
+ }
32921
+ async function issueReferencesOrUnavailable(client, key) {
32922
+ try {
32923
+ return await client.getIssueReferences(key);
32924
+ } catch (error) {
32925
+ return unavailableReason(error);
32865
32926
  }
32866
32927
  }
32867
32928
  async function graphSections(client, ref) {
@@ -33071,8 +33132,9 @@ async function executeDispatchTool(input) {
33071
33132
  problems.push(...argumentProblems(input.tool, ownerArguments.args));
33072
33133
  if (problems.length > 0)
33073
33134
  throw new ToolInputError(input.tool, problems);
33135
+ const dispatchClient = () => new DispatchClient(configUrl, configToken, fetchImpl, input.signal);
33074
33136
  if (input.tool === "dispatch_open_asks") {
33075
- const client = new DispatchClient(configUrl, configToken, fetchImpl, input.signal);
33137
+ const client = dispatchClient();
33076
33138
  const project = optionalString(ownerArguments.args, "project");
33077
33139
  if (project !== undefined) {
33078
33140
  const response = await client.openAsksForProject(project);
@@ -33088,7 +33150,7 @@ async function executeDispatchTool(input) {
33088
33150
  const sessionId = input.sessionId?.trim();
33089
33151
  if (!sessionId)
33090
33152
  throw new Error("host session id is required for dispatch_whoami");
33091
- const client = new DispatchClient(configUrl, configToken, fetchImpl, input.signal);
33153
+ const client = dispatchClient();
33092
33154
  const identity = await client.whoami();
33093
33155
  const owner = identity.kind === "agent" ? identity.owner : identity.login.toLowerCase();
33094
33156
  return {
@@ -33098,7 +33160,7 @@ async function executeDispatchTool(input) {
33098
33160
  }
33099
33161
  const args = parsed.success ? parsed.data : ownerArguments.args;
33100
33162
  const actor = toolActor(await resolveOrigin(env, exec, input.cwd), input);
33101
- const client = new DispatchClient(configUrl, configToken, fetchImpl, input.signal);
33163
+ const client = dispatchClient();
33102
33164
  const owner = ownerArguments.owner?.kind === "issue" ? {
33103
33165
  kind: "issue",
33104
33166
  issue: await ensureIssue(client, ownerArguments.owner.issue, actor)
@@ -33337,7 +33399,7 @@ async function executeDispatchTool(input) {
33337
33399
  return {
33338
33400
  text: `Asked ${ask.id} on ${askOwner.label} (urgency ${ask.urgency}): ${ask.question}
33339
33401
  ${followsAsk(askOwner)}`,
33340
- details: await followedAskDetails(client, ask, resolved)
33402
+ details: await followedAskDetails(client, ask, resolved?.artifact)
33341
33403
  };
33342
33404
  }
33343
33405
  case "dispatch_edit_ask": {
@@ -33419,7 +33481,7 @@ ${followsAsk(askOwner)}`,
33419
33481
  ...inReplyTo === undefined ? {} : { in_reply_to: inReplyTo },
33420
33482
  actor
33421
33483
  });
33422
- const messageRef = `dispatch://${issueKey}/message/${message.id}`;
33484
+ const messageRef = dispatchChildRef(dispatchIssueRef(issueKey), "message", message.id);
33423
33485
  return {
33424
33486
  text: `Posted message ${message.id} (${messageRef}) ${notSubscribed(issueTopic(issueKey))}`,
33425
33487
  details: { issue: issueKey, message: message.id }
@@ -33481,7 +33543,7 @@ ${trailer.join(`
33481
33543
  }
33482
33544
  };
33483
33545
  }
33484
- const details = await followedAskDetails(client, result.ask, resolved);
33546
+ const details = await followedAskDetails(client, result.ask, resolved.artifact);
33485
33547
  return {
33486
33548
  text: `Approval requested for ${resolved.artifact.name} (document id ${resolved.artifact.id}) at version ${result.version} (ask ${result.ask.id}). The answer arrives as artifact.approved or artifact.changes_requested; an edit after approval makes it stale, so request again for the new version.`,
33487
33549
  details: { ...details, artifact: resolved.artifact.id, version: result.version }
@@ -33504,7 +33566,7 @@ ${trailer.join(`
33504
33566
  };
33505
33567
  const artifactOwner = documentOwner();
33506
33568
  const result = artifactOwner.kind === "project" ? await client.projectArtifact(artifactOwner.project, artifactInput) : await client.artifact(issue(), artifactInput);
33507
- const artifactRef = artifactOwner.kind === "project" ? `dispatch://${artifactOwner.project}/artifact/${result.artifact.slug}` : `dispatch://${issue()}/artifact/${result.artifact.slug}`;
33569
+ const artifactRef = dispatchDocumentRef(artifactOwner.kind === "project" ? artifactOwner.project : issue(), result.artifact.slug);
33508
33570
  const uploadOwner = artifactOwner.kind === "project" ? documentTopic(result.artifact) : issueTopic(issue());
33509
33571
  return {
33510
33572
  text: `Uploaded ${result.artifact.name} as version ${result.version.number} (artifact slug ${result.artifact.slug}; ${artifactRef}) ${notSubscribed(uploadOwner)}`,
@@ -33540,7 +33602,7 @@ ${trailer.join(`
33540
33602
  const ref = ownerArguments.ref;
33541
33603
  const id = await resolveIdPrefix(input.tool, "ask", ref, async () => ref.owner.kind === "issue" ? client.listIssueAsks(ref.owner.issue) : client.getArtifactAsks((await resolveArtifact(client, ref.owner, ref.artifact)).artifact.id, "all"));
33542
33604
  const askRead = await client.getAsk(id);
33543
- const askRef = ref.owner.kind === "issue" ? `dispatch://${ref.owner.issue}/ask/${id}` : `dispatch://${ref.owner.project}/artifact/${ref.artifact}/ask/${id}`;
33605
+ const askRef = refTarget(ref, "ask", id);
33544
33606
  return {
33545
33607
  text: askSummary(askRead, await graphSections(client, askRef)),
33546
33608
  details: ref.owner.kind === "project" ? { project: ref.owner.project } : { issue: ref.owner.issue }
@@ -33550,7 +33612,7 @@ ${trailer.join(`
33550
33612
  const ref = ownerArguments.ref;
33551
33613
  const id = await resolveIdPrefix(input.tool, "comment", ref, async () => ref.owner.kind === "issue" ? client.getComments(ref.owner.issue) : client.getArtifactComments((await resolveArtifact(client, ref.owner, ref.artifact)).artifact.id));
33552
33614
  const comment = await client.getComment(id);
33553
- const commentRef = ref.owner.kind === "issue" ? `dispatch://${ref.owner.issue}/comment/${id}` : `dispatch://${ref.owner.project}/artifact/${ref.artifact}/comment/${id}`;
33615
+ const commentRef = refTarget(ref, "comment", id);
33554
33616
  return {
33555
33617
  text: commentSummary(comment, await graphSections(client, commentRef)),
33556
33618
  details: ref.owner.kind === "project" ? { project: ref.owner.project } : { issue: comment.comment.issue_key }
@@ -33561,7 +33623,7 @@ ${trailer.join(`
33561
33623
  throw new Error("message references are issue-scoped");
33562
33624
  }
33563
33625
  const messageRead = await client.getMessage(ownerArguments.ref.owner.issue, ownerArguments.ref.id);
33564
- const messageRef = `dispatch://${ownerArguments.ref.owner.issue}/message/${ownerArguments.ref.id}`;
33626
+ const messageRef = dispatchChildRef(dispatchIssueRef(ownerArguments.ref.owner.issue), "message", ownerArguments.ref.id);
33565
33627
  return {
33566
33628
  text: messageSummary(messageRead, await graphSections(client, messageRef)),
33567
33629
  details: { issue: messageRead.message.issue_key }
@@ -33569,7 +33631,7 @@ ${trailer.join(`
33569
33631
  }
33570
33632
  if (documentOwner().kind === "project") {
33571
33633
  const resolved = await resolveArtifact(client, documentOwner(), stringArg(args, "artifact"));
33572
- const documentRef = `dispatch://${resolved.artifact.project}/artifact/${resolved.artifact.slug}`;
33634
+ const documentRef = dispatchDocumentRef(resolved.artifact.project, resolved.artifact.slug);
33573
33635
  return {
33574
33636
  text: [
33575
33637
  `Document: ${resolved.artifact.project} / ${resolved.artifact.name}`,
@@ -33581,7 +33643,7 @@ ${trailer.join(`
33581
33643
  `),
33582
33644
  details: {
33583
33645
  project: resolved.artifact.project,
33584
- document: `${resolved.artifact.project}/${resolved.artifact.slug}`
33646
+ document: documentLabel(resolved.artifact.project, resolved.artifact.slug)
33585
33647
  }
33586
33648
  };
33587
33649
  }
@@ -33598,14 +33660,12 @@ ${trailer.join(`
33598
33660
  details: { issue: read.issue.key }
33599
33661
  };
33600
33662
  }
33601
- let references;
33602
- try {
33603
- references = await client.getIssueReferences(read.issue.key);
33604
- } catch (error) {
33605
- references = error instanceof DispatchServiceError && error.status === 404 ? "unavailable" : `unavailable: ${error instanceof Error ? error.message : String(error)}`;
33606
- }
33663
+ const [references, graph] = await Promise.all([
33664
+ issueReferencesOrUnavailable(client, read.issue.key),
33665
+ graphSections(client, dispatchIssueRef(read.issue.key))
33666
+ ]);
33607
33667
  return {
33608
- text: issueSummary(read.issue, read.events, references, await graphSections(client, `dispatch://${read.issue.key}`)),
33668
+ text: issueSummary(read.issue, read.events, references, graph),
33609
33669
  details: { issue: read.issue.key }
33610
33670
  };
33611
33671
  }
@@ -33640,12 +33700,12 @@ function dispatchFollowNotice(details) {
33640
33700
  return null;
33641
33701
  let owner;
33642
33702
  if (typeof issue === "string" && issue !== "") {
33643
- owner = { label: issue, topic: dispatchIssueSubject(issue, ">") };
33703
+ owner = issueTopic(issue);
33644
33704
  } else if (typeof document === "string") {
33645
33705
  const [project, slug] = document.split("/", 2);
33646
33706
  if (!project || !slug)
33647
33707
  return null;
33648
- owner = { label: document, topic: dispatchDocumentSubject(project, slug, ">") };
33708
+ owner = { label: document, topic: documentTopicOf(project, slug).topic };
33649
33709
  } else {
33650
33710
  return null;
33651
33711
  }
@@ -33654,6 +33714,16 @@ function dispatchFollowNotice(details) {
33654
33714
  text: `Following ask ${ask} on ${owner.label}: its answer and replies reach you directly (dispatch_follow unfollow to stop). For every event on ${owner.label}: envoy_subscribe ${owner.topic}.`
33655
33715
  };
33656
33716
  }
33717
+ function createFollowAnnouncer(emit) {
33718
+ const announced = new Set;
33719
+ return (details) => {
33720
+ const notice = dispatchFollowNotice(details);
33721
+ if (notice === null || announced.has(notice.ask))
33722
+ return;
33723
+ announced.add(notice.ask);
33724
+ return emit(notice.text);
33725
+ };
33726
+ }
33657
33727
  function subscriptionRemovedTopics(raw, sessionID) {
33658
33728
  let envelope;
33659
33729
  try {
@@ -33801,11 +33871,19 @@ var envoyToolSpecs = [
33801
33871
  requiresSubscriptionCapability: false
33802
33872
  }
33803
33873
  ];
33874
+ function sendConfirmationText(result) {
33875
+ return `sent ${result.envelope.event_id} to ${result.recipient}${result.confirmed ? "" : " (recipient unconfirmed by listener)"}`;
33876
+ }
33877
+ var envoyToolSchemas = new Map;
33804
33878
  function parseEnvoyToolArguments(operation, parameters) {
33805
33879
  const spec = envoyToolSpecs.find((candidate) => candidate.operation === operation);
33806
33880
  if (spec === undefined)
33807
33881
  throw new Error(`missing Envoy tool specification for ${operation}`);
33808
- const schema = object(spec.arguments(zodSchemaApi(exports_external))).strict();
33882
+ let schema = envoyToolSchemas.get(operation);
33883
+ if (schema === undefined) {
33884
+ schema = object(spec.arguments(zodSchemaApi(exports_external))).strict();
33885
+ envoyToolSchemas.set(operation, schema);
33886
+ }
33809
33887
  const parsed = schema.safeParse(parameters, { reportInput: true });
33810
33888
  if (!parsed.success) {
33811
33889
  throw new ToolInputError(spec.name, formatZodIssues(parsed.error.issues, schema));
@@ -33854,6 +33932,17 @@ var EnvelopeResponseSchema = EnvelopeSchema.extend({
33854
33932
  recipient: string2().optional(),
33855
33933
  holder: string2().optional()
33856
33934
  });
33935
+ function mergeInterestSources(registryTopics, liveTopics) {
33936
+ const interests = new Map;
33937
+ for (const topic of registryTopics) {
33938
+ interests.set(topic, liveTopics.includes(topic) ? "both" : "registry");
33939
+ }
33940
+ for (const topic of liveTopics) {
33941
+ if (!interests.has(topic))
33942
+ interests.set(topic, "live");
33943
+ }
33944
+ return interests;
33945
+ }
33857
33946
  function expandSubscriptionTopics(topics) {
33858
33947
  const expanded = new Set;
33859
33948
  for (const topic of topics) {
@@ -34135,7 +34224,6 @@ async function copySessionID(copyToClipboard, sessionID) {
34135
34224
  // extensions/envoy.ts
34136
34225
  var codec2 = import_nats.StringCodec();
34137
34226
  var NATS_RETRY_INTERVAL_MS = 15000;
34138
- var ALL_CAPABILITIES = DELIVERY_CAPABILITIES;
34139
34227
  var CAPABILITIES_WITHOUT_BTW = DELIVERY_CAPABILITIES.filter((capability) => capability !== "btw");
34140
34228
  var ROLE_CLAIM_ENTRY = "envoy-role-claim";
34141
34229
  var OPEN_ASKS_TIMEOUT_MS = 3000;
@@ -34208,7 +34296,7 @@ function envoyExtension(pi) {
34208
34296
  return null;
34209
34297
  const snapshot = await new DispatchClient(config.url, config.token, fetch, AbortSignal.timeout(OPEN_ASKS_TIMEOUT_MS)).openAsks(requestedSessionID);
34210
34298
  availabilityWarningSessionIDs.delete(requestedSessionID);
34211
- return snapshot;
34299
+ return { snapshot, url: config.url };
34212
34300
  };
34213
34301
  const restoreLocalSessionState = (context) => {
34214
34302
  sessionDirectory = context.cwd;
@@ -34230,28 +34318,17 @@ function envoyExtension(pi) {
34230
34318
  return connection;
34231
34319
  };
34232
34320
  const postDispatchReply = async (delivery, result) => {
34233
- const config = currentDispatchConfig();
34234
- const response = await fetch(`${config.url}/api/v1/messages/${delivery.messageID}/reply`, {
34235
- method: "POST",
34236
- headers: {
34237
- Authorization: `Bearer ${config.token}`,
34238
- "Content-Type": "application/json"
34239
- },
34240
- body: JSON.stringify({
34241
- actor: { kind: "session", id: sessionID },
34242
- attempt: delivery.attempt,
34243
- ...result
34244
- })
34245
- });
34246
- if (!response.ok) {
34247
- throw new Error(`Dispatch reply failed: ${response.status} ${await response.text()}`);
34248
- }
34321
+ await postDeliveryReply(currentDispatchConfig(), sessionID, delivery, result);
34249
34322
  };
34250
34323
  const deliver = async (subject, raw, reply) => {
34251
34324
  const rendered = renderInbound(raw, sessionID, subject);
34252
34325
  const directSubject = agentSubject(sessionID);
34253
- const envelopeTopic = rendered.envelope?.topic;
34254
- if (reply !== "" && subject === directSubject && envelopeTopic !== undefined && envelopeTopic !== directSubject) {
34326
+ if (expectsLaneReceipt({
34327
+ subject,
34328
+ directSubject,
34329
+ envelopeTopic: rendered.envelope?.topic,
34330
+ reply
34331
+ })) {
34255
34332
  try {
34256
34333
  (await ensureConnection()).publish(reply);
34257
34334
  } catch (error) {
@@ -34305,14 +34382,8 @@ function envoyExtension(pi) {
34305
34382
  console.warn(`[envoy] failed to deliver envelope ${envelope?.event_id ?? "unknown"}`, error);
34306
34383
  throw error;
34307
34384
  }
34308
- if (dedupeKey !== undefined) {
34309
- dedupeKeys.add(dedupeKey);
34310
- if (dedupeKeys.size > 1000) {
34311
- const oldest = dedupeKeys.values().next();
34312
- if (!oldest.done)
34313
- dedupeKeys.delete(oldest.value);
34314
- }
34315
- }
34385
+ if (dedupeKey !== undefined)
34386
+ rememberBounded(dedupeKeys, dedupeKey, 1000);
34316
34387
  }
34317
34388
  };
34318
34389
  const RESUBSCRIBE_DELAY_MS = Number(process.env.ENVOY_RESUBSCRIBE_DELAY_MS ?? "") || 5000;
@@ -34378,7 +34449,7 @@ function envoyExtension(pi) {
34378
34449
  topics: [...new Set([agentSubject(sessionID), ...subscriptions.keys()])],
34379
34450
  port: 0,
34380
34451
  title: activeSessionContext?.sessionManager.getSessionName?.() ?? "",
34381
- capabilities: typeof pi.askEphemeral === "function" ? ALL_CAPABILITIES : CAPABILITIES_WITHOUT_BTW,
34452
+ capabilities: typeof pi.askEphemeral === "function" ? DELIVERY_CAPABILITIES : CAPABILITIES_WITHOUT_BTW,
34382
34453
  driving: false,
34383
34454
  selfSubscribed: true
34384
34455
  });
@@ -34734,14 +34805,14 @@ function envoyExtension(pi) {
34734
34805
  if (id === "")
34735
34806
  return;
34736
34807
  try {
34737
- const snapshot = await queryOpenAsks(id);
34738
- if (snapshot === null)
34808
+ const open = await queryOpenAsks(id);
34809
+ if (open === null)
34739
34810
  return;
34740
34811
  return {
34741
34812
  message: {
34742
34813
  customType: "dispatch-open-asks",
34743
34814
  content: `Dispatch authored-ask summary:
34744
- ${formatOpenAsksSummary(snapshot, currentDispatchConfig().url)}`,
34815
+ ${formatOpenAsksSummary(open.snapshot, open.url)}`,
34745
34816
  display: false,
34746
34817
  attribution: "agent"
34747
34818
  }
@@ -34758,15 +34829,13 @@ ${formatOpenAsksSummary(snapshot, currentDispatchConfig().url)}`,
34758
34829
  };
34759
34830
  }
34760
34831
  });
34761
- const announcedFollows = new Set;
34832
+ const announceFollow = createFollowAnnouncer((text) => {
34833
+ pi.sendMessage({ customType: "envoy-message", content: text, display: true }, { deliverAs: "steer", triggerTurn: false });
34834
+ });
34762
34835
  pi.on("tool_result", async (event) => {
34763
34836
  if (event.isError)
34764
34837
  return;
34765
- const notice = dispatchFollowNotice(event.details);
34766
- if (notice === null || announcedFollows.has(notice.ask))
34767
- return;
34768
- announcedFollows.add(notice.ask);
34769
- pi.sendMessage({ customType: "envoy-message", content: notice.text, display: true }, { deliverAs: "steer", triggerTurn: false });
34838
+ announceFollow(event.details);
34770
34839
  });
34771
34840
  async function execute(operation, rawParameters) {
34772
34841
  try {
@@ -34816,14 +34885,7 @@ Warnings: ${warnings.join("; ")}`, {
34816
34885
  }
34817
34886
  case EnvoyToolOperation.listInterests: {
34818
34887
  const registry = await client.getInterest(sessionID);
34819
- const interests = new Map;
34820
- for (const topic of registry.topics) {
34821
- interests.set(topic, subscriptions.has(topic) ? "both" : "registry");
34822
- }
34823
- for (const topic of subscriptions.keys()) {
34824
- if (!interests.has(topic))
34825
- interests.set(topic, "live");
34826
- }
34888
+ const interests = mergeInterestSources(registry.topics, [...subscriptions.keys()]);
34827
34889
  return toolSuccess(JSON.stringify({ ...registry, topics: [...interests.keys()] }, null, 2), {
34828
34890
  interests: [...interests].map(([topic, source]) => ({ topic, source }))
34829
34891
  });
@@ -34838,8 +34900,7 @@ Warnings: ${warnings.join("; ")}`, {
34838
34900
  message: stringFor(parameters, "message"),
34839
34901
  ...toMessageMetadata(parameters)
34840
34902
  });
34841
- const confirmation = result.confirmed ? "" : " (recipient unconfirmed by listener)";
34842
- return toolSuccess(`sent ${result.envelope.event_id} to ${result.recipient}${confirmation}`, {
34903
+ return toolSuccess(sendConfirmationText(result), {
34843
34904
  event_id: result.envelope.event_id,
34844
34905
  recipient: result.recipient,
34845
34906
  confirmed: result.confirmed
package/dist/legion.js CHANGED
@@ -28923,6 +28923,7 @@ function preprocess(fn, schema) {
28923
28923
  return pipe(transform(fn), schema);
28924
28924
  }
28925
28925
  // ../contracts/src/dispatch-api.ts
28926
+ var ASK_TURNS = ["human", "agent"];
28926
28927
  var DELIVERY_CAPABILITIES = ["aside", "btw", "steer"];
28927
28928
  var DispatchEventSchema = object({
28928
28929
  issue_key: string2().nullable(),
@@ -29003,8 +29004,8 @@ var CommentEventPayloadSchema = object({
29003
29004
  ask_id: string2().nullish(),
29004
29005
  ask_question: string2().optional(),
29005
29006
  ask_state: _enum2(["open", "answered", "resolved"]).optional(),
29006
- ask_waiting_on: _enum2(["human", "agent"]).optional(),
29007
- turn: _enum2(["human", "agent"]).nullish(),
29007
+ ask_waiting_on: _enum2(ASK_TURNS).optional(),
29008
+ turn: _enum2(ASK_TURNS).nullish(),
29008
29009
  anchor: object({ block_id: string2().nullable().optional(), quote: string2().optional() }).nullish(),
29009
29010
  suggestion: object({ replace_with: string2().optional() }).nullish(),
29010
29011
  author: object({ kind: string2(), id: string2() }).optional(),
@@ -29127,16 +29128,14 @@ function documentOwnerValidation(requireArtifact, alwaysRequireArtifact = false)
29127
29128
  message: alwaysRequireArtifact ? "Exactly one of issue and project is required; artifact or ref must name the document." : "Exactly one of issue and project is required; with project, artifact names the document."
29128
29129
  };
29129
29130
  }
29130
- var commentValidation = (() => {
29131
- const owner = documentOwnerValidation(true);
29132
- return {
29133
- check: (value) => {
29134
- const input = value;
29135
- return owner.check(value) && (input.turn === undefined || typeof input.reply_to_ask === "string");
29136
- },
29137
- message: `${owner.message} turn requires reply_to_ask.`
29138
- };
29139
- })();
29131
+ var commentOwner = documentOwnerValidation(true);
29132
+ var commentValidation = {
29133
+ check: (value) => {
29134
+ const input = value;
29135
+ return commentOwner.check(value) && (input.turn === undefined || typeof input.reply_to_ask === "string");
29136
+ },
29137
+ message: `${commentOwner.message} turn requires reply_to_ask.`
29138
+ };
29140
29139
  var ISSUE_COMPONENTS_MODES = ["inherit", "explicit", "none"];
29141
29140
  function componentsArgument(z) {
29142
29141
  return z.object({
@@ -29158,6 +29157,7 @@ var SPEC_SECTIONS = [
29158
29157
  ];
29159
29158
  var SPEC_WRITING_GUIDANCE = `When writing a spec, use these sections in order: ${SPEC_SECTIONS.join(", ")}. ` + "Write for a reader who has not seen the code: plain sentences, every identifier expanded on " + "first use, no coined shorthand; see skills/dispatch Writing for the human and Writing a spec.";
29160
29159
  var ASK_URGENCIES = ["low", "med", "high", "blocking"];
29160
+ var ASK_QUESTION_MAX = 800;
29161
29161
  var ISSUE_STATUSES = [
29162
29162
  "triage",
29163
29163
  "icebox",
@@ -29169,6 +29169,9 @@ var ISSUE_STATUSES = [
29169
29169
  "retro",
29170
29170
  "done"
29171
29171
  ];
29172
+ function isIssueStatus(value) {
29173
+ return ISSUE_STATUSES.includes(value);
29174
+ }
29172
29175
  var DOC_EDIT_OPS = ["replace", "delete", "insert", "retype", "move"];
29173
29176
  var dispatchToolSpecs = [
29174
29177
  {
@@ -29211,13 +29214,13 @@ var dispatchToolSpecs = [
29211
29214
  },
29212
29215
  {
29213
29216
  name: "dispatch_ask",
29214
- description: "Open a durable, answerable decision on an issue or project document. Do not use it for a status update or discussion; " + "use dispatch_message instead. A to-do a human must complete is a question phrased as that to-do, with the options you want (for example Done / Can't). " + "Anchor a document question, thread reply_to/reply_to_ask, or cite a dispatch:// " + 'reference \u2014 it must be answerable from its own text and anchor alone, never "see above". A quote anchor is pinned to its block. Question is at most 800 ' + `characters and has at most 8 options. ${OWNER_REFERENCE}`,
29217
+ description: "Open a durable, answerable decision on an issue or project document. Do not use it for a status update or discussion; " + "use dispatch_message instead. A to-do a human must complete is a question phrased as that to-do, with the options you want (for example Done / Can't). " + "Anchor a document question, thread reply_to/reply_to_ask, or cite a dispatch:// " + `reference \u2014 it must be answerable from its own text and anchor alone, never "see above". A quote anchor is pinned to its block. Question is at most ${ASK_QUESTION_MAX} ` + `characters and has at most 8 options. ${OWNER_REFERENCE}`,
29215
29218
  arguments: (z) => ({
29216
29219
  issue: z.string().describe(ISSUE_REFERENCE).optional(),
29217
29220
  project: z.string().describe("Project key owning the document.").optional(),
29218
29221
  artifact: z.string().describe("Project document artifact id, slug, or filename.").optional(),
29219
29222
  ref: z.string().describe("Optional dispatch:// reference (issue, document, message, or ask); appended to the question and rendered as a link.").optional(),
29220
- question: z.string({ max: 800 }).describe("Decision question, at most 800 characters."),
29223
+ question: z.string({ max: ASK_QUESTION_MAX }).describe(`Decision question, at most ${ASK_QUESTION_MAX} characters.`),
29221
29224
  options: z.array(z.object({
29222
29225
  label: z.string().describe("Selectable option label."),
29223
29226
  description: z.string().describe("Optional option context.").optional()
@@ -29237,7 +29240,7 @@ var dispatchToolSpecs = [
29237
29240
  description: "Edit an open question in place. Use it to correct or refine the same decision; retract the " + "old ask and open a new one when the decision itself changes. Previous text remains in the " + "event log. Only the asking session can edit it; answered or resolved asks cannot be edited.",
29238
29241
  arguments: (z) => ({
29239
29242
  ask: z.string().describe("Ask id to edit."),
29240
- question: z.string({ max: 800 }).describe("Replacement decision question, at most 800 characters.").optional(),
29243
+ question: z.string({ max: ASK_QUESTION_MAX }).describe(`Replacement decision question, at most ${ASK_QUESTION_MAX} characters.`).optional(),
29241
29244
  options: z.array(z.object({
29242
29245
  label: z.string().describe("Selectable option label."),
29243
29246
  description: z.string().describe("Optional option context.").optional()
@@ -29990,11 +29993,7 @@ function generation(env) {
29990
29993
  }
29991
29994
 
29992
29995
  // src/legion/daemon-client.ts
29993
- function defaultSleep(ms) {
29994
- const { promise, resolve } = Promise.withResolvers();
29995
- setTimeout(resolve, ms);
29996
- return promise;
29997
- }
29996
+ import { setTimeout as delay } from "timers/promises";
29998
29997
  var SPAWN_WORKER_RETRY_DELAYS_MS = [2000, 5000];
29999
29998
  var SPAWN_WORKER_ATTEMPTS = SPAWN_WORKER_RETRY_DELAYS_MS.length + 1;
30000
29999
 
@@ -30053,7 +30052,7 @@ function sessionCapability(body) {
30053
30052
  return { sessionId: candidate.sessionId, secret: candidate.secret };
30054
30053
  }
30055
30054
  var WORKER_SESSION_PATH = "/legion/v1/worker-session";
30056
- function createLegionDaemonClient(baseUrl, fetchFn = fetch, recovery, sleep = defaultSleep) {
30055
+ function createLegionDaemonClient(baseUrl, fetchFn = fetch, recovery, sleep = (ms) => delay(ms)) {
30057
30056
  const endpoint = baseUrl.replace(/\/+$/, "");
30058
30057
  const postOnce = async (path, body, schema, fetchImpl = fetchFn, onResponseReadError) => {
30059
30058
  const response = await fetchImpl(`${endpoint}${path}`, {
@@ -30362,9 +30361,6 @@ function toolFailure(error) {
30362
30361
 
30363
30362
  // src/legion/tools.ts
30364
30363
  var jsonSuccess = (details) => toolSuccess(JSON.stringify(details), details);
30365
- function isIssueStatus(value) {
30366
- return ISSUE_STATUSES.includes(value);
30367
- }
30368
30364
  function isDispatchArtifactId(value) {
30369
30365
  return LegionDaemonApi.GatesRegister.request.shape.artifactId.safeParse(value).success;
30370
30366
  }
@@ -30623,7 +30619,7 @@ var JJ_LOG_REWRITE_WORDS = ["undo", "abandon"];
30623
30619
  var JJ_OP_WORDS = ["op", "operation"];
30624
30620
  var JJ_OP_LOG_REWRITE_WORDS = ["restore", "revert"];
30625
30621
  var JJ_MENTION = /\bjj\b/;
30626
- var JJ_LOG_REWRITE_MENTION = /\b(?:undo|abandon)\b|\b(?:op|operation)\b\W+(?:restore|revert)\b/;
30622
+ var JJ_LOG_REWRITE_MENTION = new RegExp(`\\b(?:${JJ_LOG_REWRITE_WORDS.join("|")})\\b|\\b(?:${JJ_OP_WORDS.join("|")})\\b\\W+(?:${JJ_OP_LOG_REWRITE_WORDS.join("|")})\\b`);
30627
30623
  function jjLogRewriteMention(text) {
30628
30624
  if (!JJ_MENTION.test(text))
30629
30625
  return;
@@ -30,6 +30,8 @@ vocabulary, and is often on a phone. Write for that person.
30
30
  what each costs, and your recommendation with its reason.
31
31
  - Before posting, test it: could Sami, reading only this text on his phone, know what he is being
32
32
  told or asked? If not, rewrite it. Length is not the problem; density is.
33
+ - When an ask or message communicates a judgment, lead with that judgment in one sentence and put the mechanism underneath it. Do not make the reader ask a second time whether the result is a win. This shapes communication only when a judgment exists; it does not pre-decide an open question or remove its genuine options. Inferred from the AGENTC-186 12-hour-cap incident (platform PO, 2026-09-17).
34
+ - When a Dispatch message states a root cause, include the reproducing command or test in that same message. Without it, label the diagnosis a hypothesis; a diagnosis still in progress may say so plainly. This boundary applies to causal claims, not to reporting that an investigation has started. Inferred from the astro lane's 2026-09-16 retro (platform PO, 2026-09-17).
33
35
 
34
36
  ## Agent authentication
35
37
 
@@ -267,6 +269,8 @@ It returns the imported commit, or the recorded error when the model was rejecte
267
269
 
268
270
  Before saying you are waiting for human input, call `dispatch_open_asks`. With no arguments it lists this session's active asks across open issues and project documents, including whether the human or agent owes the next reply. With `dispatch_open_asks({ project })` it lists every open ask in that project — on its issues and on its documents, whoever authored them — which is how you audit what a whole project is waiting on rather than just your own asks.
269
271
 
272
+ **Unsettled product shape needs a decision before implementation.** When a page, navigation entry, table key, customer-scoping rule, or persisted sidecar would set product shape that Sami has not already settled, send a one-line ask before the first implementation commit. A platform-PO schema or contract ruling does not settle product shape. This does not turn a user-specified decision or routine implementation into an approval request; it is inferred from AGENTC-186's 2026-09-16 retro (platform PO, 2026-09-17).
273
+
270
274
  **Anything that needs the human is an ask, or it does not exist.** An approval, a credential,
271
275
  a setting only they can change, a review click, a conflict between two of their own rules - if
272
276
  your work waits on it, open a `dispatch_ask` the moment you know, the action as the question.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/pi-legion-envoy",
3
- "version": "1.49.0",
3
+ "version": "1.49.1",
4
4
  "type": "module",
5
5
  "omp": {
6
6
  "extensions": [