@sjawhar/pi-legion-envoy 5.30.4 → 5.30.6

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
@@ -30366,7 +30366,7 @@ var dispatchToolSpecs = [
30366
30366
  {
30367
30367
  name: "dispatch_read",
30368
30368
  example: { issue: "DSP-1" },
30369
- 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). " + "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,
30369
+ 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) or `INTERNAL`. " + "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,
30370
30370
  arguments: (z2) => ({
30371
30371
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
30372
30372
  project: z2.string().describe("Project key owning the document.").optional(),
@@ -32463,6 +32463,7 @@ class DispatchServiceError extends Error {
32463
32463
  current;
32464
32464
  mismatches;
32465
32465
  name = "DispatchServiceError";
32466
+ fromDispatch = true;
32466
32467
  constructor(code, status, message, candidates, current, mismatches) {
32467
32468
  super(message);
32468
32469
  this.code = code;
@@ -32472,6 +32473,21 @@ class DispatchServiceError extends Error {
32472
32473
  this.mismatches = mismatches;
32473
32474
  }
32474
32475
  }
32476
+
32477
+ class DispatchGatewayError extends DispatchServiceError {
32478
+ answer;
32479
+ advice;
32480
+ fromDispatch = false;
32481
+ transient;
32482
+ mayHaveReachedDispatch;
32483
+ constructor(status, answer, advice, message = `${answer}, so ${advice}.`) {
32484
+ super(`HTTP_${status}`, status, message);
32485
+ this.answer = answer;
32486
+ this.advice = advice;
32487
+ this.transient = transientStatus(status);
32488
+ this.mayHaveReachedDispatch = reachesDispatch(status);
32489
+ }
32490
+ }
32475
32491
  function asErrorShape(value) {
32476
32492
  return typeof value === "object" && value !== null ? value : {};
32477
32493
  }
@@ -32485,7 +32501,7 @@ function requestSignal(signal) {
32485
32501
  }
32486
32502
  function whyNotAPage(answer) {
32487
32503
  if (typeof answer === "string") {
32488
- return "text that is not a JSON page, which looks like a proxy or gateway page rather than " + "Dispatch's own answer, so a retry may succeed.";
32504
+ return `text that is not a JSON page, ${GATEWAY_PAGE}, so a retry may succeed.`;
32489
32505
  }
32490
32506
  let what;
32491
32507
  if (Array.isArray(answer)) {
@@ -32504,6 +32520,53 @@ function whyNotAPage(answer) {
32504
32520
  }
32505
32521
  return `${what}. Retrying will not help: the same request gets the same answer until that ` + "Dispatch is upgraded or fixed.";
32506
32522
  }
32523
+ var GATEWAY_PAGE = "which looks like a proxy or gateway page rather than Dispatch's own answer";
32524
+ function gatewayAdvice(method, status) {
32525
+ if (!transientStatus(status)) {
32526
+ return "a retry gets the same answer until the Dispatch URL, or whatever answers in its place, is fixed";
32527
+ }
32528
+ if (method === "GET")
32529
+ return "a retry may succeed";
32530
+ return reachesDispatch(status) ? "the write may or may not have reached Dispatch: check whether it took effect before retrying it" : "the write did not reach Dispatch, and a retry may succeed";
32531
+ }
32532
+ function transientStatus(status) {
32533
+ return status >= 500 || status === 408 || status === 429;
32534
+ }
32535
+ function reachesDispatch(status) {
32536
+ return status >= 500;
32537
+ }
32538
+ var REDACTED = "[redacted]";
32539
+ var EXCERPT_SCAN_LIMIT = 64 * 1024;
32540
+ var BEARER_PIECE = 8;
32541
+ function redactBearer(text, bearer) {
32542
+ if (bearer === "")
32543
+ return text.slice(0, EXCERPT_SCAN_LIMIT);
32544
+ const length = Math.min(BEARER_PIECE, bearer.length);
32545
+ const scanned = text.slice(0, EXCERPT_SCAN_LIMIT + length - 1);
32546
+ const covered = new Uint8Array(scanned.length);
32547
+ const pieces = new Set;
32548
+ for (let at = 0;at + length <= bearer.length; at++)
32549
+ pieces.add(bearer.slice(at, at + length));
32550
+ for (const piece of pieces) {
32551
+ let found = scanned.indexOf(piece);
32552
+ while (found !== -1) {
32553
+ covered.fill(1, found, found + length);
32554
+ found = scanned.indexOf(piece, found + 1);
32555
+ }
32556
+ }
32557
+ let redacted = "";
32558
+ let kept = 0;
32559
+ for (let start = covered.indexOf(1);start !== -1; start = covered.indexOf(1, kept)) {
32560
+ redacted += `${scanned.slice(kept, start)}${REDACTED}`;
32561
+ const end = covered.indexOf(0, start);
32562
+ kept = end === -1 ? scanned.length : end;
32563
+ }
32564
+ return `${redacted}${scanned.slice(kept, EXCERPT_SCAN_LIMIT)}`.slice(0, EXCERPT_SCAN_LIMIT);
32565
+ }
32566
+ function excerpt(text, token) {
32567
+ const plain = redactBearer(text, token.trim()).replace(/<(script|style)\b[^<>]*>(?:[\s\S]*?<\/\1\s*>|[\s\S]*$)/gi, " ").replace(/<[^<>]*>/g, " ");
32568
+ return textHead(plain.replace(/(\bauthorization["']?\s*[:=]\s*["']?(?:(?:bearer|basic|digest|token)\s+)?)[^\s"'<>,;]+/gi, `$1${REDACTED}`).replace(/(\bbearer\s+)[^\s"'<>,;]+/gi, `$1${REDACTED}`));
32569
+ }
32507
32570
 
32508
32571
  class DispatchClient {
32509
32572
  token;
@@ -32761,22 +32824,24 @@ class DispatchClient {
32761
32824
  };
32762
32825
  if (body !== undefined)
32763
32826
  headers["Content-Type"] = "application/json";
32764
- const response = await this.fetchImpl(this.#url(path2, query), {
32827
+ const url2 = this.#url(path2, query);
32828
+ const response = await this.fetchImpl(url2, {
32765
32829
  method,
32766
32830
  headers,
32767
32831
  signal: this.#signal,
32768
32832
  ...body === undefined ? {} : { body: JSON.stringify(body) }
32769
32833
  });
32770
- return this.#response(response);
32834
+ return this.#response(method, url2, response);
32771
32835
  }
32772
32836
  async#form(method, path2, body) {
32773
- const response = await this.fetchImpl(this.#url(path2), {
32837
+ const url2 = this.#url(path2);
32838
+ const response = await this.fetchImpl(url2, {
32774
32839
  method,
32775
32840
  headers: { Accept: "application/json", Authorization: `Bearer ${this.token}` },
32776
32841
  body,
32777
32842
  signal: this.#signal
32778
32843
  });
32779
- return this.#response(response);
32844
+ return this.#response(method, url2, response);
32780
32845
  }
32781
32846
  #url(path2, query) {
32782
32847
  const url2 = new URL(`${path2.map((segment) => encodeURIComponent(segment)).join("/")}`, `${this.#baseUrl}/`);
@@ -32792,7 +32857,7 @@ class DispatchClient {
32792
32857
  }
32793
32858
  return url2.toString();
32794
32859
  }
32795
- async#response(response) {
32860
+ async#response(method, url2, response) {
32796
32861
  const text = await response.text();
32797
32862
  let payload = text;
32798
32863
  if (text && isJson(response)) {
@@ -32804,7 +32869,23 @@ class DispatchClient {
32804
32869
  }
32805
32870
  if (!response.ok) {
32806
32871
  const error48 = asErrorShape(payload);
32807
- throw new DispatchServiceError(error48.code ?? `HTTP_${response.status}`, response.status, error48.error ?? (typeof payload === "string" && payload ? payload : response.statusText), error48.candidates, error48.current, error48.mismatches);
32872
+ if (typeof error48.error === "string") {
32873
+ throw new DispatchServiceError(error48.code ?? `HTTP_${response.status}`, response.status, error48.error, error48.candidates, error48.current, error48.mismatches);
32874
+ }
32875
+ const quoted = excerpt(text, this.token);
32876
+ let body;
32877
+ if (quoted !== "") {
32878
+ body = `a body that is not Dispatch's error JSON (${JSON.stringify(quoted)})`;
32879
+ } else if (text === "") {
32880
+ body = "an empty body";
32881
+ } else {
32882
+ const within = text.length > EXCERPT_SCAN_LIMIT ? ` in its first ${EXCERPT_SCAN_LIMIT / 1024} KiB` : "";
32883
+ const size = Buffer.byteLength(text).toLocaleString("en-US");
32884
+ body = `a body of ${size} bytes and no readable text${within}`;
32885
+ }
32886
+ const reasonPhrase = excerpt(response.statusText, this.token);
32887
+ const reason = reasonPhrase === "" ? "" : ` ${reasonPhrase}`;
32888
+ throw new DispatchGatewayError(response.status, `${method} ${url2} answered ${response.status}${reason} with ${body}, ${GATEWAY_PAGE}`, gatewayAdvice(method, response.status));
32808
32889
  }
32809
32890
  return payload;
32810
32891
  }
@@ -33524,7 +33605,7 @@ async function resolveArtifact(client, owner, artifactReference, { canonical = f
33524
33605
  throw new Error("artifact is required for a project document");
33525
33606
  }
33526
33607
  const routed = await client.getProjectArtifact(owner.project, artifactReference).catch((error48) => {
33527
- if (!(error48 instanceof DispatchServiceError) || error48.status !== 404)
33608
+ if (!dispatchAnswered(error48, 404))
33528
33609
  throw error48;
33529
33610
  return;
33530
33611
  });
@@ -33695,12 +33776,12 @@ function referenceLines(edges) {
33695
33776
  if (edges.length === 0)
33696
33777
  return ["- none"];
33697
33778
  return edges.map((edge) => {
33698
- const excerpt = edge.excerpt === undefined ? "" : `${textHead(edge.excerpt.text)} \xB7 `;
33699
- return `- ${edge.kind} ${edge.node.kind} ${edge.node.ref ?? edge.node.id} (${excerpt}${edge.created_at})`;
33779
+ const excerpt2 = edge.excerpt === undefined ? "" : `${textHead(edge.excerpt.text)} \xB7 `;
33780
+ return `- ${edge.kind} ${edge.node.kind} ${edge.node.ref ?? edge.node.id} (${excerpt2}${edge.created_at})`;
33700
33781
  });
33701
33782
  }
33702
33783
  function unavailableReason(error48) {
33703
- return error48 instanceof DispatchServiceError && error48.status === 404 ? "unavailable" : `unavailable: ${messageFor(error48)}`;
33784
+ return dispatchAnswered(error48, 404) ? "unavailable" : `unavailable: ${messageFor(error48)}`;
33704
33785
  }
33705
33786
  async function graphEdges(client, query) {
33706
33787
  try {
@@ -33787,6 +33868,7 @@ function askSummary({ ask, replies }, graph) {
33787
33868
  ]);
33788
33869
  return [
33789
33870
  `Question: ${ask.question}`,
33871
+ ...anchorLines(ask),
33790
33872
  "Options:",
33791
33873
  ...ask.options.length === 0 ? ["- none"] : ask.options.map((option) => `- ${option.label}${option.description ? ` \u2014 ${option.description}` : ""}`),
33792
33874
  `State: ${ask.state}`,
@@ -33850,12 +33932,12 @@ function formatOpenAsksSummary(response, baseUrl) {
33850
33932
  function commentSummary({ comment, replies }, graph) {
33851
33933
  const root = [
33852
33934
  `${comment.id} \xB7 ${actorText(comment.author)}`,
33853
- ...comment.anchor?.quote === undefined ? [] : [`> ${comment.anchor.quote}`],
33935
+ ...anchorLines(comment),
33854
33936
  `Body: ${comment.body}`
33855
33937
  ];
33856
33938
  const chain = replies.flatMap((reply) => [
33857
33939
  `${reply.id} \xB7 ${actorText(reply.author)}`,
33858
- ...reply.anchor?.quote === undefined ? [] : [`> ${reply.anchor.quote}`],
33940
+ ...anchorLines(reply),
33859
33941
  `Body: ${reply.body}`
33860
33942
  ]);
33861
33943
  return [
@@ -33867,6 +33949,28 @@ function commentSummary({ comment, replies }, graph) {
33867
33949
  ].join(`
33868
33950
  `);
33869
33951
  }
33952
+ function anchorLines(record2) {
33953
+ return [
33954
+ ...record2.anchor?.quote === undefined ? [] : [`> ${record2.anchor.quote}`],
33955
+ ...record2.anchor_block === undefined ? [] : [`Position: ${positionText(record2.anchor_block)}`],
33956
+ ...record2.anchor_block_error === undefined ? [] : [`Position: unavailable (${record2.anchor_block_error})`]
33957
+ ];
33958
+ }
33959
+ function positionText(block) {
33960
+ const { table, path: path2 } = block;
33961
+ const segments = path2.map((entry) => `${entry.type}[${entry.index}]`);
33962
+ if (table === undefined || table.row === null)
33963
+ return segments.join(" \u203A ");
33964
+ const tableAt = path2.findIndex((entry) => entry.type === "table");
33965
+ const cells = table.cells ?? [];
33966
+ const label = (table.column === null ? cells : cells.slice(0, table.column)).map((cell) => cell.trim()).filter((cell) => cell !== "" && !/^\d+$/.test(cell)).join(" \xB7 ");
33967
+ const row = label === "" ? `row ${table.row}` : `row ${table.row} (${label})`;
33968
+ const header = table.header === null || table.header === "" ? String(table.column) : table.header;
33969
+ return [
33970
+ ...segments.slice(0, tableAt + 1),
33971
+ table.column === null ? row : `${row}, column ${header}`
33972
+ ].join(" \u203A ");
33973
+ }
33870
33974
  function messageSummary({ message, replies }, graph) {
33871
33975
  const root = [`${message.id} \xB7 ${actorText(message.author)}`, `Body: ${message.body}`];
33872
33976
  const chain = replies.flatMap((reply) => [
@@ -33890,9 +33994,8 @@ async function openArtifactMarks(client, resolved) {
33890
33994
  const marks = asks.filter((ask) => ask.state === "open" && ask.anchor?.artifact_id === resolved.artifact.id).map((ask) => `ask ${ask.id}`);
33891
33995
  const commentsResult = await commentsResultPromise;
33892
33996
  if (commentsResult.status === "rejected") {
33893
- if (commentsResult.reason instanceof DispatchServiceError && commentsResult.reason.status === 404) {
33997
+ if (dispatchAnswered(commentsResult.reason, 404))
33894
33998
  return marks;
33895
- }
33896
33999
  throw commentsResult.reason;
33897
34000
  }
33898
34001
  return [
@@ -33979,10 +34082,30 @@ async function refuseRemovingOpenDecisionBlocks(client, tool, resolved, ops) {
33979
34082
  `));
33980
34083
  }
33981
34084
  function refusalWithCode(error48, suffix = "") {
34085
+ if (error48 instanceof DispatchGatewayError) {
34086
+ let told = error48.message;
34087
+ if (suffix !== "") {
34088
+ told = error48.mayHaveReachedDispatch ? `${error48.answer}${suffix}` : `${error48.answer}, so ${error48.advice}${suffix}`;
34089
+ }
34090
+ return new DispatchGatewayError(error48.status, error48.answer, error48.advice, `${error48.code}: ${told}`);
34091
+ }
33982
34092
  if (!(error48 instanceof DispatchServiceError))
33983
34093
  return error48;
33984
34094
  return new DispatchServiceError(error48.code, error48.status, `${error48.code}: ${error48.message}${suffix}`, error48.candidates, error48.current, error48.mismatches);
33985
34095
  }
34096
+ function dispatchAnswered(error48, status) {
34097
+ return error48 instanceof DispatchServiceError && error48.fromDispatch && error48.status === status;
34098
+ }
34099
+ function writeMayHaveLanded(error48) {
34100
+ if (error48 instanceof DispatchGatewayError)
34101
+ return error48.mayHaveReachedDispatch;
34102
+ return !(error48 instanceof DispatchServiceError) || error48.status >= 500;
34103
+ }
34104
+ function withAccount(error48, account) {
34105
+ const message = messageFor(error48);
34106
+ const told = /[.!?]$/.test(message) ? `${message} ${account.charAt(0).toUpperCase()}${account.slice(1)}` : `${message}; ${account}`;
34107
+ return new Error(told, { cause: error48 });
34108
+ }
33986
34109
  async function executeDispatchTool(input) {
33987
34110
  const configUrl = input.config.url;
33988
34111
  const configToken = input.config.token;
@@ -34152,7 +34275,10 @@ async function executeDispatchTool(input) {
34152
34275
  ref: dispatchChildRef(dispatchIssueRef(issueKey), "message", message.id)
34153
34276
  };
34154
34277
  } catch (error48) {
34155
- throw refusalWithCode(error48, "; the reason was not posted, so the close was not sent");
34278
+ const told = writeMayHaveLanded(error48) ? "the reason may or may not have been posted, and the close was not sent: read the issue's messages before retrying, since retrying this call posts its reason again" : "the reason was not posted, so the close was not sent";
34279
+ if (error48 instanceof DispatchServiceError)
34280
+ throw refusalWithCode(error48, `; ${told}`);
34281
+ throw withAccount(error48, told);
34156
34282
  }
34157
34283
  }
34158
34284
  const linked = before.external_links.map((link) => link.url);
@@ -34171,17 +34297,15 @@ async function executeDispatchTool(input) {
34171
34297
  actor
34172
34298
  });
34173
34299
  } catch (error48) {
34174
- const taken = error48 instanceof DispatchServiceError && error48.status === 500 && newLinks.length > 0 ? `; one of ${newLinks.join(", ")} may already be linked from another issue (a URL links exactly one issue)` : "";
34300
+ const taken = dispatchAnswered(error48, 500) && newLinks.length > 0 ? `; one of ${newLinks.join(", ")} may already be linked from another issue (a URL links exactly one issue)` : "";
34175
34301
  if (closingNote === undefined)
34176
34302
  throw refusalWithCode(error48, taken);
34177
- const refused = error48 instanceof DispatchServiceError && error48.status < 500;
34178
- const posted = `; the reason already landed as message ${closingNote.id} (${closingNote.ref})`;
34179
- const landed = refused ? `${posted} but the issue did not close. Retrying this call posts its reason again, so fix what refused the close, then retry with a reason that points at message ${closingNote.id}` : `${posted}, and the close may or may not have taken effect. Read the issue's status before retrying: done means it closed; otherwise retry with a reason that points at message ${closingNote.id}, since retrying this call posts its reason again`;
34303
+ const posted = `the reason already landed as message ${closingNote.id} (${closingNote.ref})`;
34304
+ const fix = error48 instanceof DispatchGatewayError && error48.transient ? "" : "fix what refused the close, then ";
34305
+ const landed = writeMayHaveLanded(error48) ? `${posted}, and the close may or may not have taken effect. Read the issue's status before retrying: done means it closed; otherwise retry with a reason that points at message ${closingNote.id}, since retrying this call posts its reason again` : `${posted} but the issue did not close. Retrying this call posts its reason again, so ${fix}retry with a reason that points at message ${closingNote.id}`;
34180
34306
  if (error48 instanceof DispatchServiceError)
34181
- throw refusalWithCode(error48, taken + landed);
34182
- throw new Error(`${error48 instanceof Error ? error48.message : String(error48)}${landed}`, {
34183
- cause: error48
34184
- });
34307
+ throw refusalWithCode(error48, `${taken}; ${landed}`);
34308
+ throw withAccount(error48, landed);
34185
34309
  }
34186
34310
  const linkCount = `(${after.external_links.length} ${after.external_links.length === 1 ? "link" : "links"})`;
34187
34311
  const changes = [
@@ -34785,7 +34909,7 @@ async function resolveExistingIssue(client, issueReference) {
34785
34909
  try {
34786
34910
  return await client.resolveIssue(issueReference);
34787
34911
  } catch (error48) {
34788
- if (error48 instanceof DispatchServiceError && error48.status === 404) {
34912
+ if (dispatchAnswered(error48, 404)) {
34789
34913
  throw new Error(`no Dispatch issue is linked to ${issueReference}; create it first with ` + `dispatch_issue({ external: "${issueReference}", ... })`);
34790
34914
  }
34791
34915
  throw error48;
package/dist/legion.js CHANGED
@@ -30535,7 +30535,7 @@ var dispatchToolSpecs = [
30535
30535
  {
30536
30536
  name: "dispatch_read",
30537
30537
  example: { issue: "DSP-1" },
30538
- 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). " + "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,
30538
+ 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) or `INTERNAL`. " + "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,
30539
30539
  arguments: (z2) => ({
30540
30540
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
30541
30541
  project: z2.string().describe("Project key owning the document.").optional(),
@@ -31308,6 +31308,7 @@ class DispatchServiceError extends Error {
31308
31308
  current;
31309
31309
  mismatches;
31310
31310
  name = "DispatchServiceError";
31311
+ fromDispatch = true;
31311
31312
  constructor(code, status, message, candidates, current, mismatches) {
31312
31313
  super(message);
31313
31314
  this.code = code;
@@ -31317,6 +31318,21 @@ class DispatchServiceError extends Error {
31317
31318
  this.mismatches = mismatches;
31318
31319
  }
31319
31320
  }
31321
+
31322
+ class DispatchGatewayError extends DispatchServiceError {
31323
+ answer;
31324
+ advice;
31325
+ fromDispatch = false;
31326
+ transient;
31327
+ mayHaveReachedDispatch;
31328
+ constructor(status, answer, advice, message = `${answer}, so ${advice}.`) {
31329
+ super(`HTTP_${status}`, status, message);
31330
+ this.answer = answer;
31331
+ this.advice = advice;
31332
+ this.transient = transientStatus(status);
31333
+ this.mayHaveReachedDispatch = reachesDispatch(status);
31334
+ }
31335
+ }
31320
31336
  function asErrorShape(value) {
31321
31337
  return typeof value === "object" && value !== null ? value : {};
31322
31338
  }
@@ -31330,7 +31346,7 @@ function requestSignal(signal) {
31330
31346
  }
31331
31347
  function whyNotAPage(answer) {
31332
31348
  if (typeof answer === "string") {
31333
- return "text that is not a JSON page, which looks like a proxy or gateway page rather than " + "Dispatch's own answer, so a retry may succeed.";
31349
+ return `text that is not a JSON page, ${GATEWAY_PAGE}, so a retry may succeed.`;
31334
31350
  }
31335
31351
  let what;
31336
31352
  if (Array.isArray(answer)) {
@@ -31349,6 +31365,53 @@ function whyNotAPage(answer) {
31349
31365
  }
31350
31366
  return `${what}. Retrying will not help: the same request gets the same answer until that ` + "Dispatch is upgraded or fixed.";
31351
31367
  }
31368
+ var GATEWAY_PAGE = "which looks like a proxy or gateway page rather than Dispatch's own answer";
31369
+ function gatewayAdvice(method, status) {
31370
+ if (!transientStatus(status)) {
31371
+ return "a retry gets the same answer until the Dispatch URL, or whatever answers in its place, is fixed";
31372
+ }
31373
+ if (method === "GET")
31374
+ return "a retry may succeed";
31375
+ return reachesDispatch(status) ? "the write may or may not have reached Dispatch: check whether it took effect before retrying it" : "the write did not reach Dispatch, and a retry may succeed";
31376
+ }
31377
+ function transientStatus(status) {
31378
+ return status >= 500 || status === 408 || status === 429;
31379
+ }
31380
+ function reachesDispatch(status) {
31381
+ return status >= 500;
31382
+ }
31383
+ var REDACTED = "[redacted]";
31384
+ var EXCERPT_SCAN_LIMIT = 64 * 1024;
31385
+ var BEARER_PIECE = 8;
31386
+ function redactBearer(text, bearer) {
31387
+ if (bearer === "")
31388
+ return text.slice(0, EXCERPT_SCAN_LIMIT);
31389
+ const length = Math.min(BEARER_PIECE, bearer.length);
31390
+ const scanned = text.slice(0, EXCERPT_SCAN_LIMIT + length - 1);
31391
+ const covered = new Uint8Array(scanned.length);
31392
+ const pieces = new Set;
31393
+ for (let at = 0;at + length <= bearer.length; at++)
31394
+ pieces.add(bearer.slice(at, at + length));
31395
+ for (const piece of pieces) {
31396
+ let found = scanned.indexOf(piece);
31397
+ while (found !== -1) {
31398
+ covered.fill(1, found, found + length);
31399
+ found = scanned.indexOf(piece, found + 1);
31400
+ }
31401
+ }
31402
+ let redacted = "";
31403
+ let kept = 0;
31404
+ for (let start = covered.indexOf(1);start !== -1; start = covered.indexOf(1, kept)) {
31405
+ redacted += `${scanned.slice(kept, start)}${REDACTED}`;
31406
+ const end = covered.indexOf(0, start);
31407
+ kept = end === -1 ? scanned.length : end;
31408
+ }
31409
+ return `${redacted}${scanned.slice(kept, EXCERPT_SCAN_LIMIT)}`.slice(0, EXCERPT_SCAN_LIMIT);
31410
+ }
31411
+ function excerpt(text, token) {
31412
+ const plain = redactBearer(text, token.trim()).replace(/<(script|style)\b[^<>]*>(?:[\s\S]*?<\/\1\s*>|[\s\S]*$)/gi, " ").replace(/<[^<>]*>/g, " ");
31413
+ return textHead(plain.replace(/(\bauthorization["']?\s*[:=]\s*["']?(?:(?:bearer|basic|digest|token)\s+)?)[^\s"'<>,;]+/gi, `$1${REDACTED}`).replace(/(\bbearer\s+)[^\s"'<>,;]+/gi, `$1${REDACTED}`));
31414
+ }
31352
31415
 
31353
31416
  class DispatchClient {
31354
31417
  token;
@@ -31606,22 +31669,24 @@ class DispatchClient {
31606
31669
  };
31607
31670
  if (body !== undefined)
31608
31671
  headers["Content-Type"] = "application/json";
31609
- const response = await this.fetchImpl(this.#url(path2, query), {
31672
+ const url2 = this.#url(path2, query);
31673
+ const response = await this.fetchImpl(url2, {
31610
31674
  method,
31611
31675
  headers,
31612
31676
  signal: this.#signal,
31613
31677
  ...body === undefined ? {} : { body: JSON.stringify(body) }
31614
31678
  });
31615
- return this.#response(response);
31679
+ return this.#response(method, url2, response);
31616
31680
  }
31617
31681
  async#form(method, path2, body) {
31618
- const response = await this.fetchImpl(this.#url(path2), {
31682
+ const url2 = this.#url(path2);
31683
+ const response = await this.fetchImpl(url2, {
31619
31684
  method,
31620
31685
  headers: { Accept: "application/json", Authorization: `Bearer ${this.token}` },
31621
31686
  body,
31622
31687
  signal: this.#signal
31623
31688
  });
31624
- return this.#response(response);
31689
+ return this.#response(method, url2, response);
31625
31690
  }
31626
31691
  #url(path2, query) {
31627
31692
  const url2 = new URL(`${path2.map((segment) => encodeURIComponent(segment)).join("/")}`, `${this.#baseUrl}/`);
@@ -31637,7 +31702,7 @@ class DispatchClient {
31637
31702
  }
31638
31703
  return url2.toString();
31639
31704
  }
31640
- async#response(response) {
31705
+ async#response(method, url2, response) {
31641
31706
  const text = await response.text();
31642
31707
  let payload = text;
31643
31708
  if (text && isJson(response)) {
@@ -31649,7 +31714,23 @@ class DispatchClient {
31649
31714
  }
31650
31715
  if (!response.ok) {
31651
31716
  const error48 = asErrorShape(payload);
31652
- throw new DispatchServiceError(error48.code ?? `HTTP_${response.status}`, response.status, error48.error ?? (typeof payload === "string" && payload ? payload : response.statusText), error48.candidates, error48.current, error48.mismatches);
31717
+ if (typeof error48.error === "string") {
31718
+ throw new DispatchServiceError(error48.code ?? `HTTP_${response.status}`, response.status, error48.error, error48.candidates, error48.current, error48.mismatches);
31719
+ }
31720
+ const quoted = excerpt(text, this.token);
31721
+ let body;
31722
+ if (quoted !== "") {
31723
+ body = `a body that is not Dispatch's error JSON (${JSON.stringify(quoted)})`;
31724
+ } else if (text === "") {
31725
+ body = "an empty body";
31726
+ } else {
31727
+ const within = text.length > EXCERPT_SCAN_LIMIT ? ` in its first ${EXCERPT_SCAN_LIMIT / 1024} KiB` : "";
31728
+ const size = Buffer.byteLength(text).toLocaleString("en-US");
31729
+ body = `a body of ${size} bytes and no readable text${within}`;
31730
+ }
31731
+ const reasonPhrase = excerpt(response.statusText, this.token);
31732
+ const reason = reasonPhrase === "" ? "" : ` ${reasonPhrase}`;
31733
+ throw new DispatchGatewayError(response.status, `${method} ${url2} answered ${response.status}${reason} with ${body}, ${GATEWAY_PAGE}`, gatewayAdvice(method, response.status));
31653
31734
  }
31654
31735
  return payload;
31655
31736
  }
@@ -32392,7 +32473,7 @@ async function resolveArtifact(client, owner, artifactReference, { canonical = f
32392
32473
  throw new Error("artifact is required for a project document");
32393
32474
  }
32394
32475
  const routed = await client.getProjectArtifact(owner.project, artifactReference).catch((error48) => {
32395
- if (!(error48 instanceof DispatchServiceError) || error48.status !== 404)
32476
+ if (!dispatchAnswered(error48, 404))
32396
32477
  throw error48;
32397
32478
  return;
32398
32479
  });
@@ -32563,12 +32644,12 @@ function referenceLines(edges) {
32563
32644
  if (edges.length === 0)
32564
32645
  return ["- none"];
32565
32646
  return edges.map((edge) => {
32566
- const excerpt = edge.excerpt === undefined ? "" : `${textHead(edge.excerpt.text)} \xB7 `;
32567
- return `- ${edge.kind} ${edge.node.kind} ${edge.node.ref ?? edge.node.id} (${excerpt}${edge.created_at})`;
32647
+ const excerpt2 = edge.excerpt === undefined ? "" : `${textHead(edge.excerpt.text)} \xB7 `;
32648
+ return `- ${edge.kind} ${edge.node.kind} ${edge.node.ref ?? edge.node.id} (${excerpt2}${edge.created_at})`;
32568
32649
  });
32569
32650
  }
32570
32651
  function unavailableReason(error48) {
32571
- return error48 instanceof DispatchServiceError && error48.status === 404 ? "unavailable" : `unavailable: ${messageFor(error48)}`;
32652
+ return dispatchAnswered(error48, 404) ? "unavailable" : `unavailable: ${messageFor(error48)}`;
32572
32653
  }
32573
32654
  async function graphEdges(client, query) {
32574
32655
  try {
@@ -32655,6 +32736,7 @@ function askSummary({ ask, replies }, graph) {
32655
32736
  ]);
32656
32737
  return [
32657
32738
  `Question: ${ask.question}`,
32739
+ ...anchorLines(ask),
32658
32740
  "Options:",
32659
32741
  ...ask.options.length === 0 ? ["- none"] : ask.options.map((option) => `- ${option.label}${option.description ? ` \u2014 ${option.description}` : ""}`),
32660
32742
  `State: ${ask.state}`,
@@ -32718,12 +32800,12 @@ function formatOpenAsksSummary(response, baseUrl) {
32718
32800
  function commentSummary({ comment, replies }, graph) {
32719
32801
  const root = [
32720
32802
  `${comment.id} \xB7 ${actorText(comment.author)}`,
32721
- ...comment.anchor?.quote === undefined ? [] : [`> ${comment.anchor.quote}`],
32803
+ ...anchorLines(comment),
32722
32804
  `Body: ${comment.body}`
32723
32805
  ];
32724
32806
  const chain = replies.flatMap((reply) => [
32725
32807
  `${reply.id} \xB7 ${actorText(reply.author)}`,
32726
- ...reply.anchor?.quote === undefined ? [] : [`> ${reply.anchor.quote}`],
32808
+ ...anchorLines(reply),
32727
32809
  `Body: ${reply.body}`
32728
32810
  ]);
32729
32811
  return [
@@ -32735,6 +32817,28 @@ function commentSummary({ comment, replies }, graph) {
32735
32817
  ].join(`
32736
32818
  `);
32737
32819
  }
32820
+ function anchorLines(record2) {
32821
+ return [
32822
+ ...record2.anchor?.quote === undefined ? [] : [`> ${record2.anchor.quote}`],
32823
+ ...record2.anchor_block === undefined ? [] : [`Position: ${positionText(record2.anchor_block)}`],
32824
+ ...record2.anchor_block_error === undefined ? [] : [`Position: unavailable (${record2.anchor_block_error})`]
32825
+ ];
32826
+ }
32827
+ function positionText(block) {
32828
+ const { table, path: path2 } = block;
32829
+ const segments = path2.map((entry) => `${entry.type}[${entry.index}]`);
32830
+ if (table === undefined || table.row === null)
32831
+ return segments.join(" \u203A ");
32832
+ const tableAt = path2.findIndex((entry) => entry.type === "table");
32833
+ const cells = table.cells ?? [];
32834
+ const label = (table.column === null ? cells : cells.slice(0, table.column)).map((cell) => cell.trim()).filter((cell) => cell !== "" && !/^\d+$/.test(cell)).join(" \xB7 ");
32835
+ const row = label === "" ? `row ${table.row}` : `row ${table.row} (${label})`;
32836
+ const header = table.header === null || table.header === "" ? String(table.column) : table.header;
32837
+ return [
32838
+ ...segments.slice(0, tableAt + 1),
32839
+ table.column === null ? row : `${row}, column ${header}`
32840
+ ].join(" \u203A ");
32841
+ }
32738
32842
  function messageSummary({ message, replies }, graph) {
32739
32843
  const root = [`${message.id} \xB7 ${actorText(message.author)}`, `Body: ${message.body}`];
32740
32844
  const chain = replies.flatMap((reply) => [
@@ -32758,9 +32862,8 @@ async function openArtifactMarks(client, resolved) {
32758
32862
  const marks = asks.filter((ask) => ask.state === "open" && ask.anchor?.artifact_id === resolved.artifact.id).map((ask) => `ask ${ask.id}`);
32759
32863
  const commentsResult = await commentsResultPromise;
32760
32864
  if (commentsResult.status === "rejected") {
32761
- if (commentsResult.reason instanceof DispatchServiceError && commentsResult.reason.status === 404) {
32865
+ if (dispatchAnswered(commentsResult.reason, 404))
32762
32866
  return marks;
32763
- }
32764
32867
  throw commentsResult.reason;
32765
32868
  }
32766
32869
  return [
@@ -32847,10 +32950,30 @@ async function refuseRemovingOpenDecisionBlocks(client, tool, resolved, ops) {
32847
32950
  `));
32848
32951
  }
32849
32952
  function refusalWithCode(error48, suffix = "") {
32953
+ if (error48 instanceof DispatchGatewayError) {
32954
+ let told = error48.message;
32955
+ if (suffix !== "") {
32956
+ told = error48.mayHaveReachedDispatch ? `${error48.answer}${suffix}` : `${error48.answer}, so ${error48.advice}${suffix}`;
32957
+ }
32958
+ return new DispatchGatewayError(error48.status, error48.answer, error48.advice, `${error48.code}: ${told}`);
32959
+ }
32850
32960
  if (!(error48 instanceof DispatchServiceError))
32851
32961
  return error48;
32852
32962
  return new DispatchServiceError(error48.code, error48.status, `${error48.code}: ${error48.message}${suffix}`, error48.candidates, error48.current, error48.mismatches);
32853
32963
  }
32964
+ function dispatchAnswered(error48, status) {
32965
+ return error48 instanceof DispatchServiceError && error48.fromDispatch && error48.status === status;
32966
+ }
32967
+ function writeMayHaveLanded(error48) {
32968
+ if (error48 instanceof DispatchGatewayError)
32969
+ return error48.mayHaveReachedDispatch;
32970
+ return !(error48 instanceof DispatchServiceError) || error48.status >= 500;
32971
+ }
32972
+ function withAccount(error48, account) {
32973
+ const message = messageFor(error48);
32974
+ const told = /[.!?]$/.test(message) ? `${message} ${account.charAt(0).toUpperCase()}${account.slice(1)}` : `${message}; ${account}`;
32975
+ return new Error(told, { cause: error48 });
32976
+ }
32854
32977
  async function executeDispatchTool(input) {
32855
32978
  const configUrl = input.config.url;
32856
32979
  const configToken = input.config.token;
@@ -33020,7 +33143,10 @@ async function executeDispatchTool(input) {
33020
33143
  ref: dispatchChildRef(dispatchIssueRef(issueKey), "message", message.id)
33021
33144
  };
33022
33145
  } catch (error48) {
33023
- throw refusalWithCode(error48, "; the reason was not posted, so the close was not sent");
33146
+ const told = writeMayHaveLanded(error48) ? "the reason may or may not have been posted, and the close was not sent: read the issue's messages before retrying, since retrying this call posts its reason again" : "the reason was not posted, so the close was not sent";
33147
+ if (error48 instanceof DispatchServiceError)
33148
+ throw refusalWithCode(error48, `; ${told}`);
33149
+ throw withAccount(error48, told);
33024
33150
  }
33025
33151
  }
33026
33152
  const linked = before.external_links.map((link) => link.url);
@@ -33039,17 +33165,15 @@ async function executeDispatchTool(input) {
33039
33165
  actor
33040
33166
  });
33041
33167
  } catch (error48) {
33042
- const taken = error48 instanceof DispatchServiceError && error48.status === 500 && newLinks.length > 0 ? `; one of ${newLinks.join(", ")} may already be linked from another issue (a URL links exactly one issue)` : "";
33168
+ const taken = dispatchAnswered(error48, 500) && newLinks.length > 0 ? `; one of ${newLinks.join(", ")} may already be linked from another issue (a URL links exactly one issue)` : "";
33043
33169
  if (closingNote === undefined)
33044
33170
  throw refusalWithCode(error48, taken);
33045
- const refused = error48 instanceof DispatchServiceError && error48.status < 500;
33046
- const posted = `; the reason already landed as message ${closingNote.id} (${closingNote.ref})`;
33047
- const landed = refused ? `${posted} but the issue did not close. Retrying this call posts its reason again, so fix what refused the close, then retry with a reason that points at message ${closingNote.id}` : `${posted}, and the close may or may not have taken effect. Read the issue's status before retrying: done means it closed; otherwise retry with a reason that points at message ${closingNote.id}, since retrying this call posts its reason again`;
33171
+ const posted = `the reason already landed as message ${closingNote.id} (${closingNote.ref})`;
33172
+ const fix = error48 instanceof DispatchGatewayError && error48.transient ? "" : "fix what refused the close, then ";
33173
+ const landed = writeMayHaveLanded(error48) ? `${posted}, and the close may or may not have taken effect. Read the issue's status before retrying: done means it closed; otherwise retry with a reason that points at message ${closingNote.id}, since retrying this call posts its reason again` : `${posted} but the issue did not close. Retrying this call posts its reason again, so ${fix}retry with a reason that points at message ${closingNote.id}`;
33048
33174
  if (error48 instanceof DispatchServiceError)
33049
- throw refusalWithCode(error48, taken + landed);
33050
- throw new Error(`${error48 instanceof Error ? error48.message : String(error48)}${landed}`, {
33051
- cause: error48
33052
- });
33175
+ throw refusalWithCode(error48, `${taken}; ${landed}`);
33176
+ throw withAccount(error48, landed);
33053
33177
  }
33054
33178
  const linkCount = `(${after.external_links.length} ${after.external_links.length === 1 ? "link" : "links"})`;
33055
33179
  const changes = [
@@ -33653,7 +33777,7 @@ async function resolveExistingIssue(client, issueReference) {
33653
33777
  try {
33654
33778
  return await client.resolveIssue(issueReference);
33655
33779
  } catch (error48) {
33656
- if (error48 instanceof DispatchServiceError && error48.status === 404) {
33780
+ if (dispatchAnswered(error48, 404)) {
33657
33781
  throw new Error(`no Dispatch issue is linked to ${issueReference}; create it first with ` + `dispatch_issue({ external: "${issueReference}", ... })`);
33658
33782
  }
33659
33783
  throw error48;
@@ -33702,7 +33826,7 @@ import { logger } from "@oh-my-pi/pi-utils";
33702
33826
  // package.json
33703
33827
  var package_default = {
33704
33828
  name: "@sjawhar/pi-legion-envoy",
33705
- version: "5.30.4",
33829
+ version: "5.30.6",
33706
33830
  type: "module",
33707
33831
  omp: {
33708
33832
  extensions: [
@@ -57,7 +57,10 @@ table's block id. A row index includes the header: row `0` is the header and its
57
57
  row and any row's last column cannot be deleted. An index is required. A missing, non-integer, negative, or out-of-range index is
58
58
  `INVALID_OP` on `index`, naming the supplied value and the table's actual dimensions before making any change. Markdown parsing
59
59
  canonicalizes short ragged rows by padding missing cells, so column deletion preserves every non-selected cell in the canonical table.
60
- `GET /api/v1/artifacts/<artifact UUID>/blocks` reports a table's own references plus its descendant cell anchors. A row or column
60
+ `GET /api/v1/artifacts/<artifact UUID>/blocks` reports a table's own references plus its descendant cell anchors.
61
+ `GET /api/v1/artifacts/<artifact UUID>/blocks/<block id>` places one block: its path from the top-level block down, and for a cell
62
+ the row index (0 is the header, what `delete_row` takes), the column index (what `delete_column` takes), the text of the header cell
63
+ drawn above it (with colspans or rowspans, the column the cell is drawn in) and the row's cells. A row or column
61
64
  deletion that would remove an open ask or unresolved comment anchor is `INVALID_OP` on `index`, naming the axis and anchor ids;
62
65
  answered asks and resolved comments are history and do not block it. A `find` or quote anchor tolerates inline Markdown
63
66
  (`**bold**`, `` `code` ``) and a leading `# ` selects a heading by its text; a miss names the quote and the three nearest blocks so
@@ -16,6 +16,15 @@ document owner or ref, it returns a document summary with `details` `{ project,
16
16
  question, options, state, answer, and its reply thread. With a comment ref, it returns that comment and its quoted reply chain. With a
17
17
  message ref, it returns that message and its reply chain. Reads do not subscribe; use `dispatch_doc_read` for document contents.
18
18
 
19
+ An anchored comment or ask also prints `Position:`, where its quote's block stands — `table[3] › row 5 (Red-teamer loop), column Due`
20
+ is the fourth top-level block, a table, its row 5 (row 0 is the header; the index `delete_row` takes), labelled by the row's cells
21
+ before the anchored one, in the column headed Due; outside a table it is the path of types and child indexes down to the block. The
22
+ same facts are `anchor_block` on `GET /api/v1/comments/{id}` and `GET /api/v1/asks/{id}`, and
23
+ `GET /api/v1/artifacts/{id}/blocks/{block_id}` answers them for any block id a document holds. When Dispatch could not read the
24
+ document, the read still answers and prints `Position: unavailable (DOC_SERVICE_UNAVAILABLE)` (try again shortly),
25
+ `Position: unavailable (DOC_SCHEMA)` (the document needs repair) or `Position: unavailable (INTERNAL)`; the API carries that
26
+ code as `anchor_block_error`.
27
+
19
28
  Every read ends with two sections from the reference graph. `Referenced by:` lists what points at the node — every document, ask,
20
29
  comment, or message that cites it, plus its structure: child issues, attached documents, anchored and owned asks and comments, replies,
21
30
  followers — and `Links:` lists what it cites. Each row is `- <edge kind> <node kind> dispatch://… (<excerpt> · <when>)`; for a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/pi-legion-envoy",
3
- "version": "5.30.4",
3
+ "version": "5.30.6",
4
4
  "type": "module",
5
5
  "omp": {
6
6
  "extensions": [