@sjawhar/pi-legion-envoy 7.8.2 → 7.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/envoy.js CHANGED
@@ -29997,6 +29997,21 @@ function snippetSegments(snippet) {
29997
29997
  function snippetText(snippet) {
29998
29998
  return snippetSegments(snippet).map(({ text, mark }) => mark ? `**${text}**` : text).join("");
29999
29999
  }
30000
+ // ../contracts/src/dispatch-text-rules.ts
30001
+ var SESSION_ID_PATTERN = "[^/?#\\s\\p{Z}\\p{Cc}<>\"'`\\[\\]|]+";
30002
+ var sessionIdOnly = new RegExp(`^${SESSION_ID_PATTERN}$`, "u");
30003
+ function pictureCaption(name) {
30004
+ return name.replace(/[\\[\]`]/g, "\\$&").replace(/\s+/g, " ");
30005
+ }
30006
+ var PICTURE_TYPES = {
30007
+ "image/png": true,
30008
+ "image/jpeg": true,
30009
+ "image/gif": true,
30010
+ "image/webp": true
30011
+ };
30012
+ function isPictureType(mime) {
30013
+ return PICTURE_TYPES[mime] === true;
30014
+ }
30000
30015
  // ../contracts/src/dispatch-tools.ts
30001
30016
  function dispatchToolSchema(spec, z2, opts) {
30002
30017
  const shape = spec.arguments(z2);
@@ -30070,8 +30085,12 @@ function componentsArgument(z2) {
30070
30085
  }).describe("Attach the issue to architecture components. Attach the root before decomposing it; children inherit unless they choose.");
30071
30086
  }
30072
30087
  var SPEC_WRITING_POINTER = 'Write a spec as the "Writing a spec" section of skill://dispatch says.';
30088
+ function imagesArgument(z2, owner, cap) {
30089
+ return z2.array(z2.string({ min: 1 }), { min: 1 }).describe(`Optional local picture files to show inline: PNG, JPEG, GIF or WebP (by their bytes, not their name), each at most 25 MiB. Each is uploaded to ${owner} and appended after a blank line as one ![<file name>](dispatch://\u2026@vN) line, in this order; those lines count toward ${cap}.`).optional();
30090
+ }
30073
30091
  var ASK_URGENCIES = ["low", "med", "high", "blocking"];
30074
30092
  var ASK_QUESTION_MAX = 800;
30093
+ var DISPATCH_BODY_MAX = 2000;
30075
30094
  var SEARCH_QUERY_MAX = 1000;
30076
30095
  var SEARCH_QUERY_HINT = "search with a short phrase of a few words, not a passage";
30077
30096
  var SEARCH_KIND_DEPTH = 100;
@@ -30171,7 +30190,7 @@ var dispatchToolSpecs = [
30171
30190
  }
30172
30191
  ]
30173
30192
  },
30174
- description: "Open a to-do or permission only a human can give, or a decision that has no document to " + "live in. A question about the design an issue's document records is not this tool: write " + "it into that document as a decision block (dispatch_doc_edit inserting an ask block at the " + "end of the section it concerns), at every phase, approved spec or not; the block reaches " + "the Inbox and its answer lands next to its context. Never give an ask an Approve option: a " + "document is approved through dispatch_request_approval. Do not use this tool for a status " + "update or discussion; use dispatch_message instead. " + ASK_QUESTION_CONTRACT + " " + ASK_OPTIONS_CONTRACT + " For an action only a human can perform, state what it changes and risks as constraints. " + "Anything that requires a human to do, including a credential or grant renewal, is an ask, " + "never a message. " + "Anchor a to-do about a document passage, 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}`,
30193
+ description: "Open a to-do or permission only a human can give, or a decision that has no document to " + "live in. A question about the design an issue's document records is not this tool: write " + "it into that document as a decision block (dispatch_doc_edit inserting an ask block at the " + "end of the section it concerns), at every phase, approved spec or not; the block reaches " + "the Inbox and its answer lands next to its context. Never give an ask an Approve option: a " + "document is approved through dispatch_request_approval. Do not use this tool for a status " + "update or discussion; use dispatch_message instead. " + ASK_QUESTION_CONTRACT + " " + ASK_OPTIONS_CONTRACT + " For an action only a human can perform, state what it changes and risks as constraints. " + "Anything that requires a human to do, including a credential or grant renewal, is an ask, " + "never a message. " + "Anchor a to-do about a document passage, 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, ` + `the lines images appends included, and has at most 8 options. ${OWNER_REFERENCE}`,
30175
30194
  arguments: (z2) => ({
30176
30195
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
30177
30196
  project: z2.string().describe("Project key owning the document.").optional(),
@@ -30188,7 +30207,8 @@ var dispatchToolSpecs = [
30188
30207
  artifact: z2.string().describe("Artifact slug or id containing the quoted text."),
30189
30208
  quote: z2.string().describe("Exact text the decision concerns."),
30190
30209
  occurrence: z2.number({ int: true, min: 0 }).describe("Zero-based occurrence of the quote.").optional()
30191
- }).describe("Optional document location for the question.").optional()
30210
+ }).describe("Optional document location for the question.").optional(),
30211
+ images: imagesArgument(z2, "the issue (or the project document's project)", `the ${ASK_QUESTION_MAX}-character question limit`)
30192
30212
  }),
30193
30213
  validation: documentOwnerValidation(true)
30194
30214
  },
@@ -30263,7 +30283,7 @@ var dispatchToolSpecs = [
30263
30283
  {
30264
30284
  name: "dispatch_comment",
30265
30285
  example: { issue: "DSP-1", body: "Looks good." },
30266
- description: "Add review feedback to an issue or project document quote, or reply to a question asked with dispatch_ask. " + "Do not use it for an exact replacement; use " + `dispatch_suggest instead. A quote anchor is pinned to its block. Body is at most 2,000 characters. ${OWNER_REFERENCE}`,
30286
+ description: "Add review feedback to an issue or project document quote, or reply to a question asked with dispatch_ask. " + "Do not use it for an exact replacement; use " + `dispatch_suggest instead. A quote anchor is pinned to its block. Body is at most 2,000 characters, the lines images appends included. ${OWNER_REFERENCE}`,
30267
30287
  arguments: (z2) => ({
30268
30288
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
30269
30289
  project: z2.string().describe("Project key owning the document.").optional(),
@@ -30271,10 +30291,11 @@ var dispatchToolSpecs = [
30271
30291
  ref: z2.string().describe("Optional dispatch:// issue or document reference.").optional(),
30272
30292
  quote: z2.string().describe("Optional exact quoted document text.").optional(),
30273
30293
  occurrence: z2.number({ int: true, min: 0 }).describe("Optional zero-based occurrence of quote.").optional(),
30274
- body: z2.string({ max: 2000 }).describe("Review comment, at most 2,000 characters."),
30294
+ body: z2.string({ max: DISPATCH_BODY_MAX }).describe("Review comment, at most 2,000 characters."),
30275
30295
  reply_to: z2.string().describe("A comment id (uuid); replying to any comment in a thread continues that thread (an " + "ask's clarification thread included). To reply to an ask, use reply_to_ask with the " + "ask id instead.").optional(),
30276
30296
  reply_to_ask: z2.string().describe("Optional ask id to reply to, threading this comment under that question. Mutually " + "exclusive with reply_to.").optional(),
30277
- turn: z2.enum(["agent", "human"]).describe("Only with reply_to_ask: who holds the turn after this reply. agent: a progress note - " + "you keep the turn and the ask stays 'Waiting on agents' for the human; human (default): " + "you need the human to act - the ask returns to 'Waiting on you'.").optional()
30297
+ turn: z2.enum(["agent", "human"]).describe("Only with reply_to_ask: who holds the turn after this reply. agent: a progress note - " + "you keep the turn and the ask stays 'Waiting on agents' for the human; human (default): " + "you need the human to act - the ask returns to 'Waiting on you'.").optional(),
30298
+ images: imagesArgument(z2, "the issue (or the project document's project)", "the 2,000-character body limit")
30278
30299
  }),
30279
30300
  validation: commentValidation
30280
30301
  },
@@ -30302,11 +30323,12 @@ var dispatchToolSpecs = [
30302
30323
  {
30303
30324
  name: "dispatch_message",
30304
30325
  example: { issue: "DSP-1", body: "Implementation started." },
30305
- description: "Post a note humans must read now: a reply to a human's message or a deliverable that landed. A to-do only a human can " + "complete is an ask (dispatch_ask), so it reaches their inbox. Never progress or status updates - Dispatch is a high-signal " + "record, not a log. Not a design decision (write it as a decision block in the document) or document feedback (dispatch_comment). " + "To answer a human's direct message to this session - one sent from the Agents page, which names no issue - pass that message's bare id as " + "in_reply_to and no issue; the reply lands in that conversation. Another call with the same in_reply_to and new text posts a follow-up, " + "threaded under this session's first reply; the same text again posts nothing. dispatch_read({message}) reads " + "that conversation back. Every other message names its issue. " + `Body is at most 2,000 characters. ${ISSUE_REFERENCE}`,
30326
+ description: "Post a note humans must read now: a reply to a human's message or a deliverable that landed. A to-do only a human can " + "complete is an ask (dispatch_ask), so it reaches their inbox. Never progress or status updates - Dispatch is a high-signal " + "record, not a log. Not a design decision (write it as a decision block in the document) or document feedback (dispatch_comment). " + "To answer a human's direct message to this session - one sent from the Agents page, which names no issue - pass that message's bare id as " + "in_reply_to and no issue; the reply lands in that conversation. Another call with the same in_reply_to and new text posts a follow-up, " + "threaded under this session's first reply; the same text again posts nothing. dispatch_read({message}) reads " + "that conversation back. Every other message names its issue. " + `Body is at most 2,000 characters, the lines images appends included. ${ISSUE_REFERENCE}`,
30306
30327
  arguments: (z2) => ({
30307
30328
  issue: z2.string().describe(`${ISSUE_REFERENCE} Omit it only when in_reply_to answers a human's direct message to this session.`).optional(),
30308
- body: z2.string({ max: 2000 }).describe("Update text, at most 2,000 characters."),
30309
- in_reply_to: z2.string().describe("Optional message id or dispatch://KEY/message/<id> reference to reply to, threading " + "this message under it so the reply stays with the original in the Conversation. " + "A bare id with no issue answers a human's direct message to this session; a " + "dispatch://KEY/message/<id> names the issue its message lives on, so that form is a " + "reply on that issue.").optional()
30329
+ body: z2.string({ max: DISPATCH_BODY_MAX }).describe("Update text, at most 2,000 characters."),
30330
+ in_reply_to: z2.string().describe("Optional message id or dispatch://KEY/message/<id> reference to reply to, threading " + "this message under it so the reply stays with the original in the Conversation. " + "A bare id with no issue answers a human's direct message to this session; a " + "dispatch://KEY/message/<id> names the issue its message lives on, so that form is a " + "reply on that issue.").optional(),
30331
+ images: imagesArgument(z2, "the issue (a reply to a direct message: your own conversation, dispatch://agent/<your session id>)", "the 2,000-character body limit")
30310
30332
  }),
30311
30333
  validation: messageValidation
30312
30334
  },
@@ -30351,13 +30373,13 @@ var dispatchToolSpecs = [
30351
30373
  {
30352
30374
  name: "dispatch_doc_read",
30353
30375
  example: { issue: "DSP-1" },
30354
- description: "Read a live document or a named document version, or the text of an uploaded file at its latest or named version. " + "Do not use it for issue status, asks, or events; " + "use dispatch_read instead. Supply ref, issue, or project plus artifact; issue plus an omitted artifact reads the primary document. " + "A live read returns its document token for an optional dispatch_doc_edit precondition; use /blocks for per-block tokens. " + "A file that is not UTF-8 text is described, with the route that serves its bytes. " + OWNER_REFERENCE,
30376
+ description: "Read a live document or a named document version, or the text of an uploaded file at its latest or named version. " + "Do not use it for issue status, asks, or events; " + "use dispatch_read instead. Supply ref, issue, or project plus artifact; issue plus an omitted artifact reads the primary document. " + "A live read returns its document token for an optional dispatch_doc_edit precondition; use /blocks for per-block tokens. " + "A picture (an uploaded PNG, JPEG, GIF or WebP of at most 3,750,000 bytes, 5 MB once base64-encoded) comes back as an image you see, with its name, type, size and version, every time you ask, including one a read already showed this session; " + "any other file that is not UTF-8 text is described, with the route that serves its bytes. " + OWNER_REFERENCE,
30355
30377
  arguments: (z2) => ({
30356
30378
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
30357
30379
  project: z2.string().describe("Project key owning the document.").optional(),
30358
30380
  artifact: z2.string().describe("Optional project document artifact id, slug, or filename; primary document by default for an issue.").optional(),
30359
30381
  version: z2.number({ int: true, min: 1 }).describe("Optional version number.").optional(),
30360
- ref: z2.string().describe("Optional dispatch:// document reference.").optional()
30382
+ ref: z2.string().describe("Optional dispatch:// document reference; a picture in a conversation on the Agents page is dispatch://agent/<session id>/artifact/<slug>@vN.").optional()
30361
30383
  }),
30362
30384
  validation: documentOwnerValidation(true)
30363
30385
  },
@@ -30397,7 +30419,7 @@ var dispatchToolSpecs = [
30397
30419
  {
30398
30420
  name: "dispatch_read",
30399
30421
  example: { issue: "DSP-1" },
30400
- description: "Read an issue or project-document summary, targeted ask, or targeted comment reply chain, or the conversation " + "a message belongs to. Do not use it for document contents; use dispatch_doc_read instead. Supply ref, issue, " + "or project plus artifact; or message alone, which reads a human's direct message to this session and every " + "reply to it (they belong to no issue). " + "An anchored comment or ask also says where its quote sits, as `Position:`: the block's path from the top, " + "and in a table the row (0 is the header), the cells before the anchored one, and the column's header; " + "`Position: unavailable (<code>)` when Dispatch could not read the document: `DOC_SERVICE_UNAVAILABLE` " + "(try again shortly), `DOC_SCHEMA` (the document needs repair), `DOCUMENT_UNLOADABLE` (the document " + "needs a rebuild) or `INTERNAL`. " + "An issue read carries a `Progress:` line, `tasks 3/7, children 2/5`: its spec's task-list items " + "(`- [ ]` / `- [x]`, nested lists included) and its direct children, each done of total and each only " + "when it has any (`Progress: none` otherwise). " + "Every read ends with `Referenced by:` (what cites or hangs off this node, each with its dispatch:// address, " + "an excerpt, and when) and `Links:` (what it cites), so tracing provenance is one call. " + OWNER_REFERENCE,
30422
+ description: "Read an issue or project-document summary, targeted ask, or targeted comment reply chain, or the conversation " + "a message belongs to. Do not use it for document contents; use dispatch_doc_read instead. Supply ref, issue, " + "or project plus artifact; or message alone, which reads a human's direct message to this session and every " + "reply to it (they belong to no issue). " + "An anchored comment or ask also says where its quote sits, as `Position:`: the block's path from the top, " + "and in a table the row (0 is the header), the cells before the anchored one, and the column's header; " + "`Position: unavailable (<code>)` when Dispatch could not read the document: `DOC_SERVICE_UNAVAILABLE` " + "(try again shortly), `DOC_SCHEMA` (the document needs repair), `DOCUMENT_UNLOADABLE` (the document " + "needs a rebuild) or `INTERNAL`. " + "An issue read carries a `Progress:` line, `tasks 3/7, children 2/5`: its spec's task-list items " + "(`- [ ]` / `- [x]`, nested lists included) and its direct children, each done of total and each only " + "when it has any (`Progress: none` otherwise). " + "Every read ends with `Referenced by:` (what cites or hangs off this node, each with its dispatch:// address, " + "an excerpt, and when) and `Links:` (what it cites), so tracing provenance is one call. " + "The pictures the shown messages, asks and comments embed come back as images you see, newest first, at most 8 " + "and 10 MiB of them per read, each a PNG, JPEG, GIF or WebP of at most 3,750,000 bytes (5 MB of base64); a `Pictures:` section names each " + "one shown, in order, and the rest by reference, for dispatch_doc_read. A picture this session was already shown is named, not sent again " + "(every request carries the session's history, and the provider refuses one over 32 MB); dispatch_doc_read shows it again. " + OWNER_REFERENCE,
30401
30423
  arguments: (z2) => ({
30402
30424
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
30403
30425
  project: z2.string().describe("Project key owning the document.").optional(),
@@ -31218,6 +31240,116 @@ function dispatchDocumentRef(owner, slug) {
31218
31240
  function dispatchChildRef(ownerRef, kind, id) {
31219
31241
  return `${ownerRef}/${kind}/${id}`;
31220
31242
  }
31243
+ function parseDispatchRef(ref) {
31244
+ const projectDocument = ref.match(/^dispatch:\/\/([A-Z][A-Z0-9]{1,9})\/artifact\/([^/@]+)(?:@v(\d+))?(?:\/(ask|comment)\/([^/]+))?$/);
31245
+ if (projectDocument) {
31246
+ const [, project, artifact2, version3, targetKind, targetID] = projectDocument;
31247
+ if (project === undefined || artifact2 === undefined || version3 !== undefined && Number(version3) < 1) {
31248
+ return null;
31249
+ }
31250
+ if (targetKind === undefined) {
31251
+ return {
31252
+ owner: { kind: "project", project },
31253
+ kind: "artifact",
31254
+ id: artifact2,
31255
+ ...version3 === undefined ? {} : { version: Number(version3) }
31256
+ };
31257
+ }
31258
+ if (targetID === undefined || targetKind !== "ask" && targetKind !== "comment")
31259
+ return null;
31260
+ return {
31261
+ owner: { kind: "project", project },
31262
+ kind: targetKind,
31263
+ id: targetID,
31264
+ artifact: artifact2
31265
+ };
31266
+ }
31267
+ const issueReference = ref.match(/^dispatch:\/\/([A-Z][A-Z0-9]{1,9}-[1-9][0-9]*)(?:\/(spec)|\/(log)|\/(children)|\/artifact\/([^/@]+)(?:@v(\d+))?|\/ask\/([^/]+)|\/comment\/([^/]+)|\/message\/([^/]+))?$/);
31268
+ if (!issueReference)
31269
+ return null;
31270
+ const [, issue2, spec, log, children, artifact, version2, ask, comment, message] = issueReference;
31271
+ if (!issue2 || version2 !== undefined && Number(version2) < 1)
31272
+ return null;
31273
+ const owner = { kind: "issue", issue: issue2 };
31274
+ if (spec)
31275
+ return { owner, kind: "spec", id: spec };
31276
+ if (log)
31277
+ return { owner, kind: "log", id: log };
31278
+ if (children)
31279
+ return { owner, kind: "children", id: children };
31280
+ if (artifact) {
31281
+ return {
31282
+ owner,
31283
+ kind: "artifact",
31284
+ id: artifact,
31285
+ ...version2 === undefined ? {} : { version: Number(version2) }
31286
+ };
31287
+ }
31288
+ if (ask)
31289
+ return { owner, kind: "ask", id: ask };
31290
+ if (comment)
31291
+ return { owner, kind: "comment", id: comment };
31292
+ if (message)
31293
+ return { owner, kind: "message", id: message };
31294
+ return { owner, kind: "issue", id: issue2 };
31295
+ }
31296
+
31297
+ // ../envoy-client/src/dispatch-pictures.ts
31298
+ var PICTURE_UPLOAD_MAX_BYTES = 25 * 1024 * 1024;
31299
+ var PICTURE_SHOWN_MAX_BYTES = 3750000;
31300
+ var pictureShownMaxText = `${PICTURE_SHOWN_MAX_BYTES.toLocaleString("en-US")} bytes (5 MB of base64)`;
31301
+ function oversizePictureText(name, bytes) {
31302
+ return `${name} is ${bytes.toLocaleString("en-US")} bytes, over the ${pictureShownMaxText} a model is shown`;
31303
+ }
31304
+ var PICTURES_SHOWN_MAX = 8;
31305
+ var PICTURES_SHOWN_MAX_BYTES = 10 * 1024 * 1024;
31306
+ var PICTURE_SNIFF_BYTES = 12;
31307
+ function bytesAt(bytes, offset, expected) {
31308
+ return expected.every((byte, index) => bytes[offset + index] === byte);
31309
+ }
31310
+ function sniffPictureType(bytes) {
31311
+ if (bytesAt(bytes, 0, [137, 80, 78, 71, 13, 10, 26, 10]))
31312
+ return "image/png";
31313
+ if (bytesAt(bytes, 0, [255, 216, 255]))
31314
+ return "image/jpeg";
31315
+ if (bytesAt(bytes, 0, [71, 73, 70, 56]) && (bytes[4] === 55 || bytes[4] === 57) && bytes[5] === 97) {
31316
+ return "image/gif";
31317
+ }
31318
+ if (bytesAt(bytes, 0, [82, 73, 70, 70]) && bytesAt(bytes, 8, [87, 69, 66, 80])) {
31319
+ return "image/webp";
31320
+ }
31321
+ return;
31322
+ }
31323
+ function toolImage(bytes) {
31324
+ const mimeType = sniffPictureType(bytes);
31325
+ if (mimeType === undefined || bytes.length > PICTURE_SHOWN_MAX_BYTES)
31326
+ return;
31327
+ return { data: Buffer.from(bytes).toString("base64"), mimeType };
31328
+ }
31329
+ function imageBlocks(images) {
31330
+ return images.map(({ data, mimeType }) => ({ type: "image", data, mimeType }));
31331
+ }
31332
+ function pictureLine(name, address) {
31333
+ return `![${pictureCaption(name)}](${address})`;
31334
+ }
31335
+ function withPictureLines(text, lines) {
31336
+ if (lines.length === 0)
31337
+ return text;
31338
+ const head = text.trimEnd();
31339
+ return head === "" ? lines.join(`
31340
+ `) : `${head}
31341
+
31342
+ ${lines.join(`
31343
+ `)}`;
31344
+ }
31345
+ var PICTURE_SYNTAX = /!\[(?:\\.|[^\\\]])*\]\((dispatch:\/\/[^\s()]+@v[1-9][0-9]*)(?:\s+"(?:\\.|[^\\"])*")?\)/g;
31346
+ function pictureAddresses(text) {
31347
+ return [...text.matchAll(PICTURE_SYNTAX)].map((match) => match[1]);
31348
+ }
31349
+ function picturesNewestFirst(texts) {
31350
+ 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);
31351
+ return [...new Set(ordered.flatMap(({ item }) => pictureAddresses(item.text)))];
31352
+ }
31221
31353
 
31222
31354
  // ../envoy-client/src/delivery.ts
31223
31355
  var KNOWN_SOURCES = EnvelopeSchema.shape.source.enum;
@@ -31372,6 +31504,33 @@ var RecoverableDispatchDeliveryFailureSchema = exports_external.object({
31372
31504
  }).passthrough(),
31373
31505
  delivery: DispatchTargetedDeliverySchema
31374
31506
  }).refine(({ event, delivery }) => event.type === "comment.created" ? isCommentTargetedDelivery(delivery) : !isCommentTargetedDelivery(delivery) && DispatchTargetedResourceIDSchema.safeParse(event.payload.id).success, { message: "delivery resource must match its event type" });
31507
+ function eventPictures(event) {
31508
+ let text;
31509
+ if (event.type.startsWith("message.")) {
31510
+ const parsed = MessageEventPayloadSchema.safeParse(event.payload);
31511
+ text = parsed.success ? parsed.data.body : undefined;
31512
+ } else if (event.type.startsWith("comment.")) {
31513
+ const parsed = CommentEventPayloadSchema.safeParse(event.payload);
31514
+ text = parsed.success ? parsed.data.body : undefined;
31515
+ } else if (event.type.startsWith("ask.")) {
31516
+ const parsed = (event.type === "ask.edited" ? AskEditedEventPayloadSchema : AskEventPayloadSchema).safeParse(event.payload);
31517
+ if (parsed.success) {
31518
+ switch (event.type) {
31519
+ case "ask.answered":
31520
+ text = parsed.data.answer?.text;
31521
+ break;
31522
+ case "ask.resolved":
31523
+ text = parsed.data.resolution?.reason;
31524
+ break;
31525
+ case "ask.opened":
31526
+ case "ask.edited":
31527
+ text = parsed.data.question;
31528
+ break;
31529
+ }
31530
+ }
31531
+ }
31532
+ return typeof text === "string" ? pictureAddresses(text) : [];
31533
+ }
31375
31534
  function senderLabel(envelope2) {
31376
31535
  const sender = envelope2.source_session ?? envelope2.source ?? "unknown";
31377
31536
  return envelope2.sender?.title === undefined ? sender : `${sender} (${envelope2.sender.title})`;
@@ -31648,6 +31807,7 @@ function renderInbound(raw, sessionID, subject2) {
31648
31807
  let delivery;
31649
31808
  let rejectedDelivery;
31650
31809
  let malformedDelivery = false;
31810
+ let pictures = [];
31651
31811
  let inReplyTo = envelope2.in_reply_to;
31652
31812
  let askAuthor;
31653
31813
  const dispatchRendered = envelope2.source === "dispatch" && envelope2.payload !== undefined;
@@ -31754,6 +31914,7 @@ function renderInbound(raw, sessionID, subject2) {
31754
31914
  ...compactRecord ?? { payload: dispatchPayloadForReader(frame.event, sessionID) }
31755
31915
  };
31756
31916
  dispatchActor = frame.event.actor;
31917
+ pictures = eventPictures(frame.event);
31757
31918
  } else {
31758
31919
  dispatchEvent = frame.raw;
31759
31920
  rejectedDelivery = frame.rejectedDelivery;
@@ -31824,7 +31985,8 @@ function renderInbound(raw, sessionID, subject2) {
31824
31985
  ...dispatchActor === undefined ? {} : { dispatchActor },
31825
31986
  ...delivery === undefined ? {} : { delivery },
31826
31987
  ...rejectedDelivery === undefined ? {} : { rejectedDelivery },
31827
- ...malformedDelivery ? { malformedDelivery: true } : {}
31988
+ ...malformedDelivery ? { malformedDelivery: true } : {},
31989
+ ...pictures.length === 0 ? {} : { pictures }
31828
31990
  };
31829
31991
  }
31830
31992
 
@@ -31978,7 +32140,7 @@ function activeDispatchConfig(env, options = {}) {
31978
32140
  }
31979
32141
 
31980
32142
  // ../envoy-client/src/dispatch-execute.ts
31981
- import { resolve as resolvePath } from "path";
32143
+ import { resolve as resolvePath2 } from "path";
31982
32144
 
31983
32145
  // ../envoy-client/src/dispatch-cwd.ts
31984
32146
  import { execFile } from "child_process";
@@ -32206,21 +32368,27 @@ class DispatchClient {
32206
32368
  return this.#json("GET", ["api", "v1", "projects", project, "artifacts"], undefined, unlinked ? { unlinked: "true" } : undefined);
32207
32369
  }
32208
32370
  async projectArtifact(project, input) {
32209
- const artifactPath = ["api", "v1", "projects", project, "artifacts"];
32210
- if ("content" in input)
32211
- return this.#json("POST", artifactPath, input);
32212
- const form = new FormData;
32213
- form.set("name", input.name);
32214
- if (input.summary !== undefined)
32215
- form.set("summary", input.summary);
32216
- if (input.actor !== undefined)
32217
- form.set("actor", JSON.stringify(input.actor));
32218
- form.set("file", input.file, input.name);
32219
- return this.#form("POST", artifactPath, form);
32371
+ return this.#upload(["api", "v1", "projects", project, "artifacts"], input);
32220
32372
  }
32221
32373
  async getProjectArtifact(project, slug) {
32222
32374
  return this.#json("GET", ["api", "v1", "projects", project, "artifacts", slug]);
32223
32375
  }
32376
+ async agentArtifact(sessionId, input) {
32377
+ return this.#upload(["api", "v1", "agents", sessionId, "artifacts"], input);
32378
+ }
32379
+ async getAgentArtifact(sessionId, slug) {
32380
+ return this.#json("GET", ["api", "v1", "agents", sessionId, "artifacts", slug]);
32381
+ }
32382
+ async getIssueArtifact(issue2, slug) {
32383
+ return this.#json("GET", [
32384
+ "api",
32385
+ "v1",
32386
+ "issues",
32387
+ await this.#resolveIssue(issue2),
32388
+ "artifacts",
32389
+ slug
32390
+ ]);
32391
+ }
32224
32392
  async search(query, options = {}) {
32225
32393
  return this.#json("GET", ["api", "v1", "search"], undefined, { q: query, ...options });
32226
32394
  }
@@ -32341,17 +32509,7 @@ class DispatchClient {
32341
32509
  return this.#json("POST", ["api", "v1", "messages", id, "deliveries", String(attempt), "accept"], input);
32342
32510
  }
32343
32511
  async artifact(issue2, input) {
32344
- const artifactPath = ["api", "v1", "issues", await this.#resolveIssue(issue2), "artifacts"];
32345
- if ("content" in input)
32346
- return this.#json("POST", artifactPath, input);
32347
- const form = new FormData;
32348
- form.set("name", input.name);
32349
- if (input.summary !== undefined)
32350
- form.set("summary", input.summary);
32351
- if (input.actor !== undefined)
32352
- form.set("actor", JSON.stringify(input.actor));
32353
- form.set("file", input.file, input.name);
32354
- return this.#form("POST", artifactPath, form);
32512
+ return this.#upload(["api", "v1", "issues", await this.#resolveIssue(issue2), "artifacts"], input);
32355
32513
  }
32356
32514
  async getArtifact(id) {
32357
32515
  return this.#json("GET", ["api", "v1", "artifacts", id]);
@@ -32458,6 +32616,18 @@ class DispatchClient {
32458
32616
  });
32459
32617
  return { status: response.status, payload: await this.#response(method, url2, response) };
32460
32618
  }
32619
+ async#upload(path2, input) {
32620
+ if ("content" in input)
32621
+ return this.#json("POST", path2, input);
32622
+ const form = new FormData;
32623
+ form.set("name", input.name);
32624
+ if (input.summary !== undefined)
32625
+ form.set("summary", input.summary);
32626
+ if (input.actor !== undefined)
32627
+ form.set("actor", JSON.stringify(input.actor));
32628
+ form.set("file", input.file, input.name);
32629
+ return this.#form("POST", path2, form);
32630
+ }
32461
32631
  async#form(method, path2, body) {
32462
32632
  const url2 = this.#url(path2);
32463
32633
  const response = await this.fetchImpl(url2, {
@@ -32516,70 +32686,8 @@ class DispatchClient {
32516
32686
  }
32517
32687
  }
32518
32688
 
32519
- // ../envoy-client/src/search-answer.ts
32520
- function pageSummaryText(offset, count, total) {
32521
- return `showing ${offset + 1}-${offset + count} of ${total}`;
32522
- }
32523
- function searchResultLine(result, baseUrl) {
32524
- const href = new URL(result.href, baseUrl).toString();
32525
- const { owner } = result;
32526
- if (owner.kind === "document") {
32527
- const reference = dispatchDocumentRef(owner.project, owner.slug);
32528
- return `${reference} [document] ${owner.name} - ${result.kind}: ${snippetText(result.snippet)} -> ${href}`;
32529
- }
32530
- const artifactName = result.artifact ? ` ${result.artifact.name}` : "";
32531
- const label = `${owner.key} [${owner.status}] ${owner.title} - ${result.kind}${artifactName}`;
32532
- return `${label}: ${snippetText(result.snippet)} -> ${href}`;
32533
- }
32534
- function searchAnswer(search, query, offset, configUrl) {
32535
- const results = search.results;
32536
- const count = results.length;
32537
- const lines = results.map((result) => searchResultLine(result, configUrl));
32538
- const noun = count === 1 ? "result" : "results";
32539
- const noResults = `No results for "${query}".`;
32540
- if (typeof search.total !== "number") {
32541
- if (offset !== undefined && offset > 0) {
32542
- throw new Error(`Dispatch answered without a total: it predates search paging and ignored offset ${offset}, so this would be its first page again.`);
32543
- }
32544
- return {
32545
- text: count === 0 ? noResults : [`${count} ${noun} for "${query}" (${search.took_ms} ms)`, ...lines].join(`
32546
- `),
32547
- details: { query, results }
32548
- };
32549
- }
32550
- const { total, reachable, offset: pageOffset } = search;
32551
- const end = pageOffset + count;
32552
- const cut = reachable < total ? `Each kind lists only its best ${SEARCH_KIND_DEPTH} matches, so ${reachable} of the ${total} can be paged to; narrow the query or name a project to reach the rest.` : undefined;
32553
- const details = {
32554
- query,
32555
- results,
32556
- total,
32557
- reachable,
32558
- offset: pageOffset,
32559
- limit: search.limit
32560
- };
32561
- if (count === 0) {
32562
- return {
32563
- text: total === 0 ? noResults : [
32564
- `No results for "${query}" at offset ${pageOffset}: it matches ${total}, and the pages reach the first ${reachable}.`,
32565
- ...cut === undefined ? [] : [cut]
32566
- ].join(`
32567
- `),
32568
- details
32569
- };
32570
- }
32571
- const showing = pageOffset === 0 && count === total ? "" : `${pageSummaryText(pageOffset, count, total)}, `;
32572
- return {
32573
- text: [
32574
- `${count} ${noun} for "${query}" (${showing}${search.took_ms} ms)`,
32575
- ...cut === undefined ? [] : [cut],
32576
- ...lines,
32577
- ...end < reachable ? [`Next page: offset ${end}.`] : []
32578
- ].join(`
32579
- `),
32580
- details
32581
- };
32582
- }
32689
+ // ../envoy-client/src/dispatch-picture-tools.ts
32690
+ import { basename, resolve as resolvePath } from "path";
32583
32691
 
32584
32692
  // ../envoy-client/src/tool-input-errors.ts
32585
32693
  class ToolInputError extends Error {
@@ -32704,6 +32812,326 @@ function formatZodIssues(issues, schema) {
32704
32812
  });
32705
32813
  }
32706
32814
 
32815
+ // ../envoy-client/src/dispatch-picture-tools.ts
32816
+ var PICTURE_TYPE_NAMES = "a PNG, JPEG, GIF or WebP";
32817
+ async function localPictures(images, cwd, problems) {
32818
+ if (!Array.isArray(images))
32819
+ return [];
32820
+ const checked = await Promise.all(images.map((path2) => localPicture(path2, cwd)));
32821
+ const pictures = [];
32822
+ for (const outcome of checked) {
32823
+ if (typeof outcome === "string")
32824
+ problems.push(outcome);
32825
+ else if (outcome !== undefined)
32826
+ pictures.push(outcome);
32827
+ }
32828
+ return pictures;
32829
+ }
32830
+ async function localPicture(path2, cwd) {
32831
+ if (typeof path2 !== "string" || path2 === "")
32832
+ return;
32833
+ const absolute = resolvePath(cwd, path2);
32834
+ const file2 = Bun.file(absolute);
32835
+ let size;
32836
+ let head;
32837
+ try {
32838
+ const stats = await file2.stat();
32839
+ if (!stats.isFile())
32840
+ return `images: ${path2} is not a file`;
32841
+ size = stats.size;
32842
+ head = new Uint8Array(await file2.slice(0, PICTURE_SNIFF_BYTES).arrayBuffer());
32843
+ } catch (error48) {
32844
+ return `images: ${path2} cannot be read: ${messageFor(error48)}`;
32845
+ }
32846
+ if (size > PICTURE_UPLOAD_MAX_BYTES) {
32847
+ return `images: ${path2} is ${size.toLocaleString("en-US")} bytes, over the ${PICTURE_UPLOAD_MAX_BYTES / 1024 / 1024} MiB one Dispatch upload takes`;
32848
+ }
32849
+ const type = sniffPictureType(head);
32850
+ if (type === undefined) {
32851
+ return `images: ${path2} is not ${PICTURE_TYPE_NAMES} picture (judged by its bytes, not its name)`;
32852
+ }
32853
+ return { path: path2, name: basename(absolute), file: Bun.file(absolute, { type }) };
32854
+ }
32855
+ function uploadSlug(name) {
32856
+ const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
32857
+ return slug === "" ? "artifact" : slug;
32858
+ }
32859
+ async function textWithPictures(tool, text, pictures, limit, owner, upload, actor) {
32860
+ if (pictures.length === 0)
32861
+ return text;
32862
+ const counted = `${limit.text} plus ${pictures.length} ${pictures.length === 1 ? "picture" : "pictures"}`;
32863
+ const fix = `${limit.shorten} or send fewer pictures`;
32864
+ const predicted = withPictureLines(text, pictures.map((picture) => pictureLine(picture.name, `dispatch://${owner}/artifact/${uploadSlug(picture.name)}@v1`)));
32865
+ if (predicted.length > limit.cap) {
32866
+ throw new ToolInputError(tool, [
32867
+ `${counted} ${overCapMessage(predicted.length, limit.cap)}; ${fix}`
32868
+ ]);
32869
+ }
32870
+ const lines = [];
32871
+ const addresses = [];
32872
+ for (const picture of pictures) {
32873
+ let uploaded;
32874
+ try {
32875
+ uploaded = await upload({ name: picture.name, file: picture.file, actor });
32876
+ } catch (error48) {
32877
+ const earlier = addresses.length === 0 ? "" : `; the pictures before it are uploaded (${addresses.join(", ")})`;
32878
+ throw new Error(`images: ${picture.path} was not uploaded: ${messageFor(error48)}. Nothing was posted${earlier}.`);
32879
+ }
32880
+ const address = `dispatch://${owner}/artifact/${uploaded.artifact.slug}@v${uploaded.version.number}`;
32881
+ addresses.push(address);
32882
+ lines.push(pictureLine(picture.name, address));
32883
+ }
32884
+ const posted = withPictureLines(text, lines);
32885
+ if (posted.length > limit.cap) {
32886
+ throw new Error(`${counted} ${overCapMessage(posted.length, limit.cap)} with the addresses Dispatch gave ` + `the uploads (${addresses.join(", ")}), so nothing was posted; ${fix}.`);
32887
+ }
32888
+ return posted;
32889
+ }
32890
+ var AGENT_ARTIFACT_REF = new RegExp(`^dispatch://agent/(${SESSION_ID_PATTERN})/artifact/([^/@]+)(?:@v(\\d+))?$`, "u");
32891
+ function parseAgentArtifactRef(ref) {
32892
+ const match = ref.match(AGENT_ARTIFACT_REF);
32893
+ if (match === null)
32894
+ return null;
32895
+ const [, session, slug, version2] = match;
32896
+ if (session === undefined || slug === undefined || version2 !== undefined && Number(version2) < 1) {
32897
+ return null;
32898
+ }
32899
+ return { session, slug, ...version2 === undefined ? {} : { version: Number(version2) } };
32900
+ }
32901
+ function datedBodies(records) {
32902
+ return records.map((record2) => ({ text: record2.body, at: record2.created_at }));
32903
+ }
32904
+ function askTexts({ ask, replies }) {
32905
+ return [
32906
+ { text: ask.question, at: ask.created_at },
32907
+ ...ask.answer?.text ? [{ text: ask.answer.text, at: ask.answer.at }] : [],
32908
+ ...ask.resolution === undefined ? [] : [{ text: ask.resolution.reason, at: ask.resolution.at }],
32909
+ ...datedBodies(replies)
32910
+ ];
32911
+ }
32912
+ function eventTexts(event) {
32913
+ let texts;
32914
+ switch (event.type) {
32915
+ case "ask.opened":
32916
+ case "ask.anchor_refreshed":
32917
+ case "ask.edited":
32918
+ case "ask.handed_back":
32919
+ case "ask.answered":
32920
+ case "ask.resolved":
32921
+ texts = [
32922
+ event.payload.question,
32923
+ event.payload.answer?.text,
32924
+ event.payload.resolution?.reason
32925
+ ];
32926
+ break;
32927
+ case "comment.created":
32928
+ case "comment.answered":
32929
+ case "comment.anchor_refreshed":
32930
+ case "comment.edited":
32931
+ case "comment.resolved":
32932
+ case "comment.reopened":
32933
+ case "suggestion.accepted":
32934
+ case "suggestion.rejected":
32935
+ case "message.created":
32936
+ case "message.answered":
32937
+ texts = [event.payload.body];
32938
+ break;
32939
+ default:
32940
+ texts = [];
32941
+ }
32942
+ return texts.filter((text) => typeof text === "string" && text !== "").map((text) => ({ text, at: event.created_at }));
32943
+ }
32944
+ function shownPictureLine(index, address, name, mimeType, bytes) {
32945
+ return `- image ${index}: ${address} (${name}, ${mimeType}, ${bytes.toLocaleString("en-US")} bytes)`;
32946
+ }
32947
+ var SHOWN_PICTURE_LINE = /^- image [1-9][0-9]*: (dispatch:\/\/[^\s()]+@v[1-9][0-9]*) \(/gm;
32948
+ function shownPictureAddresses(texts) {
32949
+ const addresses = new Set;
32950
+ for (const text of texts) {
32951
+ for (const match of text.matchAll(SHOWN_PICTURE_LINE))
32952
+ addresses.add(match[1]);
32953
+ }
32954
+ return [...addresses];
32955
+ }
32956
+ function pictureTarget(address) {
32957
+ const agent = parseAgentArtifactRef(address);
32958
+ if (agent !== null) {
32959
+ if (agent.version === undefined)
32960
+ return;
32961
+ return {
32962
+ key: `agent/${agent.session}/${agent.slug}`,
32963
+ read: (client) => client.getAgentArtifact(agent.session, agent.slug),
32964
+ version: agent.version
32965
+ };
32966
+ }
32967
+ const ref = parseDispatchRef(address);
32968
+ if (ref?.kind !== "artifact" || ref.version === undefined)
32969
+ return;
32970
+ const { owner, id: slug } = ref;
32971
+ return owner.kind === "issue" ? {
32972
+ key: `${owner.issue}/${slug}`,
32973
+ read: (client) => client.getIssueArtifact(owner.issue, slug),
32974
+ version: ref.version
32975
+ } : {
32976
+ key: `${owner.project}/${slug}`,
32977
+ read: (client) => client.getProjectArtifact(owner.project, slug),
32978
+ version: ref.version
32979
+ };
32980
+ }
32981
+ var SESSIONS_SHOWN_MAX = 64;
32982
+ var picturesShown = new Map;
32983
+ function shownPictures(sessionId) {
32984
+ const shown = picturesShown.get(sessionId) ?? new Set;
32985
+ picturesShown.delete(sessionId);
32986
+ picturesShown.set(sessionId, shown);
32987
+ const oldest = picturesShown.keys().next();
32988
+ if (picturesShown.size > SESSIONS_SHOWN_MAX && oldest.done !== true) {
32989
+ picturesShown.delete(oldest.value);
32990
+ }
32991
+ return shown;
32992
+ }
32993
+ function forgetShownPictures(sessionId) {
32994
+ picturesShown.delete(sessionId);
32995
+ }
32996
+ async function readPictures(client, addresses, shown) {
32997
+ if (addresses.length === 0)
32998
+ return { lines: [], images: [], shown: [] };
32999
+ const artifacts = new Map;
33000
+ const images = [];
33001
+ const showing = [];
33002
+ const lines = ["Pictures:"];
33003
+ let shownBytes = 0;
33004
+ const overBudget = `past this read's ${PICTURES_SHOWN_MAX_BYTES / 1024 / 1024} MiB of pictures; dispatch_doc_read shows it`;
33005
+ for (const address of addresses) {
33006
+ const skip = (reason) => lines.push(`- not shown: ${address} (${reason})`);
33007
+ if (shown?.has(address)) {
33008
+ skip("shown earlier this session; dispatch_doc_read shows it again");
33009
+ continue;
33010
+ }
33011
+ if (images.length === PICTURES_SHOWN_MAX) {
33012
+ skip(`past this read's ${PICTURES_SHOWN_MAX} pictures; dispatch_doc_read shows it`);
33013
+ continue;
33014
+ }
33015
+ const target = pictureTarget(address);
33016
+ if (target === undefined) {
33017
+ skip("names no versioned Dispatch artifact");
33018
+ continue;
33019
+ }
33020
+ try {
33021
+ let read = artifacts.get(target.key);
33022
+ if (read === undefined) {
33023
+ read = target.read(client);
33024
+ artifacts.set(target.key, read);
33025
+ }
33026
+ const artifact = await read;
33027
+ const version2 = artifact.versions.find((candidate) => candidate.number === target.version);
33028
+ if (version2 === undefined) {
33029
+ skip(`${artifact.name} has no version ${target.version}`);
33030
+ continue;
33031
+ }
33032
+ if (artifact.kind !== "image") {
33033
+ skip(`${artifact.name} is a ${artifact.kind}, not a picture`);
33034
+ continue;
33035
+ }
33036
+ const mime = version2.mime?.split(";")[0]?.trim();
33037
+ if (mime !== undefined && !isPictureType(mime)) {
33038
+ skip(`${artifact.name} is ${mime}, not ${PICTURE_TYPE_NAMES} a model is shown`);
33039
+ continue;
33040
+ }
33041
+ if (version2.size !== undefined && version2.size > PICTURE_SHOWN_MAX_BYTES) {
33042
+ skip(oversizePictureText(artifact.name, version2.size));
33043
+ continue;
33044
+ }
33045
+ if (version2.size !== undefined && shownBytes + version2.size > PICTURES_SHOWN_MAX_BYTES) {
33046
+ skip(overBudget);
33047
+ continue;
33048
+ }
33049
+ const file2 = await client.fileVersion(artifact.id, target.version);
33050
+ const image = toolImage(file2.bytes);
33051
+ if (image === undefined) {
33052
+ skip(sniffPictureType(file2.bytes) === undefined ? `${artifact.name} is not ${PICTURE_TYPE_NAMES} a model is shown` : oversizePictureText(artifact.name, file2.bytes.length));
33053
+ continue;
33054
+ }
33055
+ if (shownBytes + file2.bytes.length > PICTURES_SHOWN_MAX_BYTES) {
33056
+ skip(overBudget);
33057
+ continue;
33058
+ }
33059
+ shownBytes += file2.bytes.length;
33060
+ images.push(image);
33061
+ showing.push(address);
33062
+ lines.push(shownPictureLine(images.length, address, artifact.name, image.mimeType, file2.bytes.length));
33063
+ } catch (error48) {
33064
+ skip(`unavailable: ${messageFor(error48)}`);
33065
+ }
33066
+ }
33067
+ return { lines, images, shown: showing };
33068
+ }
33069
+
33070
+ // ../envoy-client/src/search-answer.ts
33071
+ function pageSummaryText(offset, count, total) {
33072
+ return `showing ${offset + 1}-${offset + count} of ${total}`;
33073
+ }
33074
+ function searchResultLine(result, baseUrl) {
33075
+ const href = new URL(result.href, baseUrl).toString();
33076
+ const { owner } = result;
33077
+ if (owner.kind === "document") {
33078
+ const reference = dispatchDocumentRef(owner.project, owner.slug);
33079
+ return `${reference} [document] ${owner.name} - ${result.kind}: ${snippetText(result.snippet)} -> ${href}`;
33080
+ }
33081
+ const artifactName = result.artifact ? ` ${result.artifact.name}` : "";
33082
+ const label = `${owner.key} [${owner.status}] ${owner.title} - ${result.kind}${artifactName}`;
33083
+ return `${label}: ${snippetText(result.snippet)} -> ${href}`;
33084
+ }
33085
+ function searchAnswer(search, query, offset, configUrl) {
33086
+ const results = search.results;
33087
+ const count = results.length;
33088
+ const lines = results.map((result) => searchResultLine(result, configUrl));
33089
+ const noun = count === 1 ? "result" : "results";
33090
+ const noResults = `No results for "${query}".`;
33091
+ if (typeof search.total !== "number") {
33092
+ if (offset !== undefined && offset > 0) {
33093
+ throw new Error(`Dispatch answered without a total: it predates search paging and ignored offset ${offset}, so this would be its first page again.`);
33094
+ }
33095
+ return {
33096
+ text: count === 0 ? noResults : [`${count} ${noun} for "${query}" (${search.took_ms} ms)`, ...lines].join(`
33097
+ `),
33098
+ details: { query, results }
33099
+ };
33100
+ }
33101
+ const { total, reachable, offset: pageOffset } = search;
33102
+ const end = pageOffset + count;
33103
+ const cut = reachable < total ? `Each kind lists only its best ${SEARCH_KIND_DEPTH} matches, so ${reachable} of the ${total} can be paged to; narrow the query or name a project to reach the rest.` : undefined;
33104
+ const details = {
33105
+ query,
33106
+ results,
33107
+ total,
33108
+ reachable,
33109
+ offset: pageOffset,
33110
+ limit: search.limit
33111
+ };
33112
+ if (count === 0) {
33113
+ return {
33114
+ text: total === 0 ? noResults : [
33115
+ `No results for "${query}" at offset ${pageOffset}: it matches ${total}, and the pages reach the first ${reachable}.`,
33116
+ ...cut === undefined ? [] : [cut]
33117
+ ].join(`
33118
+ `),
33119
+ details
33120
+ };
33121
+ }
33122
+ const showing = pageOffset === 0 && count === total ? "" : `${pageSummaryText(pageOffset, count, total)}, `;
33123
+ return {
33124
+ text: [
33125
+ `${count} ${noun} for "${query}" (${showing}${search.took_ms} ms)`,
33126
+ ...cut === undefined ? [] : [cut],
33127
+ ...lines,
33128
+ ...end < reachable ? [`Next page: offset ${end}.`] : []
33129
+ ].join(`
33130
+ `),
33131
+ details
33132
+ };
33133
+ }
33134
+
32707
33135
  // ../envoy-client/src/dispatch-execute.ts
32708
33136
  function documentTopic(artifact) {
32709
33137
  return documentTopicOf(artifact.project, artifact.slug);
@@ -32931,59 +33359,11 @@ function askQuestionWithRef(args) {
32931
33359
  function askQuestionProblem(withRef) {
32932
33360
  return withRef.length > ASK_QUESTION_MAX ? `question plus ref ${overCapMessage(withRef.length, ASK_QUESTION_MAX)}; shorten the question or drop the ref` : undefined;
32933
33361
  }
32934
- function parseDispatchRef(ref) {
32935
- const projectDocument = ref.match(/^dispatch:\/\/([A-Z][A-Z0-9]{1,9})\/artifact\/([^/@]+)(?:@v(\d+))?(?:\/(ask|comment)\/([^/]+))?$/);
32936
- if (projectDocument) {
32937
- const [, project, artifact2, version3, targetKind, targetID] = projectDocument;
32938
- if (project === undefined || artifact2 === undefined || version3 !== undefined && Number(version3) < 1) {
32939
- return null;
32940
- }
32941
- if (targetKind === undefined) {
32942
- return {
32943
- owner: { kind: "project", project },
32944
- kind: "artifact",
32945
- id: artifact2,
32946
- ...version3 === undefined ? {} : { version: Number(version3) }
32947
- };
32948
- }
32949
- if (targetID === undefined || targetKind !== "ask" && targetKind !== "comment")
32950
- return null;
32951
- return {
32952
- owner: { kind: "project", project },
32953
- kind: targetKind,
32954
- id: targetID,
32955
- artifact: artifact2
32956
- };
32957
- }
32958
- const issueReference = ref.match(/^dispatch:\/\/([A-Z][A-Z0-9]{1,9}-[1-9][0-9]*)(?:\/(spec)|\/(log)|\/(children)|\/artifact\/([^/@]+)(?:@v(\d+))?|\/ask\/([^/]+)|\/comment\/([^/]+)|\/message\/([^/]+))?$/);
32959
- if (!issueReference)
32960
- return null;
32961
- const [, issue2, spec, log, children, artifact, version2, ask, comment, message] = issueReference;
32962
- if (!issue2 || version2 !== undefined && Number(version2) < 1)
32963
- return null;
32964
- const owner = { kind: "issue", issue: issue2 };
32965
- if (spec)
32966
- return { owner, kind: "spec", id: spec };
32967
- if (log)
32968
- return { owner, kind: "log", id: log };
32969
- if (children)
32970
- return { owner, kind: "children", id: children };
32971
- if (artifact) {
32972
- return {
32973
- owner,
32974
- kind: "artifact",
32975
- id: artifact,
32976
- ...version2 === undefined ? {} : { version: Number(version2) }
32977
- };
32978
- }
32979
- if (ask)
32980
- return { owner, kind: "ask", id: ask };
32981
- if (comment)
32982
- return { owner, kind: "comment", id: comment };
32983
- if (message)
32984
- return { owner, kind: "message", id: message };
32985
- return { owner, kind: "issue", id: issue2 };
32986
- }
33362
+ var pictureSendingTools = {
33363
+ dispatch_ask: true,
33364
+ dispatch_comment: true,
33365
+ dispatch_message: true
33366
+ };
32987
33367
  function refTarget(ref, kind, id) {
32988
33368
  const ownerRef = ref.owner.kind === "issue" ? dispatchIssueRef(ref.owner.issue) : dispatchDocumentRef(ref.owner.project, `${ref.artifact}`);
32989
33369
  return dispatchChildRef(ownerRef, kind, id);
@@ -33163,6 +33543,15 @@ function dispatchRefFromUrl(value, serverUrl) {
33163
33543
  return `dispatch://${key}/message/${message}`;
33164
33544
  return `dispatch://${key}`;
33165
33545
  }
33546
+ const agentArtifactPage = url2.pathname.match(/^\/agents\/([^/]+)\/artifacts\/([^/]+)\/?$/);
33547
+ if (agentArtifactPage) {
33548
+ const [, session, slug2] = agentArtifactPage;
33549
+ try {
33550
+ return `dispatch://agent/${decodeURIComponent(session ?? "")}/artifact/${decodeURIComponent(slug2 ?? "")}${versionOf("v")}`;
33551
+ } catch {
33552
+ return;
33553
+ }
33554
+ }
33166
33555
  const documentPage = url2.pathname.match(/^\/projects\/([A-Z][A-Z0-9]{1,9})\/documents\/([^/]+)\/?$/);
33167
33556
  if (!documentPage)
33168
33557
  return;
@@ -33173,7 +33562,7 @@ function dispatchRefFromUrl(value, serverUrl) {
33173
33562
  return;
33174
33563
  return item === undefined ? document : `${document}/${item.kind}/${item.id}`;
33175
33564
  }
33176
- 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";
33565
+ 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>, " + "dispatch://PROJECT/artifact/<document-ref> (an artifact id, slug, or filename), or " + "dispatch://agent/<session id>/artifact/<slug> (a picture in a conversation on the Agents page, " + "for dispatch_doc_read); a dashboard URL on this Dispatch server is accepted too";
33177
33566
  var ownerRequiredProblem = "issue is required; supply issue or set LEGION_ISSUE";
33178
33567
  var toolSchemas = new Map;
33179
33568
  function toolSchema(tool) {
@@ -33197,6 +33586,16 @@ async function resolveOwnerArguments(tool, input, cwd, env, exec, serverUrl, pro
33197
33586
  const refText = dispatchRefFromUrl(refArgument, serverUrl) ?? refArgument;
33198
33587
  if (refText !== refArgument)
33199
33588
  args = { ...input, ref: refText };
33589
+ const agentArtifact = parseAgentArtifactRef(refText);
33590
+ if (agentArtifact !== null) {
33591
+ if (tool !== "dispatch_doc_read") {
33592
+ problems.push(`ref ${refText} names a picture in a conversation on the Agents page; only dispatch_doc_read reads one`);
33593
+ }
33594
+ if (args.issue !== undefined || args.project !== undefined || args.artifact !== undefined) {
33595
+ problems.push("a dispatch://agent/... ref names its upload alone: name no issue, project, or artifact with it");
33596
+ }
33597
+ return { args, ref: null, owner: null, agentArtifact };
33598
+ }
33200
33599
  ref = parseDispatchRef(refText);
33201
33600
  if (ref === null) {
33202
33601
  problems.push(tool === "dispatch_ask" && !refText.startsWith("dispatch://") ? "ref must be a dispatch:// reference" : refGrammarProblem);
@@ -33542,6 +33941,7 @@ function eventHead(event) {
33542
33941
  case "ask.answered":
33543
33942
  return `${textHead(event.payload.question)} -> ${textHead(askAnswerText(event.payload.answer))}`;
33544
33943
  case "comment.created":
33944
+ case "comment.answered":
33545
33945
  case "comment.anchor_refreshed":
33546
33946
  case "comment.edited":
33547
33947
  case "comment.resolved":
@@ -33727,20 +34127,42 @@ async function blockAsks(client, resolved, state) {
33727
34127
  const asks = await (resolved.issue === undefined ? client.getArtifactAsks(resolved.artifact.id, state) : client.listIssueAsks(resolved.issue.key, state));
33728
34128
  return asks.filter((ask) => typeof ask.block_id === "string" && ask.block_artifact?.id === resolved.artifact.id);
33729
34129
  }
33730
- async function readUploadedFile(client, resolved, requested) {
33731
- const { artifact } = resolved;
34130
+ async function readUploadedFile(client, artifact, details, requested, owner, shown) {
33732
34131
  const latest = Math.max(0, ...artifact.versions.map((version2) => version2.number));
33733
34132
  const number4 = requested ?? latest;
33734
- const file2 = await client.fileVersion(artifact.id, number4);
33735
34133
  const of = number4 === latest ? "" : ` of ${latest}`;
34134
+ const bytesRoute = `GET /api/v1/artifacts/${artifact.id}/versions/${number4} serves its bytes.`;
34135
+ const tooLarge = (mime, bytes) => ({
34136
+ text: `${oversizePictureText(artifact.name, bytes)}, so dispatch_doc_read cannot show this ` + `uploaded ${mime} picture (version ${number4}${of}). ${bytesRoute}`,
34137
+ details
34138
+ });
34139
+ const stated = artifact.versions.find((version2) => version2.number === number4);
34140
+ if (artifact.kind === "image" && stated?.size !== undefined && stated.size > PICTURE_SHOWN_MAX_BYTES) {
34141
+ return tooLarge(stated.mime ?? "image", stated.size);
34142
+ }
34143
+ const file2 = await client.fileVersion(artifact.id, number4);
33736
34144
  const size = `${file2.bytes.length.toLocaleString("en-US")} bytes`;
33737
- const details = resolved.owner.kind === "project" ? { project: artifact.project, document: documentLabel(artifact.project, artifact.slug) } : { issue: resolved.issue?.key };
34145
+ if (artifact.kind === "image") {
34146
+ const image = toolImage(file2.bytes);
34147
+ if (image !== undefined) {
34148
+ const address = `dispatch://${owner}/artifact/${artifact.slug}@v${number4}`;
34149
+ shown?.add(address);
34150
+ return {
34151
+ text: `Picture ${artifact.name}: ${image.mimeType}, version ${number4}${of}, ${size}.
34152
+ ` + shownPictureLine(1, address, artifact.name, image.mimeType, file2.bytes.length),
34153
+ details,
34154
+ images: [image]
34155
+ };
34156
+ }
34157
+ if (sniffPictureType(file2.bytes) !== undefined)
34158
+ return tooLarge(file2.mime, file2.bytes.length);
34159
+ }
33738
34160
  let text;
33739
34161
  try {
33740
34162
  text = new TextDecoder("utf-8", { fatal: true }).decode(file2.bytes);
33741
34163
  } catch {
33742
34164
  return {
33743
- text: `${artifact.name} is an uploaded ${file2.mime} file (version ${number4}${of}, ${size}) that is not ` + `UTF-8 text, so dispatch_doc_read cannot show it. GET /api/v1/artifacts/${artifact.id}/versions/${number4} serves its bytes.`,
34165
+ text: `${artifact.name} is an uploaded ${file2.mime} file (version ${number4}${of}, ${size}) that is not ` + `UTF-8 text, so dispatch_doc_read cannot show it. ${bytesRoute}`,
33744
34166
  details
33745
34167
  };
33746
34168
  }
@@ -33881,6 +34303,7 @@ async function executeDispatchTool(input) {
33881
34303
  problems.push(...formatZodIssues(issues, schema));
33882
34304
  }
33883
34305
  problems.push(...argumentProblems(input.tool, ownerArguments.args));
34306
+ const pictures = pictureSendingTools[input.tool] === true ? await localPictures(ownerArguments.args.images, input.cwd, problems) : [];
33884
34307
  if (problems.length > 0)
33885
34308
  throw new ToolInputError(input.tool, problems);
33886
34309
  const dispatchClient = () => new DispatchClient(configUrl, configToken, fetchImpl, input.signal);
@@ -33932,6 +34355,17 @@ async function executeDispatchTool(input) {
33932
34355
  throw new Error("issue or project is required");
33933
34356
  return owner;
33934
34357
  };
34358
+ const postWithPictures = (text, limit, owner2, upload) => textWithPictures(input.tool, text, pictures, limit, owner2, upload, actor);
34359
+ const writePictures = (resolved, text, limit) => {
34360
+ if (resolved?.owner.kind === "project") {
34361
+ const { project } = resolved.owner;
34362
+ return postWithPictures(text, limit, project, (upload) => client.projectArtifact(project, upload));
34363
+ }
34364
+ const issueKey = issue2();
34365
+ return postWithPictures(text, limit, issueKey, (upload) => client.artifact(issueKey, upload));
34366
+ };
34367
+ const hostSession = input.sessionId?.trim();
34368
+ const shown = hostSession ? shownPictures(hostSession) : undefined;
33935
34369
  switch (input.tool) {
33936
34370
  case "dispatch_issue": {
33937
34371
  const project = stringArg(args, "project");
@@ -34234,8 +34668,15 @@ async function executeDispatchTool(input) {
34234
34668
  const multiple = optionalBoolean(args, "multiple");
34235
34669
  const urgency = askUrgency(args);
34236
34670
  const anchored = anchorArgs && resolved ? anchor(resolved.artifact, anchorArgs) : undefined;
34671
+ const withRef = askQuestionWithRef(args);
34672
+ const refAppended = withRef !== stringArg(args, "question");
34673
+ const question = await writePictures(resolved, withRef, {
34674
+ cap: ASK_QUESTION_MAX,
34675
+ text: refAppended ? "question plus ref" : "question",
34676
+ shorten: refAppended ? "shorten the question, drop the ref" : "shorten the question"
34677
+ });
34237
34678
  const askInput = {
34238
- question: askQuestionWithRef(args),
34679
+ question,
34239
34680
  ...Array.isArray(options) ? { options } : {},
34240
34681
  ...multiple === undefined ? {} : { multiple },
34241
34682
  ...urgency === undefined ? {} : { urgency },
@@ -34293,8 +34734,13 @@ ${followsAsk(askOwner)}`,
34293
34734
  ]);
34294
34735
  }
34295
34736
  const requestedTurn = optionalString(args, "turn");
34737
+ const body = await writePictures(resolved, stringArg(args, "body"), {
34738
+ cap: DISPATCH_BODY_MAX,
34739
+ text: "body",
34740
+ shorten: "shorten the body"
34741
+ });
34296
34742
  const commentInput = {
34297
- body: stringArg(args, "body"),
34743
+ body,
34298
34744
  ...anchored === undefined ? {} : { anchor: anchored },
34299
34745
  ...replyTo === undefined ? {} : { reply_to: replyTo },
34300
34746
  ...replyToAsk === undefined ? {} : { ask_id: replyToAsk },
@@ -34354,16 +34800,29 @@ ${followsAsk(askOwner)}`,
34354
34800
  }
34355
34801
  case "dispatch_message": {
34356
34802
  const inReplyTo = messageInReplyTo(args);
34357
- const body = stringArg(args, "body");
34803
+ const written = stringArg(args, "body");
34804
+ const bodyLimit = {
34805
+ cap: DISPATCH_BODY_MAX,
34806
+ text: "body",
34807
+ shorten: "shorten the body"
34808
+ };
34358
34809
  if (owner === null && inReplyTo !== undefined) {
34359
- const reply = await client.messageReply(inReplyTo, { body, attempt: 1, actor }, { followUp: true });
34360
- if (reply.duplicate === true && reply.body === body) {
34810
+ let body2 = written;
34811
+ if (pictures.length > 0) {
34812
+ const sessionId = input.sessionId?.trim();
34813
+ if (!sessionId) {
34814
+ throw new Error("host session id is required to send pictures in a direct-message reply");
34815
+ }
34816
+ body2 = await postWithPictures(written, bodyLimit, `agent/${sessionId}`, (upload) => client.agentArtifact(sessionId, upload));
34817
+ }
34818
+ const reply = await client.messageReply(inReplyTo, { body: body2, attempt: 1, actor }, { followUp: true });
34819
+ if (reply.duplicate === true && reply.body === body2) {
34361
34820
  return {
34362
34821
  text: `Dispatch already has this exact text in the conversation (message ${reply.id}); ` + "nothing new was posted. Send different text if you have more to say.",
34363
34822
  details: { message: reply.id, in_reply_to: inReplyTo, posted: false, duplicate: true }
34364
34823
  };
34365
34824
  }
34366
- if (reply.body !== body) {
34825
+ if (reply.body !== body2) {
34367
34826
  return {
34368
34827
  text: `Message ${inReplyTo} was already answered by message ${reply.id}; Dispatch kept ` + "that reply and posted nothing. Wait for their next message rather than answering " + "this one again.",
34369
34828
  details: { message: reply.id, in_reply_to: inReplyTo, posted: false }
@@ -34383,6 +34842,7 @@ ${followsAsk(askOwner)}`,
34383
34842
  };
34384
34843
  }
34385
34844
  const issueKey = issue2();
34845
+ const body = await postWithPictures(written, bodyLimit, issueKey, (upload) => client.artifact(issueKey, upload));
34386
34846
  const message = await client.message(issueKey, {
34387
34847
  body,
34388
34848
  ...inReplyTo === undefined ? {} : { in_reply_to: inReplyTo },
@@ -34446,11 +34906,20 @@ ${followsAsk(askOwner)}`,
34446
34906
  };
34447
34907
  }
34448
34908
  case "dispatch_doc_read": {
34909
+ const { agentArtifact } = ownerArguments;
34910
+ if (agentArtifact !== undefined) {
34911
+ const artifact = await client.getAgentArtifact(agentArtifact.session, agentArtifact.slug);
34912
+ return readUploadedFile(client, artifact, { session: agentArtifact.session, artifact: artifact.slug }, optionalNumber(args, "version") ?? agentArtifact.version, `agent/${agentArtifact.session}`, shown);
34913
+ }
34449
34914
  const artifactReference = optionalString(args, "artifact") ?? (ownerArguments.ref?.kind === "spec" || ownerArguments.ref?.kind === "artifact" ? ownerArguments.ref.id : undefined);
34450
34915
  const resolved = await resolveDocument(documentOwner(), artifactReference);
34451
34916
  const version2 = optionalNumber(args, "version") ?? ownerArguments.ref?.version;
34452
34917
  if (resolved.artifact.kind === "file" || resolved.artifact.kind === "image") {
34453
- return readUploadedFile(client, resolved, version2);
34918
+ const { artifact } = resolved;
34919
+ return readUploadedFile(client, artifact, resolved.owner.kind === "project" ? {
34920
+ project: artifact.project,
34921
+ document: documentLabel(artifact.project, artifact.slug)
34922
+ } : { issue: resolved.issue?.key }, version2, resolved.owner.kind === "project" ? resolved.owner.project : resolved.owner.issue, shown);
34454
34923
  }
34455
34924
  const documentPromise = client.docRead(resolved.artifact.id, version2);
34456
34925
  const marksPromise = openArtifactMarks(client, resolved);
@@ -34508,7 +34977,7 @@ ${trailer.join(`
34508
34977
  const content = optionalString(args, "content");
34509
34978
  const artifactInput = content === undefined ? {
34510
34979
  name,
34511
- file: Bun.file(resolvePath(input.cwd, stringArg(args, "path"))),
34980
+ file: Bun.file(resolvePath2(input.cwd, stringArg(args, "path"))),
34512
34981
  ...summary === undefined ? {} : { summary },
34513
34982
  actor
34514
34983
  } : {
@@ -34561,18 +35030,29 @@ ${trailer.join(`
34561
35030
  }
34562
35031
  case "dispatch_read": {
34563
35032
  const message = optionalString(args, "message");
35033
+ const picturesOf = (texts) => readPictures(client, picturesNewestFirst(texts), shown);
35034
+ const showImages = (pictures3) => {
35035
+ for (const address of pictures3.shown)
35036
+ shown?.add(address);
35037
+ return pictures3.images.length === 0 ? {} : { images: pictures3.images };
35038
+ };
34564
35039
  if (message !== undefined) {
34565
35040
  const sessionId = input.sessionId?.trim();
34566
35041
  if (!sessionId)
34567
35042
  throw new Error("host session id is required for dispatch_read({message})");
34568
35043
  const thread = await client.getMessageThread(messageIdOf(message), sessionId);
34569
35044
  const issueKey2 = thread.message.issue_key;
35045
+ const [pictures3, graph2] = await Promise.all([
35046
+ picturesOf(datedBodies([thread.message, ...thread.replies])),
35047
+ issueKey2 === null ? [] : graphSections(client, dispatchChildRef(dispatchIssueRef(issueKey2), "message", thread.message.id))
35048
+ ]);
34570
35049
  return {
34571
- text: messageSummary(thread, issueKey2 === null ? [] : await graphSections(client, dispatchChildRef(dispatchIssueRef(issueKey2), "message", thread.message.id))),
35050
+ text: messageSummary(thread, [...pictures3.lines, ...graph2]),
34572
35051
  details: {
34573
35052
  message: thread.message.id,
34574
35053
  ...issueKey2 === null ? {} : { issue: issueKey2 }
34575
- }
35054
+ },
35055
+ ...showImages(pictures3)
34576
35056
  };
34577
35057
  }
34578
35058
  if (ownerArguments.ref?.kind === "ask") {
@@ -34580,9 +35060,14 @@ ${trailer.join(`
34580
35060
  const id = await resolveIdPrefix(input.tool, "ask", ref.id, refOwnerName(ref), async () => ref.owner.kind === "issue" ? client.listIssueAsks(ref.owner.issue) : client.getArtifactAsks((await resolveDocument(ref.owner, ref.artifact)).artifact.id, "all"));
34581
35061
  const askRead = await client.getAsk(id);
34582
35062
  const askRef = refTarget(ref, "ask", id);
35063
+ const [pictures3, graph2] = await Promise.all([
35064
+ picturesOf(askTexts(askRead)),
35065
+ graphSections(client, askRef)
35066
+ ]);
34583
35067
  return {
34584
- text: askSummary(askRead, await graphSections(client, askRef)),
34585
- details: ref.owner.kind === "project" ? { project: ref.owner.project } : { issue: ref.owner.issue }
35068
+ text: askSummary(askRead, [...pictures3.lines, ...graph2]),
35069
+ details: ref.owner.kind === "project" ? { project: ref.owner.project } : { issue: ref.owner.issue },
35070
+ ...showImages(pictures3)
34586
35071
  };
34587
35072
  }
34588
35073
  if (ownerArguments.ref?.kind === "comment") {
@@ -34590,9 +35075,14 @@ ${trailer.join(`
34590
35075
  const id = await resolveIdPrefix(input.tool, "comment", ref.id, refOwnerName(ref), async () => ref.owner.kind === "issue" ? client.getComments(ref.owner.issue) : client.getArtifactComments((await resolveDocument(ref.owner, ref.artifact)).artifact.id));
34591
35076
  const comment = await client.getComment(id);
34592
35077
  const commentRef = refTarget(ref, "comment", id);
35078
+ const [pictures3, graph2] = await Promise.all([
35079
+ picturesOf(datedBodies([comment.comment, ...comment.replies])),
35080
+ graphSections(client, commentRef)
35081
+ ]);
34593
35082
  return {
34594
- text: commentSummary(comment, await graphSections(client, commentRef)),
34595
- details: ref.owner.kind === "project" ? { project: ref.owner.project } : { issue: comment.comment.issue_key }
35083
+ text: commentSummary(comment, [...pictures3.lines, ...graph2]),
35084
+ details: ref.owner.kind === "project" ? { project: ref.owner.project } : { issue: comment.comment.issue_key },
35085
+ ...showImages(pictures3)
34596
35086
  };
34597
35087
  }
34598
35088
  if (ownerArguments.ref?.kind === "message") {
@@ -34601,9 +35091,14 @@ ${trailer.join(`
34601
35091
  }
34602
35092
  const messageRead = await client.getMessage(ownerArguments.ref.owner.issue, ownerArguments.ref.id);
34603
35093
  const messageRef = dispatchChildRef(dispatchIssueRef(ownerArguments.ref.owner.issue), "message", ownerArguments.ref.id);
35094
+ const [pictures3, graph2] = await Promise.all([
35095
+ picturesOf(datedBodies([messageRead.message, ...messageRead.replies])),
35096
+ graphSections(client, messageRef)
35097
+ ]);
34604
35098
  return {
34605
- text: messageSummary(messageRead, await graphSections(client, messageRef)),
34606
- details: { issue: messageRead.message.issue_key }
35099
+ text: messageSummary(messageRead, [...pictures3.lines, ...graph2]),
35100
+ details: { issue: messageRead.message.issue_key },
35101
+ ...showImages(pictures3)
34607
35102
  };
34608
35103
  }
34609
35104
  if (documentOwner().kind === "project") {
@@ -34627,9 +35122,12 @@ ${trailer.join(`
34627
35122
  const issueKey = issue2();
34628
35123
  if (ownerArguments.ref?.kind === "log") {
34629
35124
  const read2 = await client.read(issueKey);
35125
+ const pictures3 = await picturesOf(read2.events.flatMap(eventTexts));
34630
35126
  return {
34631
- text: logSummary(read2.issue, read2.events),
34632
- details: { issue: read2.issue.key }
35127
+ text: [logSummary(read2.issue, read2.events), ...pictures3.lines].join(`
35128
+ `),
35129
+ details: { issue: read2.issue.key },
35130
+ ...showImages(pictures3)
34633
35131
  };
34634
35132
  }
34635
35133
  if (ownerArguments.ref?.kind === "children") {
@@ -34642,14 +35140,19 @@ ${trailer.join(`
34642
35140
  const readPromise = client.read(issueKey);
34643
35141
  const referencesPromise = issueReferencesOrUnavailable(client, issueKey);
34644
35142
  const read = await readPromise;
34645
- const [references, graph, titles] = await Promise.all([
35143
+ const [references, graph, titles, pictures2] = await Promise.all([
34646
35144
  referencesPromise,
34647
35145
  graphSections(client, dispatchIssueRef(read.issue.key)),
34648
- liveSessionTitles(client, holdsSession(read.issue.claim) || routeHeldBySession(read.issue))
35146
+ liveSessionTitles(client, holdsSession(read.issue.claim) || routeHeldBySession(read.issue)),
35147
+ picturesOf([
35148
+ ...read.issue.open_asks.map((ask) => ({ text: ask.question, at: ask.created_at })),
35149
+ ...read.events.flatMap(eventTexts)
35150
+ ])
34649
35151
  ]);
34650
35152
  return {
34651
- text: issueSummary(read.issue, read.events, references, graph, titles),
34652
- details: { issue: read.issue.key }
35153
+ text: issueSummary(read.issue, read.events, references, [...pictures2.lines, ...graph], titles),
35154
+ details: { issue: read.issue.key },
35155
+ ...showImages(pictures2)
34653
35156
  };
34654
35157
  }
34655
35158
  default:
@@ -35385,6 +35888,83 @@ class AgentStreamPublisher {
35385
35888
  }
35386
35889
  }
35387
35890
 
35891
+ // src/delivery-pictures.ts
35892
+ async function withDeliveredPictures(text, addresses, client, named, shown) {
35893
+ const textAlone = { content: text, shown: [] };
35894
+ if (addresses.length === 0)
35895
+ return textAlone;
35896
+ let dispatch;
35897
+ try {
35898
+ dispatch = client();
35899
+ } catch {
35900
+ return textAlone;
35901
+ }
35902
+ if (dispatch === undefined)
35903
+ return textAlone;
35904
+ const unique = [...new Set(addresses)];
35905
+ const shownEarlier = unique.some((address) => shown.has(address));
35906
+ const pictures = await readPictures(dispatch, unique, shown);
35907
+ const content = named ? `${text}
35908
+
35909
+ ${pictures.lines.join(`
35910
+ `)}` : text;
35911
+ if (pictures.images.length === 0)
35912
+ return shownEarlier ? { content, shown: [] } : textAlone;
35913
+ return {
35914
+ content: [{ type: "text", text: content }, ...imageBlocks(pictures.images)],
35915
+ shown: pictures.shown
35916
+ };
35917
+ }
35918
+ function entryMessage(entry) {
35919
+ if (typeof entry !== "object" || entry === null || !("type" in entry))
35920
+ return;
35921
+ if (entry.type === "custom_message") {
35922
+ return { role: "custom", content: "content" in entry ? entry.content : undefined };
35923
+ }
35924
+ if (entry.type !== "message" || !("message" in entry))
35925
+ return;
35926
+ const { message } = entry;
35927
+ if (typeof message !== "object" || message === null)
35928
+ return;
35929
+ return {
35930
+ role: "role" in message ? message.role : undefined,
35931
+ content: "content" in message ? message.content : undefined
35932
+ };
35933
+ }
35934
+ function transcriptPictures(entries) {
35935
+ const shown = new Set;
35936
+ for (const entry of entries) {
35937
+ const message = entryMessage(entry);
35938
+ if (message === undefined)
35939
+ continue;
35940
+ const { content } = message;
35941
+ const texts = [];
35942
+ let images = 0;
35943
+ if (typeof content === "string")
35944
+ texts.push(content);
35945
+ else if (Array.isArray(content)) {
35946
+ for (const block of content) {
35947
+ if (typeof block !== "object" || block === null || !("type" in block))
35948
+ continue;
35949
+ if (block.type === "image")
35950
+ images++;
35951
+ if (block.type === "text" && "text" in block && typeof block.text === "string") {
35952
+ texts.push(block.text);
35953
+ }
35954
+ }
35955
+ }
35956
+ for (const address of shownPictureAddresses(texts))
35957
+ shown.add(address);
35958
+ if (message.role !== "user" || images === 0)
35959
+ continue;
35960
+ const embedded = new Set(texts.flatMap(pictureAddresses));
35961
+ if (embedded.size === images)
35962
+ for (const address of embedded)
35963
+ shown.add(address);
35964
+ }
35965
+ return [...shown];
35966
+ }
35967
+
35388
35968
  // src/dispatch-first.ts
35389
35969
  function withDispatchFirst(messages, context) {
35390
35970
  let index = 0;
@@ -35742,8 +36322,8 @@ function subagentSessionCheck() {
35742
36322
  }
35743
36323
 
35744
36324
  // src/tool-result.ts
35745
- function toolSuccess(text, details = {}) {
35746
- return { content: [{ type: "text", text }], details };
36325
+ function toolSuccess(text, details = {}, images = []) {
36326
+ return { content: [{ type: "text", text }, ...imageBlocks(images)], details };
35747
36327
  }
35748
36328
  function toolFailure(error48) {
35749
36329
  return { content: [{ type: "text", text: messageFor(error48) }], details: {}, isError: true };
@@ -35760,7 +36340,8 @@ function renderEnvoyMessage(message, options, theme) {
35760
36340
  box.setIgnoreTight(true);
35761
36341
  box.addChild(new Text(theme.fg("customMessageLabel", theme.bold(message.customType)), 0, 0));
35762
36342
  box.addChild(new Spacer(1));
35763
- let text = message.content;
36343
+ let text = typeof message.content === "string" ? message.content : message.content.map((block) => block.type === "text" ? block.text : `[picture: ${block.mimeType}]`).join(`
36344
+ `);
35764
36345
  if (!options.expanded) {
35765
36346
  const lines = text.split(`
35766
36347
  `);
@@ -35805,6 +36386,11 @@ async function copySessionID(copyToClipboard2, sessionID) {
35805
36386
  // extensions/envoy.ts
35806
36387
  var codec2 = import_nats2.StringCodec();
35807
36388
  var NATS_RETRY_INTERVAL_MS = 15000;
36389
+ var DELIVERY_PICTURES_TIMEOUT_MS = 20000;
36390
+ function timedDispatchClient(timeoutMs) {
36391
+ const config2 = activeDispatchConfig(process.env, { cwd: process.cwd() });
36392
+ return config2 === null ? undefined : new DispatchClient(config2.url, config2.token, fetch, AbortSignal.timeout(timeoutMs));
36393
+ }
35808
36394
  var CAPABILITIES_WITHOUT_BTW = DELIVERY_CAPABILITIES.filter((capability) => capability !== "btw");
35809
36395
  var ClaimIdentitySchema = DedupeIdentitySchema.extend({
35810
36396
  event_id: DedupeIdentitySchema.shape.event_id.catch(undefined),
@@ -35965,12 +36551,12 @@ function envoyExtension(pi) {
35965
36551
  });
35966
36552
  };
35967
36553
  const queryOpenAsks = async (requestedSessionID, since) => {
35968
- const config2 = activeDispatchConfig(process.env, { cwd: process.cwd() });
35969
- if (config2 === null)
36554
+ const dispatch = timedDispatchClient(OPEN_ASKS_TIMEOUT_MS);
36555
+ if (dispatch === undefined)
35970
36556
  return null;
35971
- const snapshot = await new DispatchClient(config2.url, config2.token, fetch, AbortSignal.timeout(OPEN_ASKS_TIMEOUT_MS)).openAsks(requestedSessionID, since);
36557
+ const snapshot = await dispatch.openAsks(requestedSessionID, since);
35972
36558
  availabilityWarningSessionIDs.delete(requestedSessionID);
35973
- return { snapshot, url: config2.url };
36559
+ return snapshot;
35974
36560
  };
35975
36561
  const markLegionManagedSession = (targetSessionID) => {
35976
36562
  legionRoleClaimBridge().managedSessions.add(targetSessionID);
@@ -35979,7 +36565,12 @@ function envoyExtension(pi) {
35979
36565
  legionManagedTranscript = true;
35980
36566
  pi.appendEntry(LEGION_MANAGED_ENTRY, { session_id: targetSessionID });
35981
36567
  };
36568
+ const markPicturesShown = (shown, addresses) => {
36569
+ for (const address of addresses)
36570
+ shown.add(address);
36571
+ };
35982
36572
  const restoreLocalSessionState = (context) => {
36573
+ const previousSessionID = sessionID;
35983
36574
  sessionDirectory = context.cwd;
35984
36575
  sessionID = context.sessionManager.getSessionId();
35985
36576
  recordedSessionKey = recordEnvoySession({
@@ -36002,6 +36593,9 @@ function envoyExtension(pi) {
36002
36593
  }
36003
36594
  legionManagedTranscript = branch.some(isLegionManagedEntry);
36004
36595
  handledDispatchAttempts = handledAttempts(context.sessionManager.getEntries());
36596
+ markPicturesShown(shownPictures(sessionID), transcriptPictures(branch));
36597
+ if (previousSessionID !== sessionID)
36598
+ forgetShownPictures(previousSessionID);
36005
36599
  };
36006
36600
  pi.on("resources_discover", async () => ({ skillPaths: [SKILLS_DIRECTORY] }));
36007
36601
  registerEnvoyMessageRenderer(pi);
@@ -36033,10 +36627,10 @@ function envoyExtension(pi) {
36033
36627
  handledDispatchAttempts.add(key);
36034
36628
  pi.appendEntry(HANDLED_ATTEMPT_ENTRY, { attempt: delivery.attempt, message_id: delivery.id });
36035
36629
  try {
36036
- const config2 = activeDispatchConfig(process.env, { cwd: process.cwd() });
36037
- if (config2 === null)
36630
+ const dispatch = timedDispatchClient(USER_TURN_ACCEPT_TIMEOUT_MS);
36631
+ if (dispatch === undefined)
36038
36632
  return;
36039
- const accepted = await new DispatchClient(config2.url, config2.token, fetch, AbortSignal.timeout(USER_TURN_ACCEPT_TIMEOUT_MS)).acceptMessageDelivery(delivery.id, delivery.attempt, {
36633
+ const accepted = await dispatch.acceptMessageDelivery(delivery.id, delivery.attempt, {
36040
36634
  actor: { id: sessionID, kind: "session" }
36041
36635
  });
36042
36636
  const turn = turnFromAccept(accepted);
@@ -36106,14 +36700,19 @@ function envoyExtension(pi) {
36106
36700
  }
36107
36701
  } else {
36108
36702
  const turn = await acceptedUserTurn(rendered);
36703
+ const shown = shownPictures(sessionID);
36109
36704
  if (turn === undefined) {
36110
- pi.sendMessage({ customType: "envoy-message", content: rendered.content, display: true }, {
36705
+ const card = await withDeliveredPictures(rendered.content, rendered.pictures ?? [], () => timedDispatchClient(DELIVERY_PICTURES_TIMEOUT_MS), true, shown);
36706
+ pi.sendMessage({ customType: "envoy-message", content: card.content, display: true }, {
36111
36707
  deliverAs: rendered.delivery?.mode === "aside" ? "aside" : "steer",
36112
36708
  triggerTurn: true
36113
36709
  });
36710
+ markPicturesShown(shown, card.shown);
36114
36711
  } else {
36712
+ const delivery = await withDeliveredPictures(turn.body, pictureAddresses(turn.body), () => timedDispatchClient(DELIVERY_PICTURES_TIMEOUT_MS), false, shown);
36115
36713
  noteInjectedUserTurn(sessionID, turn.body, turn.messageId);
36116
- pi.sendUserMessage(turn.body, turn.mode === "aside" ? { deliverAs: "aside" } : undefined);
36714
+ pi.sendUserMessage(delivery.content, turn.mode === "aside" ? { deliverAs: "aside" } : undefined);
36715
+ markPicturesShown(shown, delivery.shown);
36117
36716
  }
36118
36717
  }
36119
36718
  } catch (error48) {
@@ -36603,7 +37202,7 @@ function envoyExtension(pi) {
36603
37202
  pi.on("session_switch", (event, context) => rebind(event.reason, context));
36604
37203
  pi.on("session_branch", (_event, context) => rebind(undefined, context));
36605
37204
  pi.on("session_tree", (_event, context) => rebind(undefined, context));
36606
- pi.on("session_shutdown", async () => {
37205
+ pi.on("session_shutdown", async (_event, context) => {
36607
37206
  shuttingDown = true;
36608
37207
  const bound = bridge.instances.indexOf(claimInstance);
36609
37208
  if (bound !== -1)
@@ -36637,6 +37236,10 @@ function envoyExtension(pi) {
36637
37236
  awaitingRetry.clear();
36638
37237
  agentStreamControl = undefined;
36639
37238
  agentStreamControlSession = "";
37239
+ const liveSessionID = context.sessionManager.getSessionId();
37240
+ forgetShownPictures(sessionID);
37241
+ if (liveSessionID !== sessionID)
37242
+ forgetShownPictures(liveSessionID);
36640
37243
  }
36641
37244
  });
36642
37245
  for (const spec of envoyToolSpecs) {
@@ -36670,7 +37273,7 @@ function envoyExtension(pi) {
36670
37273
  signal,
36671
37274
  env: process.env
36672
37275
  });
36673
- return toolSuccess(result.text, result.details);
37276
+ return toolSuccess(result.text, result.details, result.images);
36674
37277
  } catch (error48) {
36675
37278
  return toolFailure(error48);
36676
37279
  }
@@ -36695,7 +37298,7 @@ function envoyExtension(pi) {
36695
37298
  try {
36696
37299
  const open = await queryOpenAsks(id);
36697
37300
  if (open !== null)
36698
- armAskAwareness(id, open.snapshot.as_of);
37301
+ armAskAwareness(id, open.as_of);
36699
37302
  } catch (error48) {
36700
37303
  warnAskAvailability(context, error48);
36701
37304
  }
@@ -36766,7 +37369,7 @@ function envoyExtension(pi) {
36766
37369
  const abort = new AbortController;
36767
37370
  askCheckAbort = abort;
36768
37371
  const answered = ask({
36769
- prompt: ASK_SELF_CHECK_PROMPT(open.snapshot.asks),
37372
+ prompt: ASK_SELF_CHECK_PROMPT(open.asks),
36770
37373
  signal: abort.signal
36771
37374
  }).then((reply) => reply.replyText, (error48) => {
36772
37375
  if (!abort.signal.aborted)
@@ -36797,7 +37400,7 @@ function envoyExtension(pi) {
36797
37400
  ...askAwareness,
36798
37401
  check_due: superseded,
36799
37402
  checks: askAwareness.checks + 1,
36800
- baseline_as_of: open.snapshot.as_of
37403
+ baseline_as_of: open.as_of
36801
37404
  };
36802
37405
  if (superseded)
36803
37406
  return;