@sjawhar/pi-legion-envoy 1.9.0 → 1.11.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
@@ -29730,10 +29730,20 @@ var CommentEventPayloadSchema = object({
29730
29730
  var MessageEventPayloadSchema = object({
29731
29731
  id: string2().optional(),
29732
29732
  body: string2().optional(),
29733
- reply_to: string2().nullish(),
29733
+ target: string2().nullish(),
29734
+ in_reply_to: string2().nullish(),
29734
29735
  reply_body: string2().optional(),
29735
29736
  author: object({ kind: string2(), id: string2() }).optional()
29736
29737
  });
29738
+ var MessageDeliveryEventPayloadSchema = object({
29739
+ message_id: string2().optional(),
29740
+ attempt: number2().int().positive().optional(),
29741
+ delivery: _enum2(["btw", "aside", "steer"]).optional(),
29742
+ session_id: string2().optional(),
29743
+ title: string2().optional(),
29744
+ state: _enum2(["sent", "failed"]).optional(),
29745
+ error: string2().optional()
29746
+ });
29737
29747
  var ChildStatusEventPayloadSchema = object({
29738
29748
  child_key: string2().optional(),
29739
29749
  from: string2().optional(),
@@ -29827,7 +29837,7 @@ var SPEC_SECTIONS = [
29827
29837
  ];
29828
29838
  var SPEC_WRITING_GUIDANCE = `When writing a spec, use these sections in order: ${SPEC_SECTIONS.join(", ")}. ` + "Write for a reader who has not seen the code: plain sentences, every identifier expanded on " + "first use, no coined shorthand; see skills/dispatch Writing for the human and Writing a spec.";
29829
29839
  var ASK_URGENCIES = ["low", "med", "high", "blocking"];
29830
- var DOC_EDIT_OPS = ["replace", "delete", "insert"];
29840
+ var DOC_EDIT_OPS = ["replace", "delete", "insert", "retype"];
29831
29841
  var dispatchToolSpecs = [
29832
29842
  {
29833
29843
  name: "dispatch_issue",
@@ -29932,12 +29942,12 @@ var dispatchToolSpecs = [
29932
29942
  arguments: (z) => ({
29933
29943
  issue: z.string().describe(ISSUE_REFERENCE),
29934
29944
  body: z.string({ max: 2000 }).describe("Update text, at most 2,000 characters."),
29935
- reply_to: z.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.").optional()
29945
+ in_reply_to: z.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.").optional()
29936
29946
  })
29937
29947
  },
29938
29948
  {
29939
29949
  name: "dispatch_doc_edit",
29940
- description: "Apply deterministic text edits to an issue or project document. Do not use it for review feedback or for reading; use " + "dispatch_comment, dispatch_suggest, or dispatch_doc_read instead. The spec (or any document) holds requirements, " + `design, and decisions - never progress, status, or timestamps. ${OWNER_REFERENCE} ${SPEC_WRITING_GUIDANCE}`,
29950
+ description: "Apply deterministic document edits, including retyping an identified paragraph into a schema-declared typed block. " + "Do not use it for review feedback or for reading; use dispatch_comment, dispatch_suggest, or dispatch_doc_read instead. " + `The spec (or any document) holds requirements, design, and decisions - never progress, status, or timestamps. ${OWNER_REFERENCE} ${SPEC_WRITING_GUIDANCE}`,
29941
29951
  arguments: (z) => ({
29942
29952
  issue: z.string().describe(ISSUE_REFERENCE).optional(),
29943
29953
  project: z.string().describe("Project key owning the document.").optional(),
@@ -29950,7 +29960,10 @@ var dispatchToolSpecs = [
29950
29960
  occurrence: z.number({ int: true, min: 0 }).describe("Optional zero-based match occurrence.").optional(),
29951
29961
  markdown: z.string().describe("Markdown to insert.").optional(),
29952
29962
  after: z.string().describe("Anchor after which to insert.").optional(),
29953
- before: z.string().describe("Anchor before which to insert.").optional()
29963
+ before: z.string().describe("Anchor before which to insert.").optional(),
29964
+ block: z.string().describe("Block id to retype.").optional(),
29965
+ type: z.string().describe("Typed block name for retype.").optional(),
29966
+ attributes: z.unknown().describe("Typed block attributes for retype.").optional()
29954
29967
  })).describe("Flat tagged edits; the server validates fields required for each operation."),
29955
29968
  summary: z.string().describe("Optional named-version summary.").optional()
29956
29969
  }),
@@ -30411,6 +30424,7 @@ function zodSchemaApi(zod) {
30411
30424
  },
30412
30425
  boolean: () => api.boolean(),
30413
30426
  enum: (values) => api.enum(values),
30427
+ unknown: () => api.unknown(),
30414
30428
  array: (item, opts = {}) => {
30415
30429
  let schema = api.array(item);
30416
30430
  if (opts.min !== undefined)
@@ -31008,6 +31022,31 @@ var TolerantInboundEnvelopeSchema = object({
31008
31022
  source: InboundEnvelopeSchema.shape.source.optional().catch(undefined),
31009
31023
  sender: object(tolerantShape(InboundSenderSchema.shape)).optional().catch(undefined)
31010
31024
  }).passthrough();
31025
+ var DispatchDeliveryRequestSchema = object({
31026
+ attempt: number2().int().positive(),
31027
+ mode: _enum2(["btw", "aside", "steer"])
31028
+ });
31029
+ var DispatchTargetedMessagePayloadSchema = object({
31030
+ id: string2(),
31031
+ issue_key: string2(),
31032
+ author: object({ kind: string2(), id: string2() }),
31033
+ body: string2(),
31034
+ target: string2(),
31035
+ in_reply_to: string2().nullable(),
31036
+ deliveries: array(unknown()),
31037
+ created_at: string2()
31038
+ });
31039
+ var DispatchTargetedFrameSchema = object({
31040
+ event: DispatchEventSchema,
31041
+ delivery: DispatchDeliveryRequestSchema
31042
+ }).refine(({ event }) => event.type === "message.created" && DispatchTargetedMessagePayloadSchema.safeParse(event.payload).success, { message: "targeted delivery requires a message.created event" });
31043
+ var RecoverableDispatchDeliveryFailureSchema = object({
31044
+ event: object({
31045
+ issue_key: string2(),
31046
+ payload: object({ id: string2(), body: string2().optional() }).passthrough()
31047
+ }).passthrough(),
31048
+ delivery: DispatchDeliveryRequestSchema
31049
+ });
31011
31050
  function senderLabel(envelope) {
31012
31051
  const sender = envelope.source_session ?? envelope.source ?? "unknown";
31013
31052
  return envelope.sender?.title === undefined ? sender : `${sender} (${envelope.sender.title})`;
@@ -31034,6 +31073,8 @@ var DISPATCH_PAYLOAD_SCHEMAS = {
31034
31073
  "suggestion.accepted": CommentEventPayloadSchema,
31035
31074
  "suggestion.rejected": CommentEventPayloadSchema,
31036
31075
  "message.created": MessageEventPayloadSchema,
31076
+ "message.delivery": MessageDeliveryEventPayloadSchema,
31077
+ "message.answered": MessageEventPayloadSchema,
31037
31078
  "child.status": ChildStatusEventPayloadSchema
31038
31079
  };
31039
31080
  function dispatchOwner(event, topic) {
@@ -31066,6 +31107,9 @@ function dispatchMessageReplyPreview(event) {
31066
31107
  const parsed = MessageEventPayloadSchema.safeParse(event.payload);
31067
31108
  return parsed.success && parsed.data.reply_body !== undefined && parsed.data.reply_body !== "" ? parsed.data.reply_body : undefined;
31068
31109
  }
31110
+ function isObject2(value) {
31111
+ return typeof value === "object" && value !== null;
31112
+ }
31069
31113
  function parseDispatchFrame(rawPayload) {
31070
31114
  let value;
31071
31115
  try {
@@ -31073,11 +31117,41 @@ function parseDispatchFrame(rawPayload) {
31073
31117
  } catch {
31074
31118
  return { raw: rawPayload };
31075
31119
  }
31120
+ const targeted = DispatchTargetedFrameSchema.safeParse(value);
31121
+ if (targeted.success) {
31122
+ const wireFrame = value;
31123
+ return {
31124
+ event: targeted.data.event,
31125
+ notify: wireFrame.event.notify,
31126
+ delivery: targeted.data.delivery
31127
+ };
31128
+ }
31129
+ if (isObject2(value) && "delivery" in value) {
31130
+ const recoverable = RecoverableDispatchDeliveryFailureSchema.safeParse(value);
31131
+ if (recoverable.success) {
31132
+ return {
31133
+ raw: value,
31134
+ rejectedDelivery: {
31135
+ attempt: recoverable.data.delivery.attempt,
31136
+ mode: recoverable.data.delivery.mode,
31137
+ messageID: recoverable.data.event.payload.id,
31138
+ issueKey: recoverable.data.event.issue_key,
31139
+ body: recoverable.data.event.payload.body ?? ""
31140
+ },
31141
+ malformedDelivery: true
31142
+ };
31143
+ }
31144
+ return { raw: value, malformedDelivery: true };
31145
+ }
31076
31146
  const parsed = DispatchEventSchema.safeParse(value);
31077
31147
  if (!parsed.success)
31078
31148
  return { raw: value };
31079
31149
  const wireEvent = value;
31080
- return { event: parsed.data, notify: wireEvent.notify };
31150
+ return {
31151
+ event: parsed.data,
31152
+ notify: wireEvent.notify,
31153
+ delivery: undefined
31154
+ };
31081
31155
  }
31082
31156
  function inboundTimestamp(milliseconds) {
31083
31157
  if (milliseconds === undefined)
@@ -31115,6 +31189,10 @@ function renderInbound(raw, sessionID, subject) {
31115
31189
  }
31116
31190
  let dispatchEvent;
31117
31191
  let dispatchIssue;
31192
+ let dispatchReply;
31193
+ let delivery;
31194
+ let rejectedDelivery;
31195
+ let malformedDelivery = false;
31118
31196
  let askQuestion;
31119
31197
  let messageReplyPreview;
31120
31198
  const dispatchRendered = envelope.source === "dispatch" && envelope.payload !== undefined;
@@ -31144,6 +31222,20 @@ function renderInbound(raw, sessionID, subject) {
31144
31222
  }
31145
31223
  askQuestion = dispatchAskQuestion(frame.event);
31146
31224
  messageReplyPreview = dispatchMessageReplyPreview(frame.event);
31225
+ if (frame.event.type === "message.created" && frame.event.issue_key !== null) {
31226
+ const message = MessageEventPayloadSchema.safeParse(frame.event.payload);
31227
+ const requested = DispatchDeliveryRequestSchema.safeParse(frame.delivery);
31228
+ if (message.success && requested.success && message.data.id !== undefined) {
31229
+ delivery = {
31230
+ attempt: requested.data.attempt,
31231
+ mode: requested.data.mode,
31232
+ messageID: message.data.id,
31233
+ issueKey: frame.event.issue_key,
31234
+ body: message.data.body ?? envelope.payload_summary ?? ""
31235
+ };
31236
+ dispatchReply = `dispatch_message(issue="${frame.event.issue_key}", in_reply_to="${message.data.id}", body="...")`;
31237
+ }
31238
+ }
31147
31239
  dispatchEvent = {
31148
31240
  owner: dispatchOwner(frame.event, subject ?? envelope.topic),
31149
31241
  ...frame.event.issue_key === null ? {
@@ -31158,6 +31250,8 @@ function renderInbound(raw, sessionID, subject) {
31158
31250
  };
31159
31251
  } else {
31160
31252
  dispatchEvent = frame.raw;
31253
+ rejectedDelivery = frame.rejectedDelivery;
31254
+ malformedDelivery = frame.malformedDelivery === true;
31161
31255
  }
31162
31256
  }
31163
31257
  }
@@ -31182,7 +31276,7 @@ ${envelope.payload ?? ""}`;
31182
31276
  }
31183
31277
  }
31184
31278
  const role = envelope.sender?.roles?.[0];
31185
- const reply = replyWith(envelope);
31279
+ const reply = dispatchReply ?? replyWith(envelope);
31186
31280
  const sourceIssue = parseIssues.includes("source") || KNOWN_SOURCES[envelope.source] !== undefined ? undefined : `source=${envelope.source}`;
31187
31281
  const unrecognised = [
31188
31282
  ...parseIssues,
@@ -31212,7 +31306,14 @@ ${envelope.payload ?? ""}`;
31212
31306
  },
31213
31307
  ...unrecognised === undefined ? {} : { unrecognised }
31214
31308
  };
31215
- return { skip: false, content: encode3({ envoy: rendered }), envelope };
31309
+ return {
31310
+ skip: false,
31311
+ content: encode3({ envoy: rendered }),
31312
+ envelope,
31313
+ ...delivery === undefined ? {} : { delivery },
31314
+ ...rejectedDelivery === undefined ? {} : { rejectedDelivery },
31315
+ ...malformedDelivery ? { malformedDelivery: true } : {}
31316
+ };
31216
31317
  }
31217
31318
 
31218
31319
  // ../envoy-client/src/dispatch-config.ts
@@ -31876,13 +31977,13 @@ function askId(args) {
31876
31977
  }
31877
31978
  return reference.id;
31878
31979
  }
31879
- function messageReplyTo(args) {
31880
- const replyTo = optionalString(args, "reply_to");
31881
- if (replyTo === undefined || !replyTo.startsWith("dispatch://"))
31882
- return replyTo;
31883
- const reference = parseDispatchRef(replyTo);
31980
+ function messageInReplyTo(args) {
31981
+ const inReplyTo = optionalString(args, "in_reply_to");
31982
+ if (inReplyTo === undefined || !inReplyTo.startsWith("dispatch://"))
31983
+ return inReplyTo;
31984
+ const reference = parseDispatchRef(inReplyTo);
31884
31985
  if (reference?.kind !== "message") {
31885
- throw new Error("reply_to must be a bare message id or a dispatch://.../message/<id> reference");
31986
+ throw new Error("in_reply_to must be a bare message id or a dispatch://.../message/<id> reference");
31886
31987
  }
31887
31988
  return reference.id;
31888
31989
  }
@@ -32330,10 +32431,10 @@ async function executeDispatchTool(input) {
32330
32431
  };
32331
32432
  }
32332
32433
  case "dispatch_message": {
32333
- const replyTo = messageReplyTo(args);
32434
+ const inReplyTo = messageInReplyTo(args);
32334
32435
  const message = await client.message(issue(), {
32335
32436
  body: stringArg(args, "body"),
32336
- ...replyTo === undefined ? {} : { reply_to: replyTo },
32437
+ ...inReplyTo === undefined ? {} : { in_reply_to: inReplyTo },
32337
32438
  actor
32338
32439
  });
32339
32440
  const messageRef = `dispatch://${message.issue_key}/message/${message.id}`;
@@ -32355,8 +32456,10 @@ async function executeDispatchTool(input) {
32355
32456
  ...summary === undefined ? {} : { summary },
32356
32457
  actor
32357
32458
  });
32459
+ const retyped = ops.filter((operation) => operation.op === "retype").length;
32460
+ const versionText = edited.version === null ? "no new version" : `version ${edited.version.number}`;
32358
32461
  return {
32359
- text: edited.version === null ? `Applied ${edited.applied} ops (no new version)` : `Applied ${edited.applied} ops (version ${edited.version.number})`,
32462
+ text: retyped === 0 ? `Applied ${edited.applied} ops (${versionText})` : `Applied ${edited.applied} ops; retyped ${retyped} block${retyped === 1 ? "" : "s"} (${versionText})`,
32360
32463
  details: writeResultDetails(resolved, {
32361
32464
  applied: edited.applied,
32362
32465
  ...edited.version === null ? {} : { version: edited.version.number }
@@ -32708,6 +32811,7 @@ var SessionWireSchema = object({
32708
32811
  title: string2(),
32709
32812
  topics: array(string2()),
32710
32813
  roles: array(string2()).optional(),
32814
+ capabilities: array(string2()).optional(),
32711
32815
  self_subscribed: boolean2(),
32712
32816
  last_seen: number2().int().optional()
32713
32817
  });
@@ -32816,6 +32920,7 @@ function createEnvoyClient(config) {
32816
32920
  topics: expandSubscriptionTopics(input.topics),
32817
32921
  port: input.port,
32818
32922
  title: input.title,
32923
+ ...input.capabilities === undefined ? {} : { capabilities: input.capabilities },
32819
32924
  driving: input.driving,
32820
32925
  ...input.selfSubscribed === undefined ? {} : { self_subscribed: input.selfSubscribed }
32821
32926
  }))),
@@ -33050,6 +33155,26 @@ function envoyExtension(pi) {
33050
33155
  });
33051
33156
  return connection;
33052
33157
  };
33158
+ const postDispatchReply = async (delivery, result) => {
33159
+ if (!dispatchConfig.enabled || dispatchConfig.url === null || dispatchConfig.token === null) {
33160
+ throw new Error("Dispatch reply endpoint is not configured");
33161
+ }
33162
+ const response = await fetch(`${dispatchConfig.url}/api/v1/messages/${delivery.messageID}/reply`, {
33163
+ method: "POST",
33164
+ headers: {
33165
+ Authorization: `Bearer ${dispatchConfig.token}`,
33166
+ "Content-Type": "application/json"
33167
+ },
33168
+ body: JSON.stringify({
33169
+ actor: { kind: "session", id: sessionID },
33170
+ attempt: delivery.attempt,
33171
+ ...result
33172
+ })
33173
+ });
33174
+ if (!response.ok) {
33175
+ throw new Error(`Dispatch reply failed: ${response.status} ${await response.text()}`);
33176
+ }
33177
+ };
33053
33178
  const deliver = async (subject, raw, reply) => {
33054
33179
  const rendered = renderInbound(raw, sessionID, subject);
33055
33180
  for (const topic of subscriptionRemovedTopics(raw, sessionID) ?? [])
@@ -33069,9 +33194,34 @@ function envoyExtension(pi) {
33069
33194
  inbox.pop();
33070
33195
  }
33071
33196
  try {
33072
- pi.sendMessage({ customType: "envoy-message", content: rendered.content, display: true }, { deliverAs: "steer", triggerTurn: true });
33197
+ if (rendered.rejectedDelivery !== undefined) {
33198
+ console.warn(`[envoy] rejecting malformed Dispatch targeted delivery ${rendered.rejectedDelivery.messageID}`);
33199
+ await postDispatchReply(rendered.rejectedDelivery, {
33200
+ error: "Invalid Dispatch targeted delivery frame"
33201
+ });
33202
+ } else if (rendered.malformedDelivery === true) {
33203
+ console.warn("[envoy] dropping malformed Dispatch targeted delivery without a reply address");
33204
+ } else if (rendered.delivery?.mode === "btw") {
33205
+ if (pi.askEphemeral === undefined) {
33206
+ await postDispatchReply(rendered.delivery, {
33207
+ error: "This OMP host does not support BTW delivery"
33208
+ });
33209
+ } else {
33210
+ try {
33211
+ const reply = await pi.askEphemeral({ prompt: rendered.delivery.body });
33212
+ await postDispatchReply(rendered.delivery, { body: reply.replyText });
33213
+ } catch (error) {
33214
+ await postDispatchReply(rendered.delivery, { error: messageFor(error) });
33215
+ }
33216
+ }
33217
+ } else {
33218
+ pi.sendMessage({ customType: "envoy-message", content: rendered.content, display: true }, {
33219
+ deliverAs: rendered.delivery?.mode === "aside" ? "aside" : "steer",
33220
+ triggerTurn: true
33221
+ });
33222
+ }
33073
33223
  } catch (error) {
33074
- console.warn(`[envoy] failed to inject envelope ${envelope?.event_id ?? "unknown"}`, error);
33224
+ console.warn(`[envoy] failed to deliver envelope ${envelope?.event_id ?? "unknown"}`, error);
33075
33225
  throw error;
33076
33226
  }
33077
33227
  if (dedupeKey !== undefined) {
@@ -33150,6 +33300,7 @@ function envoyExtension(pi) {
33150
33300
  topics: [...new Set([agentSubject(sessionID), ...subscriptions.keys()])],
33151
33301
  port: 0,
33152
33302
  title: activeSessionContext?.sessionManager.getSessionName?.() ?? "",
33303
+ capabilities: typeof pi.askEphemeral === "function" ? ["aside", "btw"] : ["aside"],
33153
33304
  driving: false,
33154
33305
  selfSubscribed: true
33155
33306
  });
package/dist/legion.js CHANGED
@@ -29729,10 +29729,20 @@ var CommentEventPayloadSchema = object({
29729
29729
  var MessageEventPayloadSchema = object({
29730
29730
  id: string2().optional(),
29731
29731
  body: string2().optional(),
29732
- reply_to: string2().nullish(),
29732
+ target: string2().nullish(),
29733
+ in_reply_to: string2().nullish(),
29733
29734
  reply_body: string2().optional(),
29734
29735
  author: object({ kind: string2(), id: string2() }).optional()
29735
29736
  });
29737
+ var MessageDeliveryEventPayloadSchema = object({
29738
+ message_id: string2().optional(),
29739
+ attempt: number2().int().positive().optional(),
29740
+ delivery: _enum2(["btw", "aside", "steer"]).optional(),
29741
+ session_id: string2().optional(),
29742
+ title: string2().optional(),
29743
+ state: _enum2(["sent", "failed"]).optional(),
29744
+ error: string2().optional()
29745
+ });
29736
29746
  var ChildStatusEventPayloadSchema = object({
29737
29747
  child_key: string2().optional(),
29738
29748
  from: string2().optional(),
@@ -29826,7 +29836,7 @@ var SPEC_SECTIONS = [
29826
29836
  ];
29827
29837
  var SPEC_WRITING_GUIDANCE = `When writing a spec, use these sections in order: ${SPEC_SECTIONS.join(", ")}. ` + "Write for a reader who has not seen the code: plain sentences, every identifier expanded on " + "first use, no coined shorthand; see skills/dispatch Writing for the human and Writing a spec.";
29828
29838
  var ASK_URGENCIES = ["low", "med", "high", "blocking"];
29829
- var DOC_EDIT_OPS = ["replace", "delete", "insert"];
29839
+ var DOC_EDIT_OPS = ["replace", "delete", "insert", "retype"];
29830
29840
  var dispatchToolSpecs = [
29831
29841
  {
29832
29842
  name: "dispatch_issue",
@@ -29931,12 +29941,12 @@ var dispatchToolSpecs = [
29931
29941
  arguments: (z) => ({
29932
29942
  issue: z.string().describe(ISSUE_REFERENCE),
29933
29943
  body: z.string({ max: 2000 }).describe("Update text, at most 2,000 characters."),
29934
- reply_to: z.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.").optional()
29944
+ in_reply_to: z.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.").optional()
29935
29945
  })
29936
29946
  },
29937
29947
  {
29938
29948
  name: "dispatch_doc_edit",
29939
- description: "Apply deterministic text edits to an issue or project document. Do not use it for review feedback or for reading; use " + "dispatch_comment, dispatch_suggest, or dispatch_doc_read instead. The spec (or any document) holds requirements, " + `design, and decisions - never progress, status, or timestamps. ${OWNER_REFERENCE} ${SPEC_WRITING_GUIDANCE}`,
29949
+ description: "Apply deterministic document edits, including retyping an identified paragraph into a schema-declared typed block. " + "Do not use it for review feedback or for reading; use dispatch_comment, dispatch_suggest, or dispatch_doc_read instead. " + `The spec (or any document) holds requirements, design, and decisions - never progress, status, or timestamps. ${OWNER_REFERENCE} ${SPEC_WRITING_GUIDANCE}`,
29940
29950
  arguments: (z) => ({
29941
29951
  issue: z.string().describe(ISSUE_REFERENCE).optional(),
29942
29952
  project: z.string().describe("Project key owning the document.").optional(),
@@ -29949,7 +29959,10 @@ var dispatchToolSpecs = [
29949
29959
  occurrence: z.number({ int: true, min: 0 }).describe("Optional zero-based match occurrence.").optional(),
29950
29960
  markdown: z.string().describe("Markdown to insert.").optional(),
29951
29961
  after: z.string().describe("Anchor after which to insert.").optional(),
29952
- before: z.string().describe("Anchor before which to insert.").optional()
29962
+ before: z.string().describe("Anchor before which to insert.").optional(),
29963
+ block: z.string().describe("Block id to retype.").optional(),
29964
+ type: z.string().describe("Typed block name for retype.").optional(),
29965
+ attributes: z.unknown().describe("Typed block attributes for retype.").optional()
29953
29966
  })).describe("Flat tagged edits; the server validates fields required for each operation."),
29954
29967
  summary: z.string().describe("Optional named-version summary.").optional()
29955
29968
  }),
@@ -30410,6 +30423,7 @@ function zodSchemaApi(zod) {
30410
30423
  },
30411
30424
  boolean: () => api.boolean(),
30412
30425
  enum: (values) => api.enum(values),
30426
+ unknown: () => api.unknown(),
30413
30427
  array: (item, opts = {}) => {
30414
30428
  let schema = api.array(item);
30415
30429
  if (opts.min !== undefined)
@@ -31456,6 +31470,31 @@ var TolerantInboundEnvelopeSchema = object({
31456
31470
  source: InboundEnvelopeSchema.shape.source.optional().catch(undefined),
31457
31471
  sender: object(tolerantShape(InboundSenderSchema.shape)).optional().catch(undefined)
31458
31472
  }).passthrough();
31473
+ var DispatchDeliveryRequestSchema = object({
31474
+ attempt: number2().int().positive(),
31475
+ mode: _enum2(["btw", "aside", "steer"])
31476
+ });
31477
+ var DispatchTargetedMessagePayloadSchema = object({
31478
+ id: string2(),
31479
+ issue_key: string2(),
31480
+ author: object({ kind: string2(), id: string2() }),
31481
+ body: string2(),
31482
+ target: string2(),
31483
+ in_reply_to: string2().nullable(),
31484
+ deliveries: array(unknown()),
31485
+ created_at: string2()
31486
+ });
31487
+ var DispatchTargetedFrameSchema = object({
31488
+ event: DispatchEventSchema,
31489
+ delivery: DispatchDeliveryRequestSchema
31490
+ }).refine(({ event }) => event.type === "message.created" && DispatchTargetedMessagePayloadSchema.safeParse(event.payload).success, { message: "targeted delivery requires a message.created event" });
31491
+ var RecoverableDispatchDeliveryFailureSchema = object({
31492
+ event: object({
31493
+ issue_key: string2(),
31494
+ payload: object({ id: string2(), body: string2().optional() }).passthrough()
31495
+ }).passthrough(),
31496
+ delivery: DispatchDeliveryRequestSchema
31497
+ });
31459
31498
  function senderLabel(envelope) {
31460
31499
  const sender = envelope.source_session ?? envelope.source ?? "unknown";
31461
31500
  return envelope.sender?.title === undefined ? sender : `${sender} (${envelope.sender.title})`;
@@ -31482,6 +31521,8 @@ var DISPATCH_PAYLOAD_SCHEMAS = {
31482
31521
  "suggestion.accepted": CommentEventPayloadSchema,
31483
31522
  "suggestion.rejected": CommentEventPayloadSchema,
31484
31523
  "message.created": MessageEventPayloadSchema,
31524
+ "message.delivery": MessageDeliveryEventPayloadSchema,
31525
+ "message.answered": MessageEventPayloadSchema,
31485
31526
  "child.status": ChildStatusEventPayloadSchema
31486
31527
  };
31487
31528
  function dispatchOwner(event, topic) {
@@ -31514,6 +31555,9 @@ function dispatchMessageReplyPreview(event) {
31514
31555
  const parsed = MessageEventPayloadSchema.safeParse(event.payload);
31515
31556
  return parsed.success && parsed.data.reply_body !== undefined && parsed.data.reply_body !== "" ? parsed.data.reply_body : undefined;
31516
31557
  }
31558
+ function isObject2(value) {
31559
+ return typeof value === "object" && value !== null;
31560
+ }
31517
31561
  function parseDispatchFrame(rawPayload) {
31518
31562
  let value;
31519
31563
  try {
@@ -31521,11 +31565,41 @@ function parseDispatchFrame(rawPayload) {
31521
31565
  } catch {
31522
31566
  return { raw: rawPayload };
31523
31567
  }
31568
+ const targeted = DispatchTargetedFrameSchema.safeParse(value);
31569
+ if (targeted.success) {
31570
+ const wireFrame = value;
31571
+ return {
31572
+ event: targeted.data.event,
31573
+ notify: wireFrame.event.notify,
31574
+ delivery: targeted.data.delivery
31575
+ };
31576
+ }
31577
+ if (isObject2(value) && "delivery" in value) {
31578
+ const recoverable = RecoverableDispatchDeliveryFailureSchema.safeParse(value);
31579
+ if (recoverable.success) {
31580
+ return {
31581
+ raw: value,
31582
+ rejectedDelivery: {
31583
+ attempt: recoverable.data.delivery.attempt,
31584
+ mode: recoverable.data.delivery.mode,
31585
+ messageID: recoverable.data.event.payload.id,
31586
+ issueKey: recoverable.data.event.issue_key,
31587
+ body: recoverable.data.event.payload.body ?? ""
31588
+ },
31589
+ malformedDelivery: true
31590
+ };
31591
+ }
31592
+ return { raw: value, malformedDelivery: true };
31593
+ }
31524
31594
  const parsed = DispatchEventSchema.safeParse(value);
31525
31595
  if (!parsed.success)
31526
31596
  return { raw: value };
31527
31597
  const wireEvent = value;
31528
- return { event: parsed.data, notify: wireEvent.notify };
31598
+ return {
31599
+ event: parsed.data,
31600
+ notify: wireEvent.notify,
31601
+ delivery: undefined
31602
+ };
31529
31603
  }
31530
31604
  function inboundTimestamp(milliseconds) {
31531
31605
  if (milliseconds === undefined)
@@ -31563,6 +31637,10 @@ function renderInbound(raw, sessionID, subject) {
31563
31637
  }
31564
31638
  let dispatchEvent;
31565
31639
  let dispatchIssue;
31640
+ let dispatchReply;
31641
+ let delivery;
31642
+ let rejectedDelivery;
31643
+ let malformedDelivery = false;
31566
31644
  let askQuestion;
31567
31645
  let messageReplyPreview;
31568
31646
  const dispatchRendered = envelope.source === "dispatch" && envelope.payload !== undefined;
@@ -31592,6 +31670,20 @@ function renderInbound(raw, sessionID, subject) {
31592
31670
  }
31593
31671
  askQuestion = dispatchAskQuestion(frame.event);
31594
31672
  messageReplyPreview = dispatchMessageReplyPreview(frame.event);
31673
+ if (frame.event.type === "message.created" && frame.event.issue_key !== null) {
31674
+ const message = MessageEventPayloadSchema.safeParse(frame.event.payload);
31675
+ const requested = DispatchDeliveryRequestSchema.safeParse(frame.delivery);
31676
+ if (message.success && requested.success && message.data.id !== undefined) {
31677
+ delivery = {
31678
+ attempt: requested.data.attempt,
31679
+ mode: requested.data.mode,
31680
+ messageID: message.data.id,
31681
+ issueKey: frame.event.issue_key,
31682
+ body: message.data.body ?? envelope.payload_summary ?? ""
31683
+ };
31684
+ dispatchReply = `dispatch_message(issue="${frame.event.issue_key}", in_reply_to="${message.data.id}", body="...")`;
31685
+ }
31686
+ }
31595
31687
  dispatchEvent = {
31596
31688
  owner: dispatchOwner(frame.event, subject ?? envelope.topic),
31597
31689
  ...frame.event.issue_key === null ? {
@@ -31606,6 +31698,8 @@ function renderInbound(raw, sessionID, subject) {
31606
31698
  };
31607
31699
  } else {
31608
31700
  dispatchEvent = frame.raw;
31701
+ rejectedDelivery = frame.rejectedDelivery;
31702
+ malformedDelivery = frame.malformedDelivery === true;
31609
31703
  }
31610
31704
  }
31611
31705
  }
@@ -31630,7 +31724,7 @@ ${envelope.payload ?? ""}`;
31630
31724
  }
31631
31725
  }
31632
31726
  const role = envelope.sender?.roles?.[0];
31633
- const reply = replyWith(envelope);
31727
+ const reply = dispatchReply ?? replyWith(envelope);
31634
31728
  const sourceIssue = parseIssues.includes("source") || KNOWN_SOURCES[envelope.source] !== undefined ? undefined : `source=${envelope.source}`;
31635
31729
  const unrecognised = [
31636
31730
  ...parseIssues,
@@ -31660,7 +31754,14 @@ ${envelope.payload ?? ""}`;
31660
31754
  },
31661
31755
  ...unrecognised === undefined ? {} : { unrecognised }
31662
31756
  };
31663
- return { skip: false, content: encode3({ envoy: rendered }), envelope };
31757
+ return {
31758
+ skip: false,
31759
+ content: encode3({ envoy: rendered }),
31760
+ envelope,
31761
+ ...delivery === undefined ? {} : { delivery },
31762
+ ...rejectedDelivery === undefined ? {} : { rejectedDelivery },
31763
+ ...malformedDelivery ? { malformedDelivery: true } : {}
31764
+ };
31664
31765
  }
31665
31766
 
31666
31767
  // ../envoy-client/src/dispatch-config.ts
@@ -32302,13 +32403,13 @@ function askId(args) {
32302
32403
  }
32303
32404
  return reference.id;
32304
32405
  }
32305
- function messageReplyTo(args) {
32306
- const replyTo = optionalString(args, "reply_to");
32307
- if (replyTo === undefined || !replyTo.startsWith("dispatch://"))
32308
- return replyTo;
32309
- const reference = parseDispatchRef(replyTo);
32406
+ function messageInReplyTo(args) {
32407
+ const inReplyTo = optionalString(args, "in_reply_to");
32408
+ if (inReplyTo === undefined || !inReplyTo.startsWith("dispatch://"))
32409
+ return inReplyTo;
32410
+ const reference = parseDispatchRef(inReplyTo);
32310
32411
  if (reference?.kind !== "message") {
32311
- throw new Error("reply_to must be a bare message id or a dispatch://.../message/<id> reference");
32412
+ throw new Error("in_reply_to must be a bare message id or a dispatch://.../message/<id> reference");
32312
32413
  }
32313
32414
  return reference.id;
32314
32415
  }
@@ -32756,10 +32857,10 @@ async function executeDispatchTool(input) {
32756
32857
  };
32757
32858
  }
32758
32859
  case "dispatch_message": {
32759
- const replyTo = messageReplyTo(args);
32860
+ const inReplyTo = messageInReplyTo(args);
32760
32861
  const message = await client.message(issue(), {
32761
32862
  body: stringArg(args, "body"),
32762
- ...replyTo === undefined ? {} : { reply_to: replyTo },
32863
+ ...inReplyTo === undefined ? {} : { in_reply_to: inReplyTo },
32763
32864
  actor
32764
32865
  });
32765
32866
  const messageRef = `dispatch://${message.issue_key}/message/${message.id}`;
@@ -32781,8 +32882,10 @@ async function executeDispatchTool(input) {
32781
32882
  ...summary === undefined ? {} : { summary },
32782
32883
  actor
32783
32884
  });
32885
+ const retyped = ops.filter((operation) => operation.op === "retype").length;
32886
+ const versionText = edited.version === null ? "no new version" : `version ${edited.version.number}`;
32784
32887
  return {
32785
- text: edited.version === null ? `Applied ${edited.applied} ops (no new version)` : `Applied ${edited.applied} ops (version ${edited.version.number})`,
32888
+ text: retyped === 0 ? `Applied ${edited.applied} ops (${versionText})` : `Applied ${edited.applied} ops; retyped ${retyped} block${retyped === 1 ? "" : "s"} (${versionText})`,
32786
32889
  details: writeResultDetails(resolved, {
32787
32890
  applied: edited.applied,
32788
32891
  ...edited.version === null ? {} : { version: edited.version.number }
@@ -33134,6 +33237,7 @@ var SessionWireSchema = object({
33134
33237
  title: string2(),
33135
33238
  topics: array(string2()),
33136
33239
  roles: array(string2()).optional(),
33240
+ capabilities: array(string2()).optional(),
33137
33241
  self_subscribed: boolean2(),
33138
33242
  last_seen: number2().int().optional()
33139
33243
  });
@@ -33242,6 +33346,7 @@ function createEnvoyClient(config) {
33242
33346
  topics: expandSubscriptionTopics(input.topics),
33243
33347
  port: input.port,
33244
33348
  title: input.title,
33349
+ ...input.capabilities === undefined ? {} : { capabilities: input.capabilities },
33245
33350
  driving: input.driving,
33246
33351
  ...input.selfSubscribed === undefined ? {} : { self_subscribed: input.selfSubscribed }
33247
33352
  }))),
@@ -33468,6 +33573,26 @@ function envoyExtension(pi) {
33468
33573
  });
33469
33574
  return connection;
33470
33575
  };
33576
+ const postDispatchReply = async (delivery, result) => {
33577
+ if (!dispatchConfig.enabled || dispatchConfig.url === null || dispatchConfig.token === null) {
33578
+ throw new Error("Dispatch reply endpoint is not configured");
33579
+ }
33580
+ const response = await fetch(`${dispatchConfig.url}/api/v1/messages/${delivery.messageID}/reply`, {
33581
+ method: "POST",
33582
+ headers: {
33583
+ Authorization: `Bearer ${dispatchConfig.token}`,
33584
+ "Content-Type": "application/json"
33585
+ },
33586
+ body: JSON.stringify({
33587
+ actor: { kind: "session", id: sessionID },
33588
+ attempt: delivery.attempt,
33589
+ ...result
33590
+ })
33591
+ });
33592
+ if (!response.ok) {
33593
+ throw new Error(`Dispatch reply failed: ${response.status} ${await response.text()}`);
33594
+ }
33595
+ };
33471
33596
  const deliver = async (subject, raw, reply) => {
33472
33597
  const rendered = renderInbound(raw, sessionID, subject);
33473
33598
  for (const topic of subscriptionRemovedTopics(raw, sessionID) ?? [])
@@ -33487,9 +33612,34 @@ function envoyExtension(pi) {
33487
33612
  inbox.pop();
33488
33613
  }
33489
33614
  try {
33490
- pi.sendMessage({ customType: "envoy-message", content: rendered.content, display: true }, { deliverAs: "steer", triggerTurn: true });
33615
+ if (rendered.rejectedDelivery !== undefined) {
33616
+ console.warn(`[envoy] rejecting malformed Dispatch targeted delivery ${rendered.rejectedDelivery.messageID}`);
33617
+ await postDispatchReply(rendered.rejectedDelivery, {
33618
+ error: "Invalid Dispatch targeted delivery frame"
33619
+ });
33620
+ } else if (rendered.malformedDelivery === true) {
33621
+ console.warn("[envoy] dropping malformed Dispatch targeted delivery without a reply address");
33622
+ } else if (rendered.delivery?.mode === "btw") {
33623
+ if (pi.askEphemeral === undefined) {
33624
+ await postDispatchReply(rendered.delivery, {
33625
+ error: "This OMP host does not support BTW delivery"
33626
+ });
33627
+ } else {
33628
+ try {
33629
+ const reply = await pi.askEphemeral({ prompt: rendered.delivery.body });
33630
+ await postDispatchReply(rendered.delivery, { body: reply.replyText });
33631
+ } catch (error) {
33632
+ await postDispatchReply(rendered.delivery, { error: messageFor(error) });
33633
+ }
33634
+ }
33635
+ } else {
33636
+ pi.sendMessage({ customType: "envoy-message", content: rendered.content, display: true }, {
33637
+ deliverAs: rendered.delivery?.mode === "aside" ? "aside" : "steer",
33638
+ triggerTurn: true
33639
+ });
33640
+ }
33491
33641
  } catch (error) {
33492
- console.warn(`[envoy] failed to inject envelope ${envelope?.event_id ?? "unknown"}`, error);
33642
+ console.warn(`[envoy] failed to deliver envelope ${envelope?.event_id ?? "unknown"}`, error);
33493
33643
  throw error;
33494
33644
  }
33495
33645
  if (dedupeKey !== undefined) {
@@ -33568,6 +33718,7 @@ function envoyExtension(pi) {
33568
33718
  topics: [...new Set([agentSubject(sessionID), ...subscriptions.keys()])],
33569
33719
  port: 0,
33570
33720
  title: activeSessionContext?.sessionManager.getSessionName?.() ?? "",
33721
+ capabilities: typeof pi.askEphemeral === "function" ? ["aside", "btw"] : ["aside"],
33571
33722
  driving: false,
33572
33723
  selfSubscribed: true
33573
33724
  });
@@ -199,13 +199,16 @@ exact `EditOp` shape:
199
199
 
200
200
  ```ts
201
201
  type EditOp = {
202
- op: "replace" | "delete" | "insert";
202
+ op: "replace" | "delete" | "insert" | "retype";
203
203
  find?: string;
204
204
  with?: string;
205
205
  occurrence?: number;
206
206
  markdown?: string;
207
207
  after?: string;
208
208
  before?: string;
209
+ block?: string;
210
+ type?: string;
211
+ attributes?: Record<string, unknown>;
209
212
  };
210
213
  ```
211
214
 
@@ -221,6 +224,9 @@ and deleting a cell's quoted text removes only that text.
221
224
 
222
225
  Use `replace` for inline continuation. Use zero-based `occurrence` for a repeated target; re-read a missing or ambiguous target before
223
226
  retrying. Pass `summary` to name the version when recording a decision.
227
+ `retype` turns the paragraph with `block` into the named typed `type` in place. It keeps the
228
+ block id and uses `attributes` for client-owned typed attributes. Use it when an existing
229
+ paragraph is the question that should become a decision.
224
230
 
225
231
  ## Typed blocks
226
232
 
@@ -236,6 +242,19 @@ quoted: `:::callout{kind="warning" title="Risk"}`. Do not write Pandoc-style `::
236
242
  block. Do not set attributes the schema marks `server: true`; the server ignores them and reasserts
237
243
  its authoritative value at settlement.
238
244
 
245
+ Questions about a document must be `ask` blocks, never an `Open questions` prose section. An ask
246
+ body is one or more question paragraphs followed by an optional bullet list of options, where each
247
+ item is `Label: description`. For example:
248
+
249
+ ```md
250
+ :::ask{urgency="high" multiple="false"}
251
+ Should we ship the migration?
252
+
253
+ - Ship: Release the verified change.
254
+ - Hold: Wait for another review.
255
+ :::
256
+ ```
257
+
239
258
  ## Comments and suggestions
240
259
 
241
260
  Add feedback with:
@@ -298,6 +317,24 @@ dispatch_message({ issue, body })
298
317
  It returns `details` `{ issue, topic, message }`. `body` is capped at 2,000 characters. A message is not a decision
299
318
  (`dispatch_ask`) or document feedback (`dispatch_comment`), and it does not wake anyone unless the issue is routed.
300
319
 
320
+ ### Targeted agent messages
321
+
322
+ A human can target the issue message at a live Envoy session or role as **BTW**, **Aside**, or
323
+ **Steer**. The incoming Dispatch frame names the issue and includes a `reply_with` instruction;
324
+ reply on the same open issue with the existing tool, never a new targeted send:
325
+
326
+ ```ts
327
+ dispatch_message({
328
+ issue: "CORE-1",
329
+ in_reply_to: "<targeted-message-id>",
330
+ body: "The requested answer.",
331
+ })
332
+ ```
333
+
334
+ `in_reply_to` correlates the answer under the human's message in its Conversation card. A BTW
335
+ delivery can post its answer automatically; use this call when the frame asks the primary agent to
336
+ reply. Agent bearers cannot set `target` or `delivery`.
337
+
301
338
  ## What comes back
302
339
 
303
340
  A write result's `details.topic` subscribes the host to its owner, and the first such subscription on an issue or document also tells
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/pi-legion-envoy",
3
- "version": "1.9.0",
3
+ "version": "1.11.0",
4
4
  "type": "module",
5
5
  "omp": {
6
6
  "extensions": [