@sjawhar/pi-legion-envoy 0.42.0 → 0.44.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
@@ -29725,6 +29725,11 @@ var ChildStatusEventPayloadSchema = object({
29725
29725
  from: string2().optional(),
29726
29726
  to: string2().optional()
29727
29727
  });
29728
+ var SubscriptionRemovedEventPayloadSchema = object({
29729
+ session_id: string2().optional(),
29730
+ by: object({ kind: string2(), id: string2().optional() }).passthrough().optional(),
29731
+ topics: array(string2()).optional()
29732
+ });
29728
29733
  // ../contracts/src/dispatch-snippet.ts
29729
29734
  var HTML_ENTITIES = [
29730
29735
  ["&lt;", "<"],
@@ -29782,15 +29787,17 @@ function dispatchToolSchema(spec, z, opts) {
29782
29787
  }
29783
29788
  var ISSUE_REFERENCE = "An issue is a native KEY or external owner/repo#n reference; an external reference creates its native issue in the repository's dashboard-configured project or, failing that, the default project (DISPATCH_DEFAULT_PROJECT).";
29784
29789
  var OWNER_REFERENCE = "Exactly one of issue and project is required. An issue is a native KEY or external owner/repo#n reference; a project is a project key such as CORE and addresses an unlinked project document named by artifact.";
29785
- function documentOwnerValidation(requireArtifact) {
29790
+ function documentOwnerValidation(requireArtifact, alwaysRequireArtifact = false) {
29786
29791
  return {
29787
29792
  check: (value) => {
29788
29793
  const input = value;
29789
29794
  const hasIssue = typeof input.issue === "string";
29790
29795
  const hasProject = typeof input.project === "string";
29791
- return (hasIssue !== hasProject || !hasIssue && !hasProject && typeof input.ref === "string") && (!hasProject || !requireArtifact || typeof input.artifact === "string");
29796
+ const hasArtifact = typeof input.artifact === "string";
29797
+ const hasRef = typeof input.ref === "string";
29798
+ return (hasIssue !== hasProject || !hasIssue && !hasProject && hasRef) && (!hasProject || !requireArtifact || hasArtifact) && (!alwaysRequireArtifact || hasArtifact || hasRef);
29792
29799
  },
29793
- message: "Exactly one of issue and project is required; with project, artifact names the document."
29800
+ message: alwaysRequireArtifact ? "Exactly one of issue and project is required; artifact or ref must name the document." : "Exactly one of issue and project is required; with project, artifact names the document."
29794
29801
  };
29795
29802
  }
29796
29803
  var SPEC_SECTIONS = [
@@ -29825,6 +29832,7 @@ var dispatchToolSpecs = [
29825
29832
  issue: z.string().describe(ISSUE_REFERENCE).optional(),
29826
29833
  project: z.string().describe("Project key owning the document.").optional(),
29827
29834
  artifact: z.string().describe("Project document slug or id.").optional(),
29835
+ ref: z.string().describe("Optional dispatch:// issue or document reference.").optional(),
29828
29836
  question: z.string({ max: 800 }).describe("Decision question, at most 800 characters."),
29829
29837
  options: z.array(z.object({
29830
29838
  label: z.string().describe("Selectable option label."),
@@ -29877,6 +29885,7 @@ var dispatchToolSpecs = [
29877
29885
  issue: z.string().describe(ISSUE_REFERENCE).optional(),
29878
29886
  project: z.string().describe("Project key owning the document.").optional(),
29879
29887
  artifact: z.string().describe("Artifact slug or id required when quote is given.").optional(),
29888
+ ref: z.string().describe("Optional dispatch:// issue or document reference.").optional(),
29880
29889
  quote: z.string().describe("Optional exact quoted document text.").optional(),
29881
29890
  occurrence: z.number({ int: true, min: 0 }).describe("Optional zero-based occurrence of quote.").optional(),
29882
29891
  body: z.string({ max: 2000 }).describe("Review comment, at most 2,000 characters."),
@@ -29891,13 +29900,14 @@ var dispatchToolSpecs = [
29891
29900
  arguments: (z) => ({
29892
29901
  issue: z.string().describe(ISSUE_REFERENCE).optional(),
29893
29902
  project: z.string().describe("Project key owning the document.").optional(),
29894
- artifact: z.string().describe("Artifact slug or id containing the quoted text."),
29903
+ artifact: z.string().describe("Artifact slug or id containing the quoted text; optional when ref names it.").optional(),
29904
+ ref: z.string().describe("Optional dispatch:// issue or document reference.").optional(),
29895
29905
  quote: z.string().describe("Exact document text to replace."),
29896
29906
  replace_with: z.string().describe("Replacement text."),
29897
29907
  body: z.string({ max: 2000 }).describe("Optional rationale, at most 2,000 characters.").optional(),
29898
29908
  occurrence: z.number({ int: true, min: 0 }).describe("Optional zero-based occurrence of quote.").optional()
29899
29909
  }),
29900
- validation: documentOwnerValidation(true)
29910
+ validation: documentOwnerValidation(true, true)
29901
29911
  },
29902
29912
  {
29903
29913
  name: "dispatch_message",
@@ -29914,7 +29924,8 @@ var dispatchToolSpecs = [
29914
29924
  arguments: (z) => ({
29915
29925
  issue: z.string().describe(ISSUE_REFERENCE).optional(),
29916
29926
  project: z.string().describe("Project key owning the document.").optional(),
29917
- artifact: z.string().describe("Artifact slug or id for the document."),
29927
+ artifact: z.string().describe("Artifact slug or id for the document; optional when ref names it.").optional(),
29928
+ ref: z.string().describe("Optional dispatch:// issue or document reference.").optional(),
29918
29929
  ops: z.array(z.object({
29919
29930
  op: z.enum(DOC_EDIT_OPS).describe("Edit operation."),
29920
29931
  find: z.string().describe("Text to find for replace or delete.").optional(),
@@ -29926,7 +29937,7 @@ var dispatchToolSpecs = [
29926
29937
  })).describe("Flat tagged edits; the server validates fields required for each operation."),
29927
29938
  summary: z.string().describe("Optional named-version summary.").optional()
29928
29939
  }),
29929
- validation: documentOwnerValidation(true)
29940
+ validation: documentOwnerValidation(true, true)
29930
29941
  },
29931
29942
  {
29932
29943
  name: "dispatch_doc_read",
@@ -31101,6 +31112,18 @@ function renderInbound(raw, sessionID, subject) {
31101
31112
  if (frame.event.actor.kind === "session" && frame.event.actor.id === sessionID) {
31102
31113
  return { skip: true, content: "", envelope };
31103
31114
  }
31115
+ if (frame.event.type === "subscription.removed") {
31116
+ const removed = SubscriptionRemovedEventPayloadSchema.safeParse(frame.event.payload);
31117
+ if (!removed.success || removed.data.session_id !== sessionID) {
31118
+ return { skip: true, content: "", envelope };
31119
+ }
31120
+ const who = removed.data.by?.id ?? "someone";
31121
+ return {
31122
+ skip: false,
31123
+ content: `Unsubscribed from ${dispatchOwner(frame.event, subject ?? envelope.topic)} by ${who}`,
31124
+ envelope
31125
+ };
31126
+ }
31104
31127
  askQuestion = dispatchAskQuestion(frame.event);
31105
31128
  messageReplyPreview = dispatchMessageReplyPreview(frame.event);
31106
31129
  dispatchEvent = {
@@ -32381,6 +32404,46 @@ function dispatchSubscriptionTopic(details) {
32381
32404
  const { topic } = details;
32382
32405
  return typeof topic === "string" && topic.startsWith("notifications.dispatch.") ? topic : null;
32383
32406
  }
32407
+ function dispatchTopicLabel(topic) {
32408
+ if (topic.startsWith(DISPATCH_ISSUE_TOPIC_PREFIX)) {
32409
+ const [key] = topic.slice(DISPATCH_ISSUE_TOPIC_PREFIX.length).split(".", 2);
32410
+ if (key !== undefined && key !== "")
32411
+ return key;
32412
+ }
32413
+ if (topic.startsWith(DISPATCH_DOCUMENT_TOPIC_PREFIX)) {
32414
+ const [project, slug] = topic.slice(DISPATCH_DOCUMENT_TOPIC_PREFIX.length).split(".", 3);
32415
+ if (project !== undefined && project !== "" && slug !== undefined && slug !== "") {
32416
+ return `${project}/${slug}`;
32417
+ }
32418
+ }
32419
+ return topic;
32420
+ }
32421
+ function subscriptionRemovedTopics(raw, sessionID) {
32422
+ let envelope;
32423
+ try {
32424
+ envelope = JSON.parse(raw);
32425
+ } catch {
32426
+ return;
32427
+ }
32428
+ if (typeof envelope !== "object" || envelope === null)
32429
+ return;
32430
+ const { source, payload } = envelope;
32431
+ if (source !== "dispatch" || typeof payload !== "string")
32432
+ return;
32433
+ let event;
32434
+ try {
32435
+ event = JSON.parse(payload);
32436
+ } catch {
32437
+ return;
32438
+ }
32439
+ const parsedEvent = DispatchEventSchema.safeParse(event);
32440
+ if (!parsedEvent.success || parsedEvent.data.type !== "subscription.removed")
32441
+ return;
32442
+ const parsedPayload = SubscriptionRemovedEventPayloadSchema.safeParse(parsedEvent.data.payload);
32443
+ if (!parsedPayload.success || parsedPayload.data.session_id !== sessionID)
32444
+ return;
32445
+ return parsedPayload.data.topics ?? [];
32446
+ }
32384
32447
 
32385
32448
  // ../envoy-client/src/tool-contract.ts
32386
32449
  var DELIVERY_CONTRACT = "Delivery is at-least-once, possibly out of order across topics; use id for dedupe and at for freshness.";
@@ -32858,6 +32921,8 @@ function envoyExtension(pi) {
32858
32921
  };
32859
32922
  const deliver = async (subject, raw, reply) => {
32860
32923
  const rendered = renderInbound(raw, sessionID, subject);
32924
+ for (const topic of subscriptionRemovedTopics(raw, sessionID) ?? [])
32925
+ closeIntentionally(topic);
32861
32926
  const dedupeKey = rendered.envelope?.dedupe_key;
32862
32927
  const duplicate = dedupeKey !== undefined && dedupeKeys.has(dedupeKey);
32863
32928
  if (!duplicate && !rendered.skip) {
@@ -33249,8 +33314,16 @@ function envoyExtension(pi) {
33249
33314
  if (topic === null)
33250
33315
  return;
33251
33316
  try {
33252
- if (await subscribe(topic))
33317
+ const isNew = await subscribe(topic);
33318
+ if (isNew)
33253
33319
  await registerSession();
33320
+ if (isNew) {
33321
+ pi.sendMessage({
33322
+ customType: "envoy-message",
33323
+ content: `Subscribed to ${dispatchTopicLabel(topic)} (every event on this issue reaches you; envoy_unsubscribe ${topic} to stop).`,
33324
+ display: true
33325
+ }, { deliverAs: "steer", triggerTurn: false });
33326
+ }
33254
33327
  } catch (error) {
33255
33328
  activeSessionContext?.ui.notify(`envoy: dispatch reply auto-subscribe failed (${messageFor(error)}); run envoy_subscribe ${topic}`, "warning");
33256
33329
  }
package/dist/legion.js CHANGED
@@ -29724,6 +29724,11 @@ var ChildStatusEventPayloadSchema = object({
29724
29724
  from: string2().optional(),
29725
29725
  to: string2().optional()
29726
29726
  });
29727
+ var SubscriptionRemovedEventPayloadSchema = object({
29728
+ session_id: string2().optional(),
29729
+ by: object({ kind: string2(), id: string2().optional() }).passthrough().optional(),
29730
+ topics: array(string2()).optional()
29731
+ });
29727
29732
  // ../contracts/src/dispatch-snippet.ts
29728
29733
  var HTML_ENTITIES = [
29729
29734
  ["&lt;", "<"],
@@ -29781,15 +29786,17 @@ function dispatchToolSchema(spec, z, opts) {
29781
29786
  }
29782
29787
  var ISSUE_REFERENCE = "An issue is a native KEY or external owner/repo#n reference; an external reference creates its native issue in the repository's dashboard-configured project or, failing that, the default project (DISPATCH_DEFAULT_PROJECT).";
29783
29788
  var OWNER_REFERENCE = "Exactly one of issue and project is required. An issue is a native KEY or external owner/repo#n reference; a project is a project key such as CORE and addresses an unlinked project document named by artifact.";
29784
- function documentOwnerValidation(requireArtifact) {
29789
+ function documentOwnerValidation(requireArtifact, alwaysRequireArtifact = false) {
29785
29790
  return {
29786
29791
  check: (value) => {
29787
29792
  const input = value;
29788
29793
  const hasIssue = typeof input.issue === "string";
29789
29794
  const hasProject = typeof input.project === "string";
29790
- return (hasIssue !== hasProject || !hasIssue && !hasProject && typeof input.ref === "string") && (!hasProject || !requireArtifact || typeof input.artifact === "string");
29795
+ const hasArtifact = typeof input.artifact === "string";
29796
+ const hasRef = typeof input.ref === "string";
29797
+ return (hasIssue !== hasProject || !hasIssue && !hasProject && hasRef) && (!hasProject || !requireArtifact || hasArtifact) && (!alwaysRequireArtifact || hasArtifact || hasRef);
29791
29798
  },
29792
- message: "Exactly one of issue and project is required; with project, artifact names the document."
29799
+ message: alwaysRequireArtifact ? "Exactly one of issue and project is required; artifact or ref must name the document." : "Exactly one of issue and project is required; with project, artifact names the document."
29793
29800
  };
29794
29801
  }
29795
29802
  var SPEC_SECTIONS = [
@@ -29824,6 +29831,7 @@ var dispatchToolSpecs = [
29824
29831
  issue: z.string().describe(ISSUE_REFERENCE).optional(),
29825
29832
  project: z.string().describe("Project key owning the document.").optional(),
29826
29833
  artifact: z.string().describe("Project document slug or id.").optional(),
29834
+ ref: z.string().describe("Optional dispatch:// issue or document reference.").optional(),
29827
29835
  question: z.string({ max: 800 }).describe("Decision question, at most 800 characters."),
29828
29836
  options: z.array(z.object({
29829
29837
  label: z.string().describe("Selectable option label."),
@@ -29876,6 +29884,7 @@ var dispatchToolSpecs = [
29876
29884
  issue: z.string().describe(ISSUE_REFERENCE).optional(),
29877
29885
  project: z.string().describe("Project key owning the document.").optional(),
29878
29886
  artifact: z.string().describe("Artifact slug or id required when quote is given.").optional(),
29887
+ ref: z.string().describe("Optional dispatch:// issue or document reference.").optional(),
29879
29888
  quote: z.string().describe("Optional exact quoted document text.").optional(),
29880
29889
  occurrence: z.number({ int: true, min: 0 }).describe("Optional zero-based occurrence of quote.").optional(),
29881
29890
  body: z.string({ max: 2000 }).describe("Review comment, at most 2,000 characters."),
@@ -29890,13 +29899,14 @@ var dispatchToolSpecs = [
29890
29899
  arguments: (z) => ({
29891
29900
  issue: z.string().describe(ISSUE_REFERENCE).optional(),
29892
29901
  project: z.string().describe("Project key owning the document.").optional(),
29893
- artifact: z.string().describe("Artifact slug or id containing the quoted text."),
29902
+ artifact: z.string().describe("Artifact slug or id containing the quoted text; optional when ref names it.").optional(),
29903
+ ref: z.string().describe("Optional dispatch:// issue or document reference.").optional(),
29894
29904
  quote: z.string().describe("Exact document text to replace."),
29895
29905
  replace_with: z.string().describe("Replacement text."),
29896
29906
  body: z.string({ max: 2000 }).describe("Optional rationale, at most 2,000 characters.").optional(),
29897
29907
  occurrence: z.number({ int: true, min: 0 }).describe("Optional zero-based occurrence of quote.").optional()
29898
29908
  }),
29899
- validation: documentOwnerValidation(true)
29909
+ validation: documentOwnerValidation(true, true)
29900
29910
  },
29901
29911
  {
29902
29912
  name: "dispatch_message",
@@ -29913,7 +29923,8 @@ var dispatchToolSpecs = [
29913
29923
  arguments: (z) => ({
29914
29924
  issue: z.string().describe(ISSUE_REFERENCE).optional(),
29915
29925
  project: z.string().describe("Project key owning the document.").optional(),
29916
- artifact: z.string().describe("Artifact slug or id for the document."),
29926
+ artifact: z.string().describe("Artifact slug or id for the document; optional when ref names it.").optional(),
29927
+ ref: z.string().describe("Optional dispatch:// issue or document reference.").optional(),
29917
29928
  ops: z.array(z.object({
29918
29929
  op: z.enum(DOC_EDIT_OPS).describe("Edit operation."),
29919
29930
  find: z.string().describe("Text to find for replace or delete.").optional(),
@@ -29925,7 +29936,7 @@ var dispatchToolSpecs = [
29925
29936
  })).describe("Flat tagged edits; the server validates fields required for each operation."),
29926
29937
  summary: z.string().describe("Optional named-version summary.").optional()
29927
29938
  }),
29928
- validation: documentOwnerValidation(true)
29939
+ validation: documentOwnerValidation(true, true)
29929
29940
  },
29930
29941
  {
29931
29942
  name: "dispatch_doc_read",
@@ -31548,6 +31559,18 @@ function renderInbound(raw, sessionID, subject) {
31548
31559
  if (frame.event.actor.kind === "session" && frame.event.actor.id === sessionID) {
31549
31560
  return { skip: true, content: "", envelope };
31550
31561
  }
31562
+ if (frame.event.type === "subscription.removed") {
31563
+ const removed = SubscriptionRemovedEventPayloadSchema.safeParse(frame.event.payload);
31564
+ if (!removed.success || removed.data.session_id !== sessionID) {
31565
+ return { skip: true, content: "", envelope };
31566
+ }
31567
+ const who = removed.data.by?.id ?? "someone";
31568
+ return {
31569
+ skip: false,
31570
+ content: `Unsubscribed from ${dispatchOwner(frame.event, subject ?? envelope.topic)} by ${who}`,
31571
+ envelope
31572
+ };
31573
+ }
31551
31574
  askQuestion = dispatchAskQuestion(frame.event);
31552
31575
  messageReplyPreview = dispatchMessageReplyPreview(frame.event);
31553
31576
  dispatchEvent = {
@@ -32821,6 +32844,46 @@ function dispatchSubscriptionTopic(details) {
32821
32844
  const { topic } = details;
32822
32845
  return typeof topic === "string" && topic.startsWith("notifications.dispatch.") ? topic : null;
32823
32846
  }
32847
+ function dispatchTopicLabel(topic) {
32848
+ if (topic.startsWith(DISPATCH_ISSUE_TOPIC_PREFIX)) {
32849
+ const [key] = topic.slice(DISPATCH_ISSUE_TOPIC_PREFIX.length).split(".", 2);
32850
+ if (key !== undefined && key !== "")
32851
+ return key;
32852
+ }
32853
+ if (topic.startsWith(DISPATCH_DOCUMENT_TOPIC_PREFIX)) {
32854
+ const [project, slug] = topic.slice(DISPATCH_DOCUMENT_TOPIC_PREFIX.length).split(".", 3);
32855
+ if (project !== undefined && project !== "" && slug !== undefined && slug !== "") {
32856
+ return `${project}/${slug}`;
32857
+ }
32858
+ }
32859
+ return topic;
32860
+ }
32861
+ function subscriptionRemovedTopics(raw, sessionID) {
32862
+ let envelope;
32863
+ try {
32864
+ envelope = JSON.parse(raw);
32865
+ } catch {
32866
+ return;
32867
+ }
32868
+ if (typeof envelope !== "object" || envelope === null)
32869
+ return;
32870
+ const { source, payload } = envelope;
32871
+ if (source !== "dispatch" || typeof payload !== "string")
32872
+ return;
32873
+ let event;
32874
+ try {
32875
+ event = JSON.parse(payload);
32876
+ } catch {
32877
+ return;
32878
+ }
32879
+ const parsedEvent = DispatchEventSchema.safeParse(event);
32880
+ if (!parsedEvent.success || parsedEvent.data.type !== "subscription.removed")
32881
+ return;
32882
+ const parsedPayload = SubscriptionRemovedEventPayloadSchema.safeParse(parsedEvent.data.payload);
32883
+ if (!parsedPayload.success || parsedPayload.data.session_id !== sessionID)
32884
+ return;
32885
+ return parsedPayload.data.topics ?? [];
32886
+ }
32824
32887
 
32825
32888
  // ../envoy-client/src/tool-contract.ts
32826
32889
  var DELIVERY_CONTRACT = "Delivery is at-least-once, possibly out of order across topics; use id for dedupe and at for freshness.";
@@ -33290,6 +33353,8 @@ function envoyExtension(pi) {
33290
33353
  };
33291
33354
  const deliver = async (subject, raw, reply) => {
33292
33355
  const rendered = renderInbound(raw, sessionID, subject);
33356
+ for (const topic of subscriptionRemovedTopics(raw, sessionID) ?? [])
33357
+ closeIntentionally(topic);
33293
33358
  const dedupeKey = rendered.envelope?.dedupe_key;
33294
33359
  const duplicate = dedupeKey !== undefined && dedupeKeys.has(dedupeKey);
33295
33360
  if (!duplicate && !rendered.skip) {
@@ -33681,8 +33746,16 @@ function envoyExtension(pi) {
33681
33746
  if (topic === null)
33682
33747
  return;
33683
33748
  try {
33684
- if (await subscribe(topic))
33749
+ const isNew = await subscribe(topic);
33750
+ if (isNew)
33685
33751
  await registerSession();
33752
+ if (isNew) {
33753
+ pi.sendMessage({
33754
+ customType: "envoy-message",
33755
+ content: `Subscribed to ${dispatchTopicLabel(topic)} (every event on this issue reaches you; envoy_unsubscribe ${topic} to stop).`,
33756
+ display: true
33757
+ }, { deliverAs: "steer", triggerTurn: false });
33758
+ }
33686
33759
  } catch (error) {
33687
33760
  activeSessionContext?.ui.notify(`envoy: dispatch reply auto-subscribe failed (${messageFor(error)}); run envoy_subscribe ${topic}`, "warning");
33688
33761
  }
@@ -169,19 +169,22 @@ retrying. Pass `summary` to name the version when recording a decision.
169
169
  Add feedback with:
170
170
 
171
171
  ```ts
172
- dispatch_comment({ issue?, project?, artifact?, quote?, occurrence?, body, reply_to?, reply_to_ask? })
172
+ dispatch_comment({ issue?, project?, artifact?, ref?, quote?, occurrence?, body, reply_to?, reply_to_ask? })
173
173
  ```
174
174
 
175
- It returns issue or project-document owner details plus `comment` and, for writes, `topic`. `quote` requires `artifact`; omit both for a
176
- floating issue comment. A reply (`reply_to`/`reply_to_ask`) takes no `quote`; it belongs to its parent's anchor. Use `reply_to` to
177
- continue a comment thread at its root; a reply to a resolved thread reopens it. Use `reply_to_ask` to reply directly under a question
178
- asked with `dispatch_ask`. Comments are edited only by their author from the dashboard. A delivered `comment.created` event carries the
179
- comment `id`; reply to it with `dispatch_comment({ reply_to: <id> })`.
175
+ It returns issue or project-document owner details plus `comment` and, for writes, `topic`.
176
+ `ref` names the owner (an issue or project-document reference) in place of `issue`/`project`.
177
+ `quote` requires `artifact`; omit both for a floating issue comment. A reply (`reply_to`/`reply_to_ask`)
178
+ takes no `quote`; it belongs to its parent's anchor. Use `reply_to` to continue a comment thread at
179
+ its root; a reply to a resolved thread reopens it. Use `reply_to_ask` to reply directly under a
180
+ question asked with `dispatch_ask`. Comments are edited only by their author from the dashboard. A
181
+ delivered `comment.created` event carries the comment `id`; reply to it with
182
+ `dispatch_comment({ reply_to: <id> })`.
180
183
 
181
184
  Propose an exact replacement instead of describing it:
182
185
 
183
186
  ```ts
184
- dispatch_suggest({ issue?, project?, artifact, quote, replace_with, body?, occurrence? })
187
+ dispatch_suggest({ issue?, project?, artifact, ref?, quote, replace_with, body?, occurrence? })
185
188
  ```
186
189
 
187
190
  It returns issue or project-document owner details plus `comment` and its write `topic`. A human accepts or rejects a suggestion.
@@ -223,9 +226,11 @@ It returns `details` `{ issue, topic, message }`. `body` is capped at 2,000 char
223
226
 
224
227
  ## What comes back
225
228
 
226
- A write result's `details.topic` subscribes the host to its owner. Issue writes use `notifications.dispatch.issue.<KEY>.>`;
229
+ A write result's `details.topic` subscribes the host to its owner, and the first such subscription on an issue or document also tells
230
+ you so (an already-subscribed write stays quiet — no repeat notice). Issue writes use `notifications.dispatch.issue.<KEY>.>`;
227
231
  project-document writes use `notifications.dispatch.document.<PROJECT>.<SLUG>.>`. The owner topic carries every Dispatch event; `notify`
228
- only controls agent wake and routed delivery. After a restart, catch up with:
232
+ only controls agent wake and routed delivery. A human may unsubscribe you from the issue or document header; you are told with a
233
+ `subscription.removed` notice when that happens. After a restart, catch up with:
229
234
 
230
235
  ```ts
231
236
  dispatch_read({ issue?, project?, artifact?, ref? })
@@ -238,7 +243,12 @@ message ref, it returns that message and its reply chain. Reads do not subscribe
238
243
 
239
244
  ## References
240
245
 
241
- Use these in document, ask, comment, and message bodies; Dispatch unfurls them:
246
+ Use these in document, ask, comment, and message bodies. In the dashboard, a reference renders
247
+ as an inline link whose text is the target's title (an issue's title, an ask's question, a
248
+ comment's first line, a document's name) once it resolves; a body that is only a bare reference
249
+ still gets an unfurl card instead. Every `ref` argument below (and `issue`/`project`) accepts
250
+ either form — an issue key or a project key is never ambiguous, since a project key never
251
+ contains a dash:
242
252
 
243
253
  ```text
244
254
  dispatch://KEY
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/pi-legion-envoy",
3
- "version": "0.42.0",
3
+ "version": "0.44.0",
4
4
  "type": "module",
5
5
  "omp": {
6
6
  "extensions": [