@sjawhar/pi-legion-envoy 5.29.0 → 5.30.1

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
@@ -29796,6 +29796,7 @@ var DispatchTargetedMessagePayloadSchema = MessageEventPayloadSchema.extend({
29796
29796
  body: exports_external.string(),
29797
29797
  target: exports_external.string(),
29798
29798
  in_reply_to: exports_external.string().nullable(),
29799
+ broadcast_id: exports_external.string().nullish(),
29799
29800
  deliveries: exports_external.array(exports_external.unknown()),
29800
29801
  created_at: exports_external.string()
29801
29802
  });
@@ -32030,7 +32031,8 @@ function renderInbound(raw, sessionID, subject2) {
32030
32031
  replyPath: dispatchReplyPath("message", message2.data.id),
32031
32032
  replyFields: {},
32032
32033
  issueKey: frame.event.issue_key,
32033
- body: message2.data.body
32034
+ body: message2.data.body,
32035
+ ...typeof message2.data.broadcast_id === "string" ? { broadcastId: message2.data.broadcast_id } : {}
32034
32036
  };
32035
32037
  dispatchReply = {
32036
32038
  tool: "dispatch_message",
@@ -32578,6 +32580,9 @@ class DispatchClient {
32578
32580
  async getMessageThread(id, session) {
32579
32581
  return this.#json("GET", ["api", "v1", "messages", id], undefined, { session });
32580
32582
  }
32583
+ async acceptMessageDelivery(id, attempt, input) {
32584
+ return this.#json("POST", ["api", "v1", "messages", id, "deliveries", String(attempt), "accept"], input);
32585
+ }
32581
32586
  async artifact(issue2, input) {
32582
32587
  const artifactPath = ["api", "v1", "issues", await this.#resolveIssue(issue2), "artifacts"];
32583
32588
  if ("content" in input)
@@ -35297,7 +35302,7 @@ function conversationParts(content) {
35297
35302
  return parts;
35298
35303
  }
35299
35304
  function frameFor(entry) {
35300
- const { message } = entry;
35305
+ const { dispatchMessageId, message } = entry;
35301
35306
  if (message.role === "toolResult" && message.toolCallId !== undefined) {
35302
35307
  return {
35303
35308
  kind: "tool-result",
@@ -35319,7 +35324,8 @@ function frameFor(entry) {
35319
35324
  id: entry.key.slice(2),
35320
35325
  parts: conversationParts(message.host.content),
35321
35326
  role: message.role === "assistant" ? "assistant" : "user",
35322
- streaming: entry.streaming
35327
+ streaming: entry.streaming,
35328
+ ...dispatchMessageId === undefined ? {} : { dispatchMessageId }
35323
35329
  },
35324
35330
  seq: entry.seq,
35325
35331
  v: AGENT_STREAM_PROTOCOL
@@ -35348,7 +35354,7 @@ class AgentStreamPublisher {
35348
35354
  this.#publishedAt.clear();
35349
35355
  this.#watchedUntil = 0;
35350
35356
  }
35351
- record(subject2, raw, streaming) {
35357
+ record(subject2, raw, streaming, dispatchMessageId) {
35352
35358
  const message = readMessage(raw);
35353
35359
  if (message === null)
35354
35360
  return false;
@@ -35361,6 +35367,7 @@ class AgentStreamPublisher {
35361
35367
  this.#seq += 1;
35362
35368
  const entry = {
35363
35369
  at: message.timestamp,
35370
+ dispatchMessageId: dispatchMessageId ?? this.#history.get(key)?.dispatchMessageId,
35364
35371
  key,
35365
35372
  message,
35366
35373
  seq: this.#seq,
@@ -35431,6 +35438,101 @@ function withDispatchFirst(messages, context) {
35431
35438
  ];
35432
35439
  }
35433
35440
 
35441
+ // src/dispatch-user-turn.ts
35442
+ function isUserTurnCandidate(rendered) {
35443
+ const { delivery } = rendered;
35444
+ return rendered.envelope?.source === "dispatch" && delivery?.resource === "message" && delivery.issueKey === null && delivery.broadcastId === undefined && rendered.dispatchActor?.kind === "user" && (delivery.mode === "aside" || delivery.mode === "steer");
35445
+ }
35446
+ var AcceptedDeliverySchema = exports_external.object({
35447
+ message_id: exports_external.string(),
35448
+ body: exports_external.string(),
35449
+ delivery: exports_external.enum(["aside", "steer"])
35450
+ });
35451
+ function turnFromAccept(accepted) {
35452
+ const parsed = AcceptedDeliverySchema.safeParse(accepted);
35453
+ if (!parsed.success)
35454
+ return;
35455
+ return { body: parsed.data.body, messageId: parsed.data.message_id, mode: parsed.data.delivery };
35456
+ }
35457
+ var HANDLED_ATTEMPT_ENTRY = "envoy-dispatch-handled-attempt";
35458
+ function handledAttemptKey(messageId, attempt) {
35459
+ return `${messageId}#${attempt}`;
35460
+ }
35461
+ function handledAttempts(entries) {
35462
+ const handled = new Set;
35463
+ for (const entry of entries) {
35464
+ if (typeof entry !== "object" || entry === null)
35465
+ continue;
35466
+ if (!("type" in entry) || entry.type !== "custom")
35467
+ continue;
35468
+ if (!("customType" in entry) || entry.customType !== HANDLED_ATTEMPT_ENTRY)
35469
+ continue;
35470
+ if (!("data" in entry) || typeof entry.data !== "object" || entry.data === null)
35471
+ continue;
35472
+ const { data } = entry;
35473
+ if (!("message_id" in data) || typeof data.message_id !== "string")
35474
+ continue;
35475
+ if (!("attempt" in data) || typeof data.attempt !== "number")
35476
+ continue;
35477
+ handled.add(handledAttemptKey(data.message_id, data.attempt));
35478
+ }
35479
+ return handled;
35480
+ }
35481
+ function userMessageText(content) {
35482
+ if (typeof content === "string")
35483
+ return content;
35484
+ if (!Array.isArray(content))
35485
+ return "";
35486
+ const pieces = [];
35487
+ for (const part of content) {
35488
+ if (typeof part !== "object" || part === null)
35489
+ continue;
35490
+ if ("type" in part && part.type === "text" && "text" in part && typeof part.text === "string") {
35491
+ pieces.push(part.text);
35492
+ }
35493
+ }
35494
+ return pieces.join(`
35495
+ `);
35496
+ }
35497
+ var INJECTED_USER_TURNS = Symbol.for("legion.pi-envoy.injected-user-turns");
35498
+ function injectedUserTurns() {
35499
+ const store = globalThis;
35500
+ const turns = store[INJECTED_USER_TURNS] ?? new Map;
35501
+ store[INJECTED_USER_TURNS] = turns;
35502
+ return turns;
35503
+ }
35504
+ function noteInjectedUserTurn(sessionID, body, messageId) {
35505
+ const turns = injectedUserTurns();
35506
+ const session = turns.get(sessionID) ?? { found: new Map, sent: [] };
35507
+ session.sent.push({ body, messageId });
35508
+ turns.set(sessionID, session);
35509
+ }
35510
+ function matchInjectedUserTurn(sessionID, message) {
35511
+ if (typeof message !== "object" || message === null)
35512
+ return;
35513
+ if (!("role" in message) || message.role !== "user")
35514
+ return;
35515
+ if (!("timestamp" in message) || typeof message.timestamp !== "number")
35516
+ return;
35517
+ const session = injectedUserTurns().get(sessionID);
35518
+ if (session === undefined)
35519
+ return;
35520
+ const known = session.found.get(message.timestamp);
35521
+ if (known !== undefined)
35522
+ return known;
35523
+ const text = userMessageText("content" in message ? message.content : undefined);
35524
+ const index = session.sent.findIndex((sent2) => sent2.body === text);
35525
+ const sent = session.sent[index];
35526
+ if (sent === undefined)
35527
+ return;
35528
+ session.sent.splice(index, 1);
35529
+ session.found.set(message.timestamp, sent.messageId);
35530
+ return sent.messageId;
35531
+ }
35532
+ function endInjectedUserTurns(sessionID) {
35533
+ injectedUserTurns().delete(sessionID);
35534
+ }
35535
+
35434
35536
  // src/envoy-session.ts
35435
35537
  import path2 from "path";
35436
35538
  var ENVOY_SESSIONS = Symbol.for("legion.pi-envoy.envoy-session");
@@ -35687,6 +35789,7 @@ var NATS_RETRY_INTERVAL_MS = 15000;
35687
35789
  var CAPABILITIES_WITHOUT_BTW = DELIVERY_CAPABILITIES.filter((capability) => capability !== "btw");
35688
35790
  var ROLE_CLAIM_ENTRY = "envoy-role-claim";
35689
35791
  var OPEN_ASKS_TIMEOUT_MS = 3000;
35792
+ var USER_TURN_ACCEPT_TIMEOUT_MS = 1e4;
35690
35793
  var LEGION_MANAGED_ENTRY = "legion-managed-session";
35691
35794
  var ASK_REMINDER_MESSAGE = "dispatch-ask-reminder";
35692
35795
  var ASK_PROMPT_LINE_SEPARATOR = /\r\n|[\r\n\u0085\u2028\u2029]/;
@@ -35816,6 +35919,7 @@ function envoyExtension(pi) {
35816
35919
  let runSeq = 0;
35817
35920
  let legionManagedTranscript = false;
35818
35921
  const legionManaged = (id) => legionManagedTranscript || legionRoleClaimBridge().managedSessions.has(id);
35922
+ let handledDispatchAttempts = new Set;
35819
35923
  const availabilityWarningSessionIDs = new Set;
35820
35924
  const warnAskAvailability = (context, error48) => {
35821
35925
  const sessionID2 = context.sessionManager.getSessionId();
@@ -35873,6 +35977,7 @@ function envoyExtension(pi) {
35873
35977
  awarenessGeneration++;
35874
35978
  }
35875
35979
  legionManagedTranscript = branch.some(isLegionManagedEntry);
35980
+ handledDispatchAttempts = handledAttempts(context.sessionManager.getEntries());
35876
35981
  };
35877
35982
  pi.on("resources_discover", async () => ({ skillPaths: [SKILLS_DIRECTORY] }));
35878
35983
  registerEnvoyMessageRenderer(pi);
@@ -35892,6 +35997,34 @@ function envoyExtension(pi) {
35892
35997
  const postDispatchReply = async (delivery, result) => {
35893
35998
  await postDeliveryReply(currentDispatchConfig(), sessionID, delivery, result);
35894
35999
  };
36000
+ const acceptedUserTurn = async (rendered) => {
36001
+ if (!isUserTurnCandidate(rendered))
36002
+ return;
36003
+ const { delivery } = rendered;
36004
+ const key = handledAttemptKey(delivery.id, delivery.attempt);
36005
+ if (handledDispatchAttempts.has(key)) {
36006
+ logger.warn("envoy: a frame names a Dispatch attempt this session already delivered; it arrives as a card", { attempt: delivery.attempt, messageID: delivery.id });
36007
+ return;
36008
+ }
36009
+ handledDispatchAttempts.add(key);
36010
+ pi.appendEntry(HANDLED_ATTEMPT_ENTRY, { attempt: delivery.attempt, message_id: delivery.id });
36011
+ try {
36012
+ const config2 = activeDispatchConfig(process.env, { cwd: process.cwd() });
36013
+ if (config2 === null)
36014
+ return;
36015
+ const accepted = await new DispatchClient(config2.url, config2.token, fetch, AbortSignal.timeout(USER_TURN_ACCEPT_TIMEOUT_MS)).acceptMessageDelivery(delivery.id, delivery.attempt, {
36016
+ actor: { id: sessionID, kind: "session" }
36017
+ });
36018
+ const turn = turnFromAccept(accepted);
36019
+ if (turn === undefined) {
36020
+ logger.warn("envoy: Dispatch accepted a direct message without the stored body and mode to inject; it arrives as a card", { attempt: delivery.attempt, messageID: delivery.id });
36021
+ }
36022
+ return turn;
36023
+ } catch (error48) {
36024
+ logger.warn("envoy: Dispatch did not accept a direct message as this session's turn; it arrives as a card", { attempt: delivery.attempt, messageID: delivery.id, error: messageFor(error48) });
36025
+ return;
36026
+ }
36027
+ };
35895
36028
  const deliver = async (subject2, raw, reply) => {
35896
36029
  const rendered = renderInbound(raw, sessionID, subject2);
35897
36030
  const directSubject = agentSubject(sessionID);
@@ -35950,10 +36083,16 @@ function envoyExtension(pi) {
35950
36083
  }
35951
36084
  }
35952
36085
  } else {
35953
- pi.sendMessage({ customType: "envoy-message", content: rendered.content, display: true }, {
35954
- deliverAs: rendered.delivery?.mode === "aside" ? "aside" : "steer",
35955
- triggerTurn: true
35956
- });
36086
+ const turn = await acceptedUserTurn(rendered);
36087
+ if (turn === undefined) {
36088
+ pi.sendMessage({ customType: "envoy-message", content: rendered.content, display: true }, {
36089
+ deliverAs: rendered.delivery?.mode === "aside" ? "aside" : "steer",
36090
+ triggerTurn: true
36091
+ });
36092
+ } else {
36093
+ noteInjectedUserTurn(sessionID, turn.body, turn.messageId);
36094
+ pi.sendUserMessage(turn.body, turn.mode === "aside" ? { deliverAs: "aside" } : undefined);
36095
+ }
35957
36096
  }
35958
36097
  } catch (error48) {
35959
36098
  console.warn(`[envoy] failed to deliver envelope ${envelope2?.event_id ?? "unknown"}`, error48);
@@ -36079,7 +36218,7 @@ function envoyExtension(pi) {
36079
36218
  const recordAgentStreamMessage = (message, streaming) => {
36080
36219
  if (sessionID === "")
36081
36220
  return;
36082
- agentStream.record(agentStreamFramesSubject(sessionID), message, streaming);
36221
+ agentStream.record(agentStreamFramesSubject(sessionID), message, streaming, matchInjectedUserTurn(sessionID, message));
36083
36222
  };
36084
36223
  pi.on("message_start", async (event) => {
36085
36224
  recordAgentStreamMessage(event.message, true);
@@ -36556,6 +36695,7 @@ function envoyExtension(pi) {
36556
36695
  }
36557
36696
  pi.on("agent_end", async (event, context) => {
36558
36697
  const id = context.sessionManager.getSessionId();
36698
+ endInjectedUserTurns(sessionID);
36559
36699
  const lastReply = event.messages?.findLast((message) => message.role === "assistant");
36560
36700
  const ask = sideTurn(pi, context);
36561
36701
  if (event.willContinue === true || lastReply?.stopReason !== "stop" || !context.hasUI || id === "" || ask === undefined || !checkOwed(id)) {
package/dist/legion.js CHANGED
@@ -29965,6 +29965,7 @@ var DispatchTargetedMessagePayloadSchema = MessageEventPayloadSchema.extend({
29965
29965
  body: exports_external.string(),
29966
29966
  target: exports_external.string(),
29967
29967
  in_reply_to: exports_external.string().nullable(),
29968
+ broadcast_id: exports_external.string().nullish(),
29968
29969
  deliveries: exports_external.array(exports_external.unknown()),
29969
29970
  created_at: exports_external.string()
29970
29971
  });
@@ -31484,6 +31485,9 @@ class DispatchClient {
31484
31485
  async getMessageThread(id, session) {
31485
31486
  return this.#json("GET", ["api", "v1", "messages", id], undefined, { session });
31486
31487
  }
31488
+ async acceptMessageDelivery(id, attempt, input) {
31489
+ return this.#json("POST", ["api", "v1", "messages", id, "deliveries", String(attempt), "accept"], input);
31490
+ }
31487
31491
  async artifact(issue2, input) {
31488
31492
  const artifactPath = ["api", "v1", "issues", await this.#resolveIssue(issue2), "artifacts"];
31489
31493
  if ("content" in input)
@@ -33672,7 +33676,7 @@ import { logger } from "@oh-my-pi/pi-utils";
33672
33676
  // package.json
33673
33677
  var package_default = {
33674
33678
  name: "@sjawhar/pi-legion-envoy",
33675
- version: "5.29.0",
33679
+ version: "5.30.1",
33676
33680
  type: "module",
33677
33681
  omp: {
33678
33682
  extensions: [
@@ -33717,6 +33721,101 @@ var package_default = {
33717
33721
  }
33718
33722
  };
33719
33723
 
33724
+ // src/dispatch-user-turn.ts
33725
+ function isUserTurnCandidate(rendered) {
33726
+ const { delivery } = rendered;
33727
+ return rendered.envelope?.source === "dispatch" && delivery?.resource === "message" && delivery.issueKey === null && delivery.broadcastId === undefined && rendered.dispatchActor?.kind === "user" && (delivery.mode === "aside" || delivery.mode === "steer");
33728
+ }
33729
+ var AcceptedDeliverySchema = exports_external.object({
33730
+ message_id: exports_external.string(),
33731
+ body: exports_external.string(),
33732
+ delivery: exports_external.enum(["aside", "steer"])
33733
+ });
33734
+ function turnFromAccept(accepted) {
33735
+ const parsed = AcceptedDeliverySchema.safeParse(accepted);
33736
+ if (!parsed.success)
33737
+ return;
33738
+ return { body: parsed.data.body, messageId: parsed.data.message_id, mode: parsed.data.delivery };
33739
+ }
33740
+ var HANDLED_ATTEMPT_ENTRY = "envoy-dispatch-handled-attempt";
33741
+ function handledAttemptKey(messageId, attempt) {
33742
+ return `${messageId}#${attempt}`;
33743
+ }
33744
+ function handledAttempts(entries) {
33745
+ const handled = new Set;
33746
+ for (const entry of entries) {
33747
+ if (typeof entry !== "object" || entry === null)
33748
+ continue;
33749
+ if (!("type" in entry) || entry.type !== "custom")
33750
+ continue;
33751
+ if (!("customType" in entry) || entry.customType !== HANDLED_ATTEMPT_ENTRY)
33752
+ continue;
33753
+ if (!("data" in entry) || typeof entry.data !== "object" || entry.data === null)
33754
+ continue;
33755
+ const { data } = entry;
33756
+ if (!("message_id" in data) || typeof data.message_id !== "string")
33757
+ continue;
33758
+ if (!("attempt" in data) || typeof data.attempt !== "number")
33759
+ continue;
33760
+ handled.add(handledAttemptKey(data.message_id, data.attempt));
33761
+ }
33762
+ return handled;
33763
+ }
33764
+ function userMessageText(content) {
33765
+ if (typeof content === "string")
33766
+ return content;
33767
+ if (!Array.isArray(content))
33768
+ return "";
33769
+ const pieces = [];
33770
+ for (const part of content) {
33771
+ if (typeof part !== "object" || part === null)
33772
+ continue;
33773
+ if ("type" in part && part.type === "text" && "text" in part && typeof part.text === "string") {
33774
+ pieces.push(part.text);
33775
+ }
33776
+ }
33777
+ return pieces.join(`
33778
+ `);
33779
+ }
33780
+ var INJECTED_USER_TURNS = Symbol.for("legion.pi-envoy.injected-user-turns");
33781
+ function injectedUserTurns() {
33782
+ const store = globalThis;
33783
+ const turns = store[INJECTED_USER_TURNS] ?? new Map;
33784
+ store[INJECTED_USER_TURNS] = turns;
33785
+ return turns;
33786
+ }
33787
+ function noteInjectedUserTurn(sessionID, body, messageId) {
33788
+ const turns = injectedUserTurns();
33789
+ const session = turns.get(sessionID) ?? { found: new Map, sent: [] };
33790
+ session.sent.push({ body, messageId });
33791
+ turns.set(sessionID, session);
33792
+ }
33793
+ function matchInjectedUserTurn(sessionID, message) {
33794
+ if (typeof message !== "object" || message === null)
33795
+ return;
33796
+ if (!("role" in message) || message.role !== "user")
33797
+ return;
33798
+ if (!("timestamp" in message) || typeof message.timestamp !== "number")
33799
+ return;
33800
+ const session = injectedUserTurns().get(sessionID);
33801
+ if (session === undefined)
33802
+ return;
33803
+ const known = session.found.get(message.timestamp);
33804
+ if (known !== undefined)
33805
+ return known;
33806
+ const text = userMessageText("content" in message ? message.content : undefined);
33807
+ const index = session.sent.findIndex((sent2) => sent2.body === text);
33808
+ const sent = session.sent[index];
33809
+ if (sent === undefined)
33810
+ return;
33811
+ session.sent.splice(index, 1);
33812
+ session.found.set(message.timestamp, sent.messageId);
33813
+ return sent.messageId;
33814
+ }
33815
+ function endInjectedUserTurns(sessionID) {
33816
+ injectedUserTurns().delete(sessionID);
33817
+ }
33818
+
33720
33819
  // src/legion/classify.ts
33721
33820
  function classifySession(env) {
33722
33821
  if (env.LEGION_CONTROLLER !== undefined && env.LEGION_TREE !== undefined) {
@@ -35838,7 +35937,8 @@ function legionExtension(pi) {
35838
35937
  pi.on("message_start", async (event, context) => {
35839
35938
  if (!phaseWorkerSession(context))
35840
35939
  return;
35841
- const kind = inboundKind(event.message);
35940
+ const injected = matchInjectedUserTurn(context.sessionManager.getSessionId(), event.message) !== undefined;
35941
+ const kind = injected ? "inbound-event" : inboundKind(event.message);
35842
35942
  if (kind !== undefined)
35843
35943
  advancePhaseStall({ kind });
35844
35944
  });
@@ -6,7 +6,8 @@ human messages you directly from the Agents page.
6
6
  ## Targeted agent messages
7
7
 
8
8
  A human — or any bearer caller over HTTP, such as a test rig — can target the issue message at a
9
- live Envoy session or role as **BTW**, **Aside**, or **Steer**. The incoming Dispatch frame names
9
+ live Envoy session or role as **BTW**, **Aside**, or **Steer** (the dashboard calls a steer
10
+ **Send**). The incoming Dispatch frame names
10
11
  the issue and includes a `reply_with` hint (`{ tool, args }`, ready to issue on any host); reply on the same open issue with the existing
11
12
  tool, never a new targeted send:
12
13
 
@@ -32,9 +33,17 @@ target only a session that advertises the mode you want. Sending to a session wi
32
33
 
33
34
  ## Answering a direct message
34
35
 
35
- A human can also message you directly from the **Agents** page, with no issue at all. That frame
36
- names no issue and its `reply_with` hint carries none either; answer it with the message's bare
37
- id in `in_reply_to`, alone:
36
+ A human can also message you directly from the **Agents** page, with no issue at all. On Oh My
37
+ Pi, a person's **Send** or **Aside** arrives as their own user message, exactly as if they had
38
+ typed it at your terminal: your Envoy plugin takes it only once Dispatch accepts it as a person's
39
+ own fresh message to you, and injects the text Dispatch stored. Answer it in the conversation as
40
+ you would anything typed, with no `dispatch_message`; the Agents page shows your conversation
41
+ live, so they read your answer there.
42
+
43
+ Everything else still arrives as a Dispatch frame: a **BTW**, a broadcast, a direct message on a
44
+ host that takes no user turn from its plugin (Claude Code), and one Dispatch did not accept.
45
+ That frame names no issue and its `reply_with` hint carries none either; answer it with the
46
+ message's bare id in `in_reply_to`, alone:
38
47
 
39
48
  ```ts
40
49
  dispatch_message({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/pi-legion-envoy",
3
- "version": "5.29.0",
3
+ "version": "5.30.1",
4
4
  "type": "module",
5
5
  "omp": {
6
6
  "extensions": [