@sjawhar/pi-legion-envoy 1.9.0 → 1.10.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(),
@@ -29932,7 +29942,7 @@ 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
  {
@@ -31008,6 +31018,31 @@ var TolerantInboundEnvelopeSchema = object({
31008
31018
  source: InboundEnvelopeSchema.shape.source.optional().catch(undefined),
31009
31019
  sender: object(tolerantShape(InboundSenderSchema.shape)).optional().catch(undefined)
31010
31020
  }).passthrough();
31021
+ var DispatchDeliveryRequestSchema = object({
31022
+ attempt: number2().int().positive(),
31023
+ mode: _enum2(["btw", "aside", "steer"])
31024
+ });
31025
+ var DispatchTargetedMessagePayloadSchema = object({
31026
+ id: string2(),
31027
+ issue_key: string2(),
31028
+ author: object({ kind: string2(), id: string2() }),
31029
+ body: string2(),
31030
+ target: string2(),
31031
+ in_reply_to: string2().nullable(),
31032
+ deliveries: array(unknown()),
31033
+ created_at: string2()
31034
+ });
31035
+ var DispatchTargetedFrameSchema = object({
31036
+ event: DispatchEventSchema,
31037
+ delivery: DispatchDeliveryRequestSchema
31038
+ }).refine(({ event }) => event.type === "message.created" && DispatchTargetedMessagePayloadSchema.safeParse(event.payload).success, { message: "targeted delivery requires a message.created event" });
31039
+ var RecoverableDispatchDeliveryFailureSchema = object({
31040
+ event: object({
31041
+ issue_key: string2(),
31042
+ payload: object({ id: string2(), body: string2().optional() }).passthrough()
31043
+ }).passthrough(),
31044
+ delivery: DispatchDeliveryRequestSchema
31045
+ });
31011
31046
  function senderLabel(envelope) {
31012
31047
  const sender = envelope.source_session ?? envelope.source ?? "unknown";
31013
31048
  return envelope.sender?.title === undefined ? sender : `${sender} (${envelope.sender.title})`;
@@ -31034,6 +31069,8 @@ var DISPATCH_PAYLOAD_SCHEMAS = {
31034
31069
  "suggestion.accepted": CommentEventPayloadSchema,
31035
31070
  "suggestion.rejected": CommentEventPayloadSchema,
31036
31071
  "message.created": MessageEventPayloadSchema,
31072
+ "message.delivery": MessageDeliveryEventPayloadSchema,
31073
+ "message.answered": MessageEventPayloadSchema,
31037
31074
  "child.status": ChildStatusEventPayloadSchema
31038
31075
  };
31039
31076
  function dispatchOwner(event, topic) {
@@ -31066,6 +31103,9 @@ function dispatchMessageReplyPreview(event) {
31066
31103
  const parsed = MessageEventPayloadSchema.safeParse(event.payload);
31067
31104
  return parsed.success && parsed.data.reply_body !== undefined && parsed.data.reply_body !== "" ? parsed.data.reply_body : undefined;
31068
31105
  }
31106
+ function isObject2(value) {
31107
+ return typeof value === "object" && value !== null;
31108
+ }
31069
31109
  function parseDispatchFrame(rawPayload) {
31070
31110
  let value;
31071
31111
  try {
@@ -31073,11 +31113,41 @@ function parseDispatchFrame(rawPayload) {
31073
31113
  } catch {
31074
31114
  return { raw: rawPayload };
31075
31115
  }
31116
+ const targeted = DispatchTargetedFrameSchema.safeParse(value);
31117
+ if (targeted.success) {
31118
+ const wireFrame = value;
31119
+ return {
31120
+ event: targeted.data.event,
31121
+ notify: wireFrame.event.notify,
31122
+ delivery: targeted.data.delivery
31123
+ };
31124
+ }
31125
+ if (isObject2(value) && "delivery" in value) {
31126
+ const recoverable = RecoverableDispatchDeliveryFailureSchema.safeParse(value);
31127
+ if (recoverable.success) {
31128
+ return {
31129
+ raw: value,
31130
+ rejectedDelivery: {
31131
+ attempt: recoverable.data.delivery.attempt,
31132
+ mode: recoverable.data.delivery.mode,
31133
+ messageID: recoverable.data.event.payload.id,
31134
+ issueKey: recoverable.data.event.issue_key,
31135
+ body: recoverable.data.event.payload.body ?? ""
31136
+ },
31137
+ malformedDelivery: true
31138
+ };
31139
+ }
31140
+ return { raw: value, malformedDelivery: true };
31141
+ }
31076
31142
  const parsed = DispatchEventSchema.safeParse(value);
31077
31143
  if (!parsed.success)
31078
31144
  return { raw: value };
31079
31145
  const wireEvent = value;
31080
- return { event: parsed.data, notify: wireEvent.notify };
31146
+ return {
31147
+ event: parsed.data,
31148
+ notify: wireEvent.notify,
31149
+ delivery: undefined
31150
+ };
31081
31151
  }
31082
31152
  function inboundTimestamp(milliseconds) {
31083
31153
  if (milliseconds === undefined)
@@ -31115,6 +31185,10 @@ function renderInbound(raw, sessionID, subject) {
31115
31185
  }
31116
31186
  let dispatchEvent;
31117
31187
  let dispatchIssue;
31188
+ let dispatchReply;
31189
+ let delivery;
31190
+ let rejectedDelivery;
31191
+ let malformedDelivery = false;
31118
31192
  let askQuestion;
31119
31193
  let messageReplyPreview;
31120
31194
  const dispatchRendered = envelope.source === "dispatch" && envelope.payload !== undefined;
@@ -31144,6 +31218,20 @@ function renderInbound(raw, sessionID, subject) {
31144
31218
  }
31145
31219
  askQuestion = dispatchAskQuestion(frame.event);
31146
31220
  messageReplyPreview = dispatchMessageReplyPreview(frame.event);
31221
+ if (frame.event.type === "message.created" && frame.event.issue_key !== null) {
31222
+ const message = MessageEventPayloadSchema.safeParse(frame.event.payload);
31223
+ const requested = DispatchDeliveryRequestSchema.safeParse(frame.delivery);
31224
+ if (message.success && requested.success && message.data.id !== undefined) {
31225
+ delivery = {
31226
+ attempt: requested.data.attempt,
31227
+ mode: requested.data.mode,
31228
+ messageID: message.data.id,
31229
+ issueKey: frame.event.issue_key,
31230
+ body: message.data.body ?? envelope.payload_summary ?? ""
31231
+ };
31232
+ dispatchReply = `dispatch_message(issue="${frame.event.issue_key}", in_reply_to="${message.data.id}", body="...")`;
31233
+ }
31234
+ }
31147
31235
  dispatchEvent = {
31148
31236
  owner: dispatchOwner(frame.event, subject ?? envelope.topic),
31149
31237
  ...frame.event.issue_key === null ? {
@@ -31158,6 +31246,8 @@ function renderInbound(raw, sessionID, subject) {
31158
31246
  };
31159
31247
  } else {
31160
31248
  dispatchEvent = frame.raw;
31249
+ rejectedDelivery = frame.rejectedDelivery;
31250
+ malformedDelivery = frame.malformedDelivery === true;
31161
31251
  }
31162
31252
  }
31163
31253
  }
@@ -31182,7 +31272,7 @@ ${envelope.payload ?? ""}`;
31182
31272
  }
31183
31273
  }
31184
31274
  const role = envelope.sender?.roles?.[0];
31185
- const reply = replyWith(envelope);
31275
+ const reply = dispatchReply ?? replyWith(envelope);
31186
31276
  const sourceIssue = parseIssues.includes("source") || KNOWN_SOURCES[envelope.source] !== undefined ? undefined : `source=${envelope.source}`;
31187
31277
  const unrecognised = [
31188
31278
  ...parseIssues,
@@ -31212,7 +31302,14 @@ ${envelope.payload ?? ""}`;
31212
31302
  },
31213
31303
  ...unrecognised === undefined ? {} : { unrecognised }
31214
31304
  };
31215
- return { skip: false, content: encode3({ envoy: rendered }), envelope };
31305
+ return {
31306
+ skip: false,
31307
+ content: encode3({ envoy: rendered }),
31308
+ envelope,
31309
+ ...delivery === undefined ? {} : { delivery },
31310
+ ...rejectedDelivery === undefined ? {} : { rejectedDelivery },
31311
+ ...malformedDelivery ? { malformedDelivery: true } : {}
31312
+ };
31216
31313
  }
31217
31314
 
31218
31315
  // ../envoy-client/src/dispatch-config.ts
@@ -31876,13 +31973,13 @@ function askId(args) {
31876
31973
  }
31877
31974
  return reference.id;
31878
31975
  }
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);
31976
+ function messageInReplyTo(args) {
31977
+ const inReplyTo = optionalString(args, "in_reply_to");
31978
+ if (inReplyTo === undefined || !inReplyTo.startsWith("dispatch://"))
31979
+ return inReplyTo;
31980
+ const reference = parseDispatchRef(inReplyTo);
31884
31981
  if (reference?.kind !== "message") {
31885
- throw new Error("reply_to must be a bare message id or a dispatch://.../message/<id> reference");
31982
+ throw new Error("in_reply_to must be a bare message id or a dispatch://.../message/<id> reference");
31886
31983
  }
31887
31984
  return reference.id;
31888
31985
  }
@@ -32330,10 +32427,10 @@ async function executeDispatchTool(input) {
32330
32427
  };
32331
32428
  }
32332
32429
  case "dispatch_message": {
32333
- const replyTo = messageReplyTo(args);
32430
+ const inReplyTo = messageInReplyTo(args);
32334
32431
  const message = await client.message(issue(), {
32335
32432
  body: stringArg(args, "body"),
32336
- ...replyTo === undefined ? {} : { reply_to: replyTo },
32433
+ ...inReplyTo === undefined ? {} : { in_reply_to: inReplyTo },
32337
32434
  actor
32338
32435
  });
32339
32436
  const messageRef = `dispatch://${message.issue_key}/message/${message.id}`;
@@ -32708,6 +32805,7 @@ var SessionWireSchema = object({
32708
32805
  title: string2(),
32709
32806
  topics: array(string2()),
32710
32807
  roles: array(string2()).optional(),
32808
+ capabilities: array(string2()).optional(),
32711
32809
  self_subscribed: boolean2(),
32712
32810
  last_seen: number2().int().optional()
32713
32811
  });
@@ -32816,6 +32914,7 @@ function createEnvoyClient(config) {
32816
32914
  topics: expandSubscriptionTopics(input.topics),
32817
32915
  port: input.port,
32818
32916
  title: input.title,
32917
+ ...input.capabilities === undefined ? {} : { capabilities: input.capabilities },
32819
32918
  driving: input.driving,
32820
32919
  ...input.selfSubscribed === undefined ? {} : { self_subscribed: input.selfSubscribed }
32821
32920
  }))),
@@ -33050,6 +33149,26 @@ function envoyExtension(pi) {
33050
33149
  });
33051
33150
  return connection;
33052
33151
  };
33152
+ const postDispatchReply = async (delivery, result) => {
33153
+ if (!dispatchConfig.enabled || dispatchConfig.url === null || dispatchConfig.token === null) {
33154
+ throw new Error("Dispatch reply endpoint is not configured");
33155
+ }
33156
+ const response = await fetch(`${dispatchConfig.url}/api/v1/messages/${delivery.messageID}/reply`, {
33157
+ method: "POST",
33158
+ headers: {
33159
+ Authorization: `Bearer ${dispatchConfig.token}`,
33160
+ "Content-Type": "application/json"
33161
+ },
33162
+ body: JSON.stringify({
33163
+ actor: { kind: "session", id: sessionID },
33164
+ attempt: delivery.attempt,
33165
+ ...result
33166
+ })
33167
+ });
33168
+ if (!response.ok) {
33169
+ throw new Error(`Dispatch reply failed: ${response.status} ${await response.text()}`);
33170
+ }
33171
+ };
33053
33172
  const deliver = async (subject, raw, reply) => {
33054
33173
  const rendered = renderInbound(raw, sessionID, subject);
33055
33174
  for (const topic of subscriptionRemovedTopics(raw, sessionID) ?? [])
@@ -33069,9 +33188,34 @@ function envoyExtension(pi) {
33069
33188
  inbox.pop();
33070
33189
  }
33071
33190
  try {
33072
- pi.sendMessage({ customType: "envoy-message", content: rendered.content, display: true }, { deliverAs: "steer", triggerTurn: true });
33191
+ if (rendered.rejectedDelivery !== undefined) {
33192
+ console.warn(`[envoy] rejecting malformed Dispatch targeted delivery ${rendered.rejectedDelivery.messageID}`);
33193
+ await postDispatchReply(rendered.rejectedDelivery, {
33194
+ error: "Invalid Dispatch targeted delivery frame"
33195
+ });
33196
+ } else if (rendered.malformedDelivery === true) {
33197
+ console.warn("[envoy] dropping malformed Dispatch targeted delivery without a reply address");
33198
+ } else if (rendered.delivery?.mode === "btw") {
33199
+ if (pi.askEphemeral === undefined) {
33200
+ await postDispatchReply(rendered.delivery, {
33201
+ error: "This OMP host does not support BTW delivery"
33202
+ });
33203
+ } else {
33204
+ try {
33205
+ const reply = await pi.askEphemeral({ prompt: rendered.delivery.body });
33206
+ await postDispatchReply(rendered.delivery, { body: reply.replyText });
33207
+ } catch (error) {
33208
+ await postDispatchReply(rendered.delivery, { error: messageFor(error) });
33209
+ }
33210
+ }
33211
+ } else {
33212
+ pi.sendMessage({ customType: "envoy-message", content: rendered.content, display: true }, {
33213
+ deliverAs: rendered.delivery?.mode === "aside" ? "aside" : "steer",
33214
+ triggerTurn: true
33215
+ });
33216
+ }
33073
33217
  } catch (error) {
33074
- console.warn(`[envoy] failed to inject envelope ${envelope?.event_id ?? "unknown"}`, error);
33218
+ console.warn(`[envoy] failed to deliver envelope ${envelope?.event_id ?? "unknown"}`, error);
33075
33219
  throw error;
33076
33220
  }
33077
33221
  if (dedupeKey !== undefined) {
@@ -33150,6 +33294,7 @@ function envoyExtension(pi) {
33150
33294
  topics: [...new Set([agentSubject(sessionID), ...subscriptions.keys()])],
33151
33295
  port: 0,
33152
33296
  title: activeSessionContext?.sessionManager.getSessionName?.() ?? "",
33297
+ capabilities: typeof pi.askEphemeral === "function" ? ["aside", "btw"] : ["aside"],
33153
33298
  driving: false,
33154
33299
  selfSubscribed: true
33155
33300
  });
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(),
@@ -29931,7 +29941,7 @@ 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
  {
@@ -31456,6 +31466,31 @@ var TolerantInboundEnvelopeSchema = object({
31456
31466
  source: InboundEnvelopeSchema.shape.source.optional().catch(undefined),
31457
31467
  sender: object(tolerantShape(InboundSenderSchema.shape)).optional().catch(undefined)
31458
31468
  }).passthrough();
31469
+ var DispatchDeliveryRequestSchema = object({
31470
+ attempt: number2().int().positive(),
31471
+ mode: _enum2(["btw", "aside", "steer"])
31472
+ });
31473
+ var DispatchTargetedMessagePayloadSchema = object({
31474
+ id: string2(),
31475
+ issue_key: string2(),
31476
+ author: object({ kind: string2(), id: string2() }),
31477
+ body: string2(),
31478
+ target: string2(),
31479
+ in_reply_to: string2().nullable(),
31480
+ deliveries: array(unknown()),
31481
+ created_at: string2()
31482
+ });
31483
+ var DispatchTargetedFrameSchema = object({
31484
+ event: DispatchEventSchema,
31485
+ delivery: DispatchDeliveryRequestSchema
31486
+ }).refine(({ event }) => event.type === "message.created" && DispatchTargetedMessagePayloadSchema.safeParse(event.payload).success, { message: "targeted delivery requires a message.created event" });
31487
+ var RecoverableDispatchDeliveryFailureSchema = object({
31488
+ event: object({
31489
+ issue_key: string2(),
31490
+ payload: object({ id: string2(), body: string2().optional() }).passthrough()
31491
+ }).passthrough(),
31492
+ delivery: DispatchDeliveryRequestSchema
31493
+ });
31459
31494
  function senderLabel(envelope) {
31460
31495
  const sender = envelope.source_session ?? envelope.source ?? "unknown";
31461
31496
  return envelope.sender?.title === undefined ? sender : `${sender} (${envelope.sender.title})`;
@@ -31482,6 +31517,8 @@ var DISPATCH_PAYLOAD_SCHEMAS = {
31482
31517
  "suggestion.accepted": CommentEventPayloadSchema,
31483
31518
  "suggestion.rejected": CommentEventPayloadSchema,
31484
31519
  "message.created": MessageEventPayloadSchema,
31520
+ "message.delivery": MessageDeliveryEventPayloadSchema,
31521
+ "message.answered": MessageEventPayloadSchema,
31485
31522
  "child.status": ChildStatusEventPayloadSchema
31486
31523
  };
31487
31524
  function dispatchOwner(event, topic) {
@@ -31514,6 +31551,9 @@ function dispatchMessageReplyPreview(event) {
31514
31551
  const parsed = MessageEventPayloadSchema.safeParse(event.payload);
31515
31552
  return parsed.success && parsed.data.reply_body !== undefined && parsed.data.reply_body !== "" ? parsed.data.reply_body : undefined;
31516
31553
  }
31554
+ function isObject2(value) {
31555
+ return typeof value === "object" && value !== null;
31556
+ }
31517
31557
  function parseDispatchFrame(rawPayload) {
31518
31558
  let value;
31519
31559
  try {
@@ -31521,11 +31561,41 @@ function parseDispatchFrame(rawPayload) {
31521
31561
  } catch {
31522
31562
  return { raw: rawPayload };
31523
31563
  }
31564
+ const targeted = DispatchTargetedFrameSchema.safeParse(value);
31565
+ if (targeted.success) {
31566
+ const wireFrame = value;
31567
+ return {
31568
+ event: targeted.data.event,
31569
+ notify: wireFrame.event.notify,
31570
+ delivery: targeted.data.delivery
31571
+ };
31572
+ }
31573
+ if (isObject2(value) && "delivery" in value) {
31574
+ const recoverable = RecoverableDispatchDeliveryFailureSchema.safeParse(value);
31575
+ if (recoverable.success) {
31576
+ return {
31577
+ raw: value,
31578
+ rejectedDelivery: {
31579
+ attempt: recoverable.data.delivery.attempt,
31580
+ mode: recoverable.data.delivery.mode,
31581
+ messageID: recoverable.data.event.payload.id,
31582
+ issueKey: recoverable.data.event.issue_key,
31583
+ body: recoverable.data.event.payload.body ?? ""
31584
+ },
31585
+ malformedDelivery: true
31586
+ };
31587
+ }
31588
+ return { raw: value, malformedDelivery: true };
31589
+ }
31524
31590
  const parsed = DispatchEventSchema.safeParse(value);
31525
31591
  if (!parsed.success)
31526
31592
  return { raw: value };
31527
31593
  const wireEvent = value;
31528
- return { event: parsed.data, notify: wireEvent.notify };
31594
+ return {
31595
+ event: parsed.data,
31596
+ notify: wireEvent.notify,
31597
+ delivery: undefined
31598
+ };
31529
31599
  }
31530
31600
  function inboundTimestamp(milliseconds) {
31531
31601
  if (milliseconds === undefined)
@@ -31563,6 +31633,10 @@ function renderInbound(raw, sessionID, subject) {
31563
31633
  }
31564
31634
  let dispatchEvent;
31565
31635
  let dispatchIssue;
31636
+ let dispatchReply;
31637
+ let delivery;
31638
+ let rejectedDelivery;
31639
+ let malformedDelivery = false;
31566
31640
  let askQuestion;
31567
31641
  let messageReplyPreview;
31568
31642
  const dispatchRendered = envelope.source === "dispatch" && envelope.payload !== undefined;
@@ -31592,6 +31666,20 @@ function renderInbound(raw, sessionID, subject) {
31592
31666
  }
31593
31667
  askQuestion = dispatchAskQuestion(frame.event);
31594
31668
  messageReplyPreview = dispatchMessageReplyPreview(frame.event);
31669
+ if (frame.event.type === "message.created" && frame.event.issue_key !== null) {
31670
+ const message = MessageEventPayloadSchema.safeParse(frame.event.payload);
31671
+ const requested = DispatchDeliveryRequestSchema.safeParse(frame.delivery);
31672
+ if (message.success && requested.success && message.data.id !== undefined) {
31673
+ delivery = {
31674
+ attempt: requested.data.attempt,
31675
+ mode: requested.data.mode,
31676
+ messageID: message.data.id,
31677
+ issueKey: frame.event.issue_key,
31678
+ body: message.data.body ?? envelope.payload_summary ?? ""
31679
+ };
31680
+ dispatchReply = `dispatch_message(issue="${frame.event.issue_key}", in_reply_to="${message.data.id}", body="...")`;
31681
+ }
31682
+ }
31595
31683
  dispatchEvent = {
31596
31684
  owner: dispatchOwner(frame.event, subject ?? envelope.topic),
31597
31685
  ...frame.event.issue_key === null ? {
@@ -31606,6 +31694,8 @@ function renderInbound(raw, sessionID, subject) {
31606
31694
  };
31607
31695
  } else {
31608
31696
  dispatchEvent = frame.raw;
31697
+ rejectedDelivery = frame.rejectedDelivery;
31698
+ malformedDelivery = frame.malformedDelivery === true;
31609
31699
  }
31610
31700
  }
31611
31701
  }
@@ -31630,7 +31720,7 @@ ${envelope.payload ?? ""}`;
31630
31720
  }
31631
31721
  }
31632
31722
  const role = envelope.sender?.roles?.[0];
31633
- const reply = replyWith(envelope);
31723
+ const reply = dispatchReply ?? replyWith(envelope);
31634
31724
  const sourceIssue = parseIssues.includes("source") || KNOWN_SOURCES[envelope.source] !== undefined ? undefined : `source=${envelope.source}`;
31635
31725
  const unrecognised = [
31636
31726
  ...parseIssues,
@@ -31660,7 +31750,14 @@ ${envelope.payload ?? ""}`;
31660
31750
  },
31661
31751
  ...unrecognised === undefined ? {} : { unrecognised }
31662
31752
  };
31663
- return { skip: false, content: encode3({ envoy: rendered }), envelope };
31753
+ return {
31754
+ skip: false,
31755
+ content: encode3({ envoy: rendered }),
31756
+ envelope,
31757
+ ...delivery === undefined ? {} : { delivery },
31758
+ ...rejectedDelivery === undefined ? {} : { rejectedDelivery },
31759
+ ...malformedDelivery ? { malformedDelivery: true } : {}
31760
+ };
31664
31761
  }
31665
31762
 
31666
31763
  // ../envoy-client/src/dispatch-config.ts
@@ -32302,13 +32399,13 @@ function askId(args) {
32302
32399
  }
32303
32400
  return reference.id;
32304
32401
  }
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);
32402
+ function messageInReplyTo(args) {
32403
+ const inReplyTo = optionalString(args, "in_reply_to");
32404
+ if (inReplyTo === undefined || !inReplyTo.startsWith("dispatch://"))
32405
+ return inReplyTo;
32406
+ const reference = parseDispatchRef(inReplyTo);
32310
32407
  if (reference?.kind !== "message") {
32311
- throw new Error("reply_to must be a bare message id or a dispatch://.../message/<id> reference");
32408
+ throw new Error("in_reply_to must be a bare message id or a dispatch://.../message/<id> reference");
32312
32409
  }
32313
32410
  return reference.id;
32314
32411
  }
@@ -32756,10 +32853,10 @@ async function executeDispatchTool(input) {
32756
32853
  };
32757
32854
  }
32758
32855
  case "dispatch_message": {
32759
- const replyTo = messageReplyTo(args);
32856
+ const inReplyTo = messageInReplyTo(args);
32760
32857
  const message = await client.message(issue(), {
32761
32858
  body: stringArg(args, "body"),
32762
- ...replyTo === undefined ? {} : { reply_to: replyTo },
32859
+ ...inReplyTo === undefined ? {} : { in_reply_to: inReplyTo },
32763
32860
  actor
32764
32861
  });
32765
32862
  const messageRef = `dispatch://${message.issue_key}/message/${message.id}`;
@@ -33134,6 +33231,7 @@ var SessionWireSchema = object({
33134
33231
  title: string2(),
33135
33232
  topics: array(string2()),
33136
33233
  roles: array(string2()).optional(),
33234
+ capabilities: array(string2()).optional(),
33137
33235
  self_subscribed: boolean2(),
33138
33236
  last_seen: number2().int().optional()
33139
33237
  });
@@ -33242,6 +33340,7 @@ function createEnvoyClient(config) {
33242
33340
  topics: expandSubscriptionTopics(input.topics),
33243
33341
  port: input.port,
33244
33342
  title: input.title,
33343
+ ...input.capabilities === undefined ? {} : { capabilities: input.capabilities },
33245
33344
  driving: input.driving,
33246
33345
  ...input.selfSubscribed === undefined ? {} : { self_subscribed: input.selfSubscribed }
33247
33346
  }))),
@@ -33468,6 +33567,26 @@ function envoyExtension(pi) {
33468
33567
  });
33469
33568
  return connection;
33470
33569
  };
33570
+ const postDispatchReply = async (delivery, result) => {
33571
+ if (!dispatchConfig.enabled || dispatchConfig.url === null || dispatchConfig.token === null) {
33572
+ throw new Error("Dispatch reply endpoint is not configured");
33573
+ }
33574
+ const response = await fetch(`${dispatchConfig.url}/api/v1/messages/${delivery.messageID}/reply`, {
33575
+ method: "POST",
33576
+ headers: {
33577
+ Authorization: `Bearer ${dispatchConfig.token}`,
33578
+ "Content-Type": "application/json"
33579
+ },
33580
+ body: JSON.stringify({
33581
+ actor: { kind: "session", id: sessionID },
33582
+ attempt: delivery.attempt,
33583
+ ...result
33584
+ })
33585
+ });
33586
+ if (!response.ok) {
33587
+ throw new Error(`Dispatch reply failed: ${response.status} ${await response.text()}`);
33588
+ }
33589
+ };
33471
33590
  const deliver = async (subject, raw, reply) => {
33472
33591
  const rendered = renderInbound(raw, sessionID, subject);
33473
33592
  for (const topic of subscriptionRemovedTopics(raw, sessionID) ?? [])
@@ -33487,9 +33606,34 @@ function envoyExtension(pi) {
33487
33606
  inbox.pop();
33488
33607
  }
33489
33608
  try {
33490
- pi.sendMessage({ customType: "envoy-message", content: rendered.content, display: true }, { deliverAs: "steer", triggerTurn: true });
33609
+ if (rendered.rejectedDelivery !== undefined) {
33610
+ console.warn(`[envoy] rejecting malformed Dispatch targeted delivery ${rendered.rejectedDelivery.messageID}`);
33611
+ await postDispatchReply(rendered.rejectedDelivery, {
33612
+ error: "Invalid Dispatch targeted delivery frame"
33613
+ });
33614
+ } else if (rendered.malformedDelivery === true) {
33615
+ console.warn("[envoy] dropping malformed Dispatch targeted delivery without a reply address");
33616
+ } else if (rendered.delivery?.mode === "btw") {
33617
+ if (pi.askEphemeral === undefined) {
33618
+ await postDispatchReply(rendered.delivery, {
33619
+ error: "This OMP host does not support BTW delivery"
33620
+ });
33621
+ } else {
33622
+ try {
33623
+ const reply = await pi.askEphemeral({ prompt: rendered.delivery.body });
33624
+ await postDispatchReply(rendered.delivery, { body: reply.replyText });
33625
+ } catch (error) {
33626
+ await postDispatchReply(rendered.delivery, { error: messageFor(error) });
33627
+ }
33628
+ }
33629
+ } else {
33630
+ pi.sendMessage({ customType: "envoy-message", content: rendered.content, display: true }, {
33631
+ deliverAs: rendered.delivery?.mode === "aside" ? "aside" : "steer",
33632
+ triggerTurn: true
33633
+ });
33634
+ }
33491
33635
  } catch (error) {
33492
- console.warn(`[envoy] failed to inject envelope ${envelope?.event_id ?? "unknown"}`, error);
33636
+ console.warn(`[envoy] failed to deliver envelope ${envelope?.event_id ?? "unknown"}`, error);
33493
33637
  throw error;
33494
33638
  }
33495
33639
  if (dedupeKey !== undefined) {
@@ -33568,6 +33712,7 @@ function envoyExtension(pi) {
33568
33712
  topics: [...new Set([agentSubject(sessionID), ...subscriptions.keys()])],
33569
33713
  port: 0,
33570
33714
  title: activeSessionContext?.sessionManager.getSessionName?.() ?? "",
33715
+ capabilities: typeof pi.askEphemeral === "function" ? ["aside", "btw"] : ["aside"],
33571
33716
  driving: false,
33572
33717
  selfSubscribed: true
33573
33718
  });
@@ -298,6 +298,24 @@ dispatch_message({ issue, body })
298
298
  It returns `details` `{ issue, topic, message }`. `body` is capped at 2,000 characters. A message is not a decision
299
299
  (`dispatch_ask`) or document feedback (`dispatch_comment`), and it does not wake anyone unless the issue is routed.
300
300
 
301
+ ### Targeted agent messages
302
+
303
+ A human can target the issue message at a live Envoy session or role as **BTW**, **Aside**, or
304
+ **Steer**. The incoming Dispatch frame names the issue and includes a `reply_with` instruction;
305
+ reply on the same open issue with the existing tool, never a new targeted send:
306
+
307
+ ```ts
308
+ dispatch_message({
309
+ issue: "CORE-1",
310
+ in_reply_to: "<targeted-message-id>",
311
+ body: "The requested answer.",
312
+ })
313
+ ```
314
+
315
+ `in_reply_to` correlates the answer under the human's message in its Conversation card. A BTW
316
+ delivery can post its answer automatically; use this call when the frame asks the primary agent to
317
+ reply. Agent bearers cannot set `target` or `delivery`.
318
+
301
319
  ## What comes back
302
320
 
303
321
  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.10.0",
4
4
  "type": "module",
5
5
  "omp": {
6
6
  "extensions": [