@sjawhar/pi-envoy 8.17.0 → 9.0.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/dispatch.js CHANGED
@@ -13801,7 +13801,10 @@ var AGENT_STREAM_LIMITS = {
13801
13801
  modelChars: 200,
13802
13802
  historyMessages: 200,
13803
13803
  historyBytes: 512 * 1024,
13804
- snapshotIntervalMs: 100
13804
+ snapshotIntervalMs: 100,
13805
+ commands: 500,
13806
+ commandNameChars: 100,
13807
+ commandDescriptionChars: 200
13805
13808
  };
13806
13809
  // ../contracts/src/claim-holds.ts
13807
13810
  function claimHolds(claim, titles) {
package/dist/envoy.js CHANGED
@@ -29863,16 +29863,23 @@ function agentStreamControlSubject(sessionID) {
29863
29863
  return `${AGENT_STREAM_SUBJECT_PREFIX}${sessionID}.control`;
29864
29864
  }
29865
29865
  var AGENT_STREAM_WATCH_TTL_MS = 30000;
29866
+ var AGENT_STREAM_WATCH_INTERVAL_MS = 1e4;
29866
29867
  var AGENT_STREAM_LIMITS = {
29867
29868
  partChars: 16000,
29868
29869
  toolChars: 8000,
29869
29870
  modelChars: 200,
29870
29871
  historyMessages: 200,
29871
29872
  historyBytes: 512 * 1024,
29872
- snapshotIntervalMs: 100
29873
+ snapshotIntervalMs: 100,
29874
+ commands: 500,
29875
+ commandNameChars: 100,
29876
+ commandDescriptionChars: 200
29873
29877
  };
29874
29878
  var AGENT_STREAM_TRUNCATION_SUFFIX = `
29875
29879
  \u2026 truncated`;
29880
+ function isTypableCommandName(name) {
29881
+ return /^[^\s\p{Cc}]+$/u.test(name);
29882
+ }
29876
29883
  function capAgentStreamText(text, limit) {
29877
29884
  if (text.length <= limit)
29878
29885
  return text;
@@ -33505,7 +33512,7 @@ function sourceSession(sessionID) {
33505
33512
 
33506
33513
  // ../pi-shared/src/interface.ts
33507
33514
  import { logger } from "@oh-my-pi/pi-utils";
33508
- var ENVOY_PLUGIN_INTERFACE_VERSION = 1;
33515
+ var ENVOY_PLUGIN_INTERFACE_VERSION = 2;
33509
33516
  var ENVOY_PLUGIN_INTERFACE_KEY = Symbol.for("legion.pi-shared.envoy-plugin-interface");
33510
33517
  var LEGION_PLUGIN_LOADED_KEY = Symbol.for("legion.pi-legion.loaded");
33511
33518
  var LEGACY_LEGION_LOADED_KEY = Symbol.for("legion.pi-envoy.legion-loaded");
@@ -33562,35 +33569,68 @@ function userMessageText(content) {
33562
33569
  return pieces.join(`
33563
33570
  `);
33564
33571
  }
33565
- function noteInjectedUserTurn(sessionID, body, messageId) {
33572
+ function sessionTurns(sessionID) {
33566
33573
  const turns = envoyPluginInterface().injectedUserTurns;
33567
33574
  const session = turns.get(sessionID) ?? { found: new Map, sent: [] };
33568
- session.sent.push({ body, messageId });
33569
33575
  turns.set(sessionID, session);
33576
+ return session;
33577
+ }
33578
+ function noteInjectedUserTurn(sessionID, body, messageId) {
33579
+ sessionTurns(sessionID).sent.push({ body, messageId, via: "text" });
33580
+ }
33581
+ function noteTypedUserTurn(sessionID, messageId) {
33582
+ sessionTurns(sessionID).sent.push({ messageId, via: "typed" });
33583
+ }
33584
+ function dropInjectedUserTurn(sessionID, messageId) {
33585
+ const session = envoyPluginInterface().injectedUserTurns.get(sessionID);
33586
+ if (session === undefined)
33587
+ return;
33588
+ const index = session.sent.findIndex((sent) => sent.messageId === messageId);
33589
+ if (index !== -1)
33590
+ session.sent.splice(index, 1);
33570
33591
  }
33571
33592
  function matchInjectedUserTurn(sessionID, message) {
33572
33593
  if (typeof message !== "object" || message === null)
33573
33594
  return;
33574
- if (!("role" in message) || message.role !== "user")
33595
+ if (!("role" in message) || message.role !== "user" && message.role !== "custom") {
33575
33596
  return;
33597
+ }
33576
33598
  if (!("timestamp" in message) || typeof message.timestamp !== "number")
33577
33599
  return;
33578
33600
  const session = envoyPluginInterface().injectedUserTurns.get(sessionID);
33579
33601
  if (session === undefined)
33580
33602
  return;
33581
- const known = session.found.get(message.timestamp);
33603
+ const tag = messageTag(message);
33604
+ const memo = tag === undefined ? `${message.timestamp}` : `${message.timestamp}:${tag}`;
33605
+ const known = session.found.get(memo);
33582
33606
  if (known !== undefined)
33583
33607
  return known;
33584
- const text = userMessageText("content" in message ? message.content : undefined);
33585
- const index = session.sent.findIndex((sent2) => sent2.body === text);
33608
+ let index = tag === undefined ? -1 : session.sent.findIndex((sent2) => sent2.messageId === tag);
33609
+ if (index === -1 && message.role === "user") {
33610
+ const text = userMessageText("content" in message ? message.content : undefined);
33611
+ index = session.sent.findIndex((sent2) => sent2.via === "text" && sent2.body === text);
33612
+ }
33586
33613
  const sent = session.sent[index];
33587
33614
  if (sent === undefined)
33588
33615
  return;
33589
33616
  session.sent.splice(index, 1);
33590
- session.found.set(message.timestamp, sent.messageId);
33617
+ session.found.set(memo, sent.messageId);
33591
33618
  return sent.messageId;
33592
33619
  }
33620
+ function messageTag(message) {
33621
+ if (typeof message !== "object" || message === null || !("tag" in message))
33622
+ return;
33623
+ return typeof message.tag === "string" ? message.tag : undefined;
33624
+ }
33593
33625
  function endInjectedUserTurns(sessionID) {
33626
+ const turns = envoyPluginInterface().injectedUserTurns;
33627
+ const typed = turns.get(sessionID)?.sent.filter((sent) => sent.via === "typed") ?? [];
33628
+ if (typed.length === 0)
33629
+ turns.delete(sessionID);
33630
+ else
33631
+ turns.set(sessionID, { found: new Map, sent: typed });
33632
+ }
33633
+ function forgetInjectedUserTurns(sessionID) {
33594
33634
  envoyPluginInterface().injectedUserTurns.delete(sessionID);
33595
33635
  }
33596
33636
 
@@ -33724,11 +33764,16 @@ function toolFailure(error48) {
33724
33764
  }
33725
33765
 
33726
33766
  // extensions/envoy.ts
33727
- import { logger as logger3, procmgr } from "@oh-my-pi/pi-utils";
33767
+ import * as host2 from "@oh-my-pi/pi-coding-agent";
33768
+ import { logger as logger3, procmgr, VERSION } from "@oh-my-pi/pi-utils";
33728
33769
  var import_nats2 = __toESM(require_mod4(), 1);
33729
33770
 
33730
33771
  // src/agent-stream.ts
33731
33772
  var encoder = new TextEncoder;
33773
+ var REPLAY_ARRAY_BYTES = 2;
33774
+ function jsonBytes(value) {
33775
+ return encoder.encode(JSON.stringify(value)).length;
33776
+ }
33732
33777
  function readModel(message) {
33733
33778
  if (typeof message.model !== "string" || message.model === "")
33734
33779
  return;
@@ -33749,16 +33794,19 @@ function readMessage(raw) {
33749
33794
  role: message.role,
33750
33795
  timestamp: message.timestamp,
33751
33796
  toolCallId: typeof message.toolCallId === "string" ? message.toolCallId : undefined,
33752
- toolName: typeof message.toolName === "string" ? message.toolName : undefined
33797
+ toolName: typeof message.toolName === "string" ? message.toolName : undefined,
33798
+ tag: typeof message.tag === "string" ? message.tag : undefined
33753
33799
  };
33754
33800
  }
33755
33801
  function keyFor(message) {
33756
33802
  if (message.role === "toolResult") {
33757
33803
  return message.toolCallId === undefined ? null : `t:${message.toolCallId}`;
33758
33804
  }
33759
- if (message.role !== "assistant" && message.role !== "user")
33805
+ if (message.role === "assistant")
33806
+ return `m:a${message.timestamp}`;
33807
+ if (message.role !== "user")
33760
33808
  return null;
33761
- return `m:${message.role === "assistant" ? "a" : "u"}${message.timestamp}`;
33809
+ return message.tag === undefined ? `m:u${message.timestamp}` : `m:u${message.timestamp}:${message.tag}`;
33762
33810
  }
33763
33811
  function contentText(content) {
33764
33812
  if (typeof content === "string")
@@ -33858,6 +33906,8 @@ class AgentStreamPublisher {
33858
33906
  #publishedAt = new Map;
33859
33907
  #seq = 0;
33860
33908
  #watchedUntil = 0;
33909
+ #commands;
33910
+ #commandsSeq = 0;
33861
33911
  constructor(deps) {
33862
33912
  this.#deps = deps;
33863
33913
  }
@@ -33872,6 +33922,46 @@ class AgentStreamPublisher {
33872
33922
  this.#settled.clear();
33873
33923
  this.#publishedAt.clear();
33874
33924
  this.#watchedUntil = 0;
33925
+ this.#commands = undefined;
33926
+ }
33927
+ setCommands(subject2, commands) {
33928
+ const limit = AGENT_STREAM_LIMITS.historyBytes - REPLAY_ARRAY_BYTES;
33929
+ let bytes = jsonBytes(this.#commandsFrame([], this.#seq + 1));
33930
+ const seen = new Set;
33931
+ const kept = [];
33932
+ for (const command of commands) {
33933
+ if (kept.length >= AGENT_STREAM_LIMITS.commands)
33934
+ break;
33935
+ const name = command.name;
33936
+ if (name.length > AGENT_STREAM_LIMITS.commandNameChars)
33937
+ continue;
33938
+ if (!isTypableCommandName(name) || seen.has(name))
33939
+ continue;
33940
+ const listed = {
33941
+ name,
33942
+ source: command.source,
33943
+ ...command.description !== undefined && {
33944
+ description: capAgentStreamText(command.description, AGENT_STREAM_LIMITS.commandDescriptionChars)
33945
+ },
33946
+ ...command.terminalOnly === true && { terminalOnly: true }
33947
+ };
33948
+ const cost = jsonBytes(listed) + (kept.length === 0 ? 0 : 1);
33949
+ if (bytes + cost > limit)
33950
+ break;
33951
+ bytes += cost;
33952
+ seen.add(name);
33953
+ kept.push(listed);
33954
+ }
33955
+ if (JSON.stringify(kept) === JSON.stringify(this.#commands))
33956
+ return;
33957
+ this.#commands = kept;
33958
+ this.#seq += 1;
33959
+ this.#commandsSeq = this.#seq;
33960
+ if (this.watched)
33961
+ this.#deps.publish(subject2, JSON.stringify(this.#commandsFrame(kept)));
33962
+ }
33963
+ #commandsFrame(commands, seq = this.#commandsSeq) {
33964
+ return { commands, kind: "commands", seq, v: AGENT_STREAM_PROTOCOL };
33875
33965
  }
33876
33966
  record(subject2, raw, streaming, dispatchMessageId) {
33877
33967
  const message = readMessage(raw);
@@ -33918,18 +34008,23 @@ class AgentStreamPublisher {
33918
34008
  if (!this.watched)
33919
34009
  return { frames: [], session_id: sessionID, v: AGENT_STREAM_PROTOCOL };
33920
34010
  const ordered = [...this.#history.values()].sort((left, right) => left.at === right.at ? left.seq - right.seq : left.at - right.at);
33921
- let budget = AGENT_STREAM_LIMITS.historyBytes;
34011
+ const commands = this.#commands === undefined ? undefined : this.#commandsFrame(this.#commands);
34012
+ let used = REPLAY_ARRAY_BYTES + (commands === undefined ? 0 : jsonBytes(commands));
33922
34013
  const kept = [];
33923
34014
  for (let index = ordered.length - 1;index >= 0; index -= 1) {
33924
34015
  const entry = ordered[index];
33925
34016
  if (entry === undefined)
33926
34017
  continue;
33927
34018
  const frame = frameFor(entry);
33928
- budget -= encoder.encode(JSON.stringify(frame)).length;
33929
- if (budget < 0)
34019
+ const separated = kept.length > 0 || commands !== undefined;
34020
+ const cost = jsonBytes(frame) + (separated ? 1 : 0);
34021
+ if (used + cost > AGENT_STREAM_LIMITS.historyBytes)
33930
34022
  break;
34023
+ used += cost;
33931
34024
  kept.unshift(frame);
33932
34025
  }
34026
+ if (commands !== undefined)
34027
+ kept.push(commands);
33933
34028
  return { frames: kept, session_id: sessionID, v: AGENT_STREAM_PROTOCOL };
33934
34029
  }
33935
34030
  }
@@ -34074,6 +34169,26 @@ function handledAttempts(entries) {
34074
34169
  }
34075
34170
  return handled;
34076
34171
  }
34172
+ function typedInputReply(text, outcome) {
34173
+ const typed = /^\/\S+/.exec(text)?.[0];
34174
+ const command = typed ?? "This message";
34175
+ let reply;
34176
+ if ("error" in outcome)
34177
+ reply = `${command} was not run: ${outcome.error}`;
34178
+ else if ("hostVersion" in outcome) {
34179
+ if (typed !== undefined) {
34180
+ reply = `${typed} was not run: this session's Oh My Pi ${outcome.hostVersion} cannot run commands sent from Dispatch; nothing was sent.`;
34181
+ }
34182
+ } else if (outcome.handled === "terminal-only") {
34183
+ reply = `${command} runs only in the session's own terminal; nothing was sent.`;
34184
+ } else if (outcome.handled === "unavailable") {
34185
+ reply = `${command} was not run: this session's host cannot run typed input from Dispatch; nothing was sent.`;
34186
+ } else if (outcome.output?.trim())
34187
+ reply = outcome.output;
34188
+ if (reply === undefined || reply.length <= DISPATCH_BODY_MAX)
34189
+ return reply;
34190
+ return `${reply.slice(0, DISPATCH_BODY_MAX - 1)}\u2026`;
34191
+ }
34077
34192
 
34078
34193
  // src/envoy-session.ts
34079
34194
  import path3 from "path";
@@ -34161,6 +34276,53 @@ function sideTurn(context) {
34161
34276
  return ({ prompt, signal }) => runEphemeralTurn({ promptText: btwPrompt(prompt), signal });
34162
34277
  }
34163
34278
 
34279
+ // src/typed-pictures.ts
34280
+ class TypedPictures {
34281
+ #admissions = new Map;
34282
+ admit(messageId, turn) {
34283
+ this.#admissions.set(messageId, {
34284
+ addresses: turn.addresses,
34285
+ body: turn.body,
34286
+ images: turn.images,
34287
+ sessionID: turn.sessionID
34288
+ });
34289
+ }
34290
+ handOver(text) {
34291
+ for (const admission of this.#admissions.values()) {
34292
+ if (admission.images === undefined || admission.body !== text)
34293
+ continue;
34294
+ const { images } = admission;
34295
+ admission.images = undefined;
34296
+ return images;
34297
+ }
34298
+ return;
34299
+ }
34300
+ withdraw(messageId) {
34301
+ this.#admissions.delete(messageId);
34302
+ }
34303
+ inputPassed(messageId) {
34304
+ if (this.#admissions.get(messageId)?.images !== undefined)
34305
+ this.#admissions.delete(messageId);
34306
+ }
34307
+ started(message) {
34308
+ const tag = messageTag(message);
34309
+ if (tag === undefined)
34310
+ return;
34311
+ const admission = this.#admissions.get(tag);
34312
+ if (admission === undefined)
34313
+ return;
34314
+ this.#admissions.delete(tag);
34315
+ return admission.images === undefined ? admission.addresses : undefined;
34316
+ }
34317
+ dropOtherSessions(sessionID) {
34318
+ for (const [messageId, admission] of this.#admissions) {
34319
+ if (admission.sessionID !== sessionID && admission.images === undefined) {
34320
+ this.#admissions.delete(messageId);
34321
+ }
34322
+ }
34323
+ }
34324
+ }
34325
+
34164
34326
  // extensions/envoy-message-renderer.ts
34165
34327
  import { Box, Spacer, Text } from "@oh-my-pi/pi-tui";
34166
34328
  var ENVOY_MESSAGE_COLLAPSED_LINES = 12;
@@ -34216,6 +34378,12 @@ async function copySessionID(copyToClipboard2, sessionID) {
34216
34378
  }
34217
34379
 
34218
34380
  // extensions/envoy.ts
34381
+ var hostBuiltinCommands;
34382
+ import("@oh-my-pi/pi-coding-agent/extensibility/extensions/send-user-input-handler").then((module) => {
34383
+ hostBuiltinCommands = module.listUserInputBuiltinCommands;
34384
+ }, () => {
34385
+ return;
34386
+ });
34219
34387
  var codec2 = import_nats2.StringCodec();
34220
34388
  var NATS_RETRY_INTERVAL_MS = 15000;
34221
34389
  var DELIVERY_PICTURES_TIMEOUT_MS = 20000;
@@ -34264,16 +34432,16 @@ var ASK_SELF_CHECK_PROMPT = (asks) => {
34264
34432
  ...asks.length > askLimit ? [`+${asks.length - askLimit} more`] : []
34265
34433
  ].join(`
34266
34434
  `);
34267
- return "Your run has just ended. " + `${promptAsks}
34435
+ return `Your run has just ended. ${promptAsks}
34268
34436
 
34269
- ` + "Are you right now waiting on a human for anything none of those asks covers? " + "Answer with exactly one word and nothing else: WAITING or PROCEEDING. " + "WAITING \u2014 you stopped because you need a decision, an approval, or information from a human. " + "PROCEEDING \u2014 you finished, you will carry on by yourself, or you are waiting only on tools, " + "subagents, or events.";
34437
+ Are you right now waiting on a human for anything none of those asks covers? Answer with exactly one word and nothing else: WAITING or PROCEEDING. ` + "WAITING \u2014 you stopped because you need a decision, an approval, or information from a human. " + "PROCEEDING \u2014 you finished, you will carry on by yourself, or you are waiting only on tools, " + "subagents, or events.";
34270
34438
  };
34271
34439
  var WAITING_VERDICT = /^\W*WAITING\b/;
34272
34440
  var ECHOED_CHOICE = /\bPROCEEDING\b/;
34273
34441
  var isWaitingVerdict = (reply) => WAITING_VERDICT.test(reply) && !ECHOED_CHOICE.test(reply);
34274
34442
  var ASK_SELF_CHECK_TIMEOUT_MS = 60000;
34275
34443
  var ASK_CHECKS_PER_PERIOD = 5;
34276
- var UNASKED_WAIT_REMINDER = "You just said you are waiting on a human for something no open ask in Dispatch covers. " + "Open it now: a decision block in the document it concerns (dispatch doc-edit with an ask block), or " + "dispatch ask for a to-do only a human can do, naming exactly what you need and from whom.";
34444
+ var UNASKED_WAIT_REMINDER = "You just said you are waiting on a human for something no open ask in Dispatch covers. Open it now: a decision block in the document it concerns (dispatch doc-edit with an ask block), or dispatch ask for a to-do only a human can do, naming exactly what you need and from whom.";
34277
34445
  function isLegionManagedEntry(entry) {
34278
34446
  if (typeof entry !== "object" || entry === null)
34279
34447
  return false;
@@ -34367,6 +34535,7 @@ function envoyExtension(pi) {
34367
34535
  let legionManagedTranscript = false;
34368
34536
  const legionManaged = (id) => legionManagedTranscript || legionRoleClaimBridge().managedSessions.has(id);
34369
34537
  let handledDispatchAttempts = new Set;
34538
+ const notedSessionIDs = new Set;
34370
34539
  const availabilityWarningSessionIDs = new Set;
34371
34540
  const warnAskAvailability = (context, error48) => {
34372
34541
  const sessionID2 = context.sessionManager.getSessionId();
@@ -34431,8 +34600,12 @@ function envoyExtension(pi) {
34431
34600
  legionManagedTranscript = branch.some(isLegionManagedEntry);
34432
34601
  handledDispatchAttempts = handledAttempts(context.sessionManager.getEntries());
34433
34602
  markPicturesShown(shownPictures(sessionID), transcriptPictures(branch));
34434
- if (previousSessionID !== sessionID)
34603
+ if (previousSessionID !== sessionID) {
34435
34604
  forgetShownPictures(previousSessionID);
34605
+ if (notedSessionIDs.delete(previousSessionID))
34606
+ forgetInjectedUserTurns(previousSessionID);
34607
+ typedPictures.dropOtherSessions(sessionID);
34608
+ }
34436
34609
  };
34437
34610
  let dispatchLedger;
34438
34611
  const bindDispatchSession = (id) => {
@@ -34475,10 +34648,57 @@ function envoyExtension(pi) {
34475
34648
  });
34476
34649
  return connection;
34477
34650
  };
34478
- const postDispatchReply = async (delivery, result) => {
34479
- await postDeliveryReply(currentDispatchConfig(), sessionID, delivery, result);
34651
+ const postDispatchReply = async (delivery, result, replySessionID = sessionID) => {
34652
+ await postDeliveryReply(currentDispatchConfig(), replySessionID, delivery, result);
34480
34653
  };
34481
- const acceptedUserTurn = async (rendered) => {
34654
+ const typedPictures = new TypedPictures;
34655
+ pi.on("input", async (event) => {
34656
+ if (event.source !== "extension")
34657
+ return;
34658
+ const images = typedPictures.handOver(event.text);
34659
+ return images === undefined ? undefined : { images: [...event.images ?? [], ...images] };
34660
+ });
34661
+ const runTypedInput = (sendUserInput, delivery, replySessionID, turn, content, pictureAddresses2) => {
34662
+ const images = typeof content === "string" ? [] : content.filter((block) => block.type === "image");
34663
+ const noted = sessionID;
34664
+ if (images.length > 0) {
34665
+ typedPictures.admit(turn.messageId, {
34666
+ addresses: pictureAddresses2,
34667
+ body: turn.body,
34668
+ images,
34669
+ sessionID: noted
34670
+ });
34671
+ }
34672
+ noteTypedUserTurn(noted, turn.messageId);
34673
+ notedSessionIDs.add(noted);
34674
+ Promise.resolve().then(() => sendUserInput(turn.body, {
34675
+ ...turn.mode === "aside" && { deliverAs: "aside" },
34676
+ tag: turn.messageId
34677
+ })).then((result) => {
34678
+ const submitted = result.handled === "prompt" || result.handled === "skill";
34679
+ if (submitted) {
34680
+ typedPictures.inputPassed(turn.messageId);
34681
+ } else {
34682
+ dropInjectedUserTurn(noted, turn.messageId);
34683
+ typedPictures.withdraw(turn.messageId);
34684
+ }
34685
+ return typedInputReply(turn.body, result);
34686
+ }, (error48) => {
34687
+ dropInjectedUserTurn(noted, turn.messageId);
34688
+ typedPictures.withdraw(turn.messageId);
34689
+ return typedInputReply(turn.body, { error: messageFor(error48) });
34690
+ }).then(async (reply) => {
34691
+ if (reply !== undefined)
34692
+ await postDispatchReply(delivery, { body: reply }, replySessionID);
34693
+ }).catch((error48) => {
34694
+ logger3.warn("envoy: the reply to a person's typed input did not reach Dispatch", {
34695
+ attempt: delivery.attempt,
34696
+ error: messageFor(error48),
34697
+ messageID: delivery.id
34698
+ });
34699
+ });
34700
+ };
34701
+ const acceptedUserTurn = async (rendered, acceptedSessionID) => {
34482
34702
  if (!isUserTurnCandidate(rendered))
34483
34703
  return;
34484
34704
  const { delivery } = rendered;
@@ -34494,13 +34714,14 @@ function envoyExtension(pi) {
34494
34714
  if (dispatch === undefined)
34495
34715
  return;
34496
34716
  const accepted = await dispatch.acceptMessageDelivery(delivery.id, delivery.attempt, {
34497
- actor: { id: sessionID, kind: "session" }
34717
+ actor: { id: acceptedSessionID, kind: "session" }
34498
34718
  });
34499
34719
  const turn = turnFromAccept(accepted);
34500
34720
  if (turn === undefined) {
34501
34721
  logger3.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 });
34722
+ return;
34502
34723
  }
34503
- return turn;
34724
+ return { delivery, turn };
34504
34725
  } catch (error48) {
34505
34726
  logger3.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) });
34506
34727
  return;
@@ -34562,20 +34783,34 @@ function envoyExtension(pi) {
34562
34783
  }
34563
34784
  }
34564
34785
  } else {
34565
- const turn = await acceptedUserTurn(rendered);
34786
+ const acceptedSessionID = sessionID;
34787
+ const accepted = await acceptedUserTurn(rendered, acceptedSessionID);
34566
34788
  const shown = shownPictures(sessionID);
34567
- if (turn === undefined) {
34789
+ if (accepted === undefined) {
34568
34790
  const card = await withDeliveredPictures(rendered.content, rendered.pictures ?? [], () => timedDispatchClient(DELIVERY_PICTURES_TIMEOUT_MS), true, shown);
34569
34791
  pi.sendMessage({ customType: "envoy-message", content: card.content, display: true }, {
34570
34792
  deliverAs: rendered.delivery?.mode === "aside" ? "aside" : "steer",
34571
34793
  triggerTurn: true
34572
34794
  });
34573
- markPicturesShown(shown, card.shown);
34795
+ markPicturesShown(shownPictures(sessionID), card.shown);
34574
34796
  } else {
34797
+ const { turn } = accepted;
34575
34798
  const delivery = await withDeliveredPictures(turn.body, pictureAddresses(turn.body), () => timedDispatchClient(DELIVERY_PICTURES_TIMEOUT_MS), false, shown);
34576
- noteInjectedUserTurn(sessionID, turn.body, turn.messageId);
34577
- pi.sendUserMessage(delivery.content, turn.mode === "aside" ? { deliverAs: "aside" } : undefined);
34578
- markPicturesShown(shown, delivery.shown);
34799
+ const sendUserInput = pi.sendUserInput;
34800
+ if (sendUserInput === undefined) {
34801
+ const refusal = typedInputReply(turn.body, { hostVersion: VERSION });
34802
+ if (refusal !== undefined) {
34803
+ await postDispatchReply(accepted.delivery, { body: refusal }, acceptedSessionID);
34804
+ } else {
34805
+ const injectionSessionID = sessionID;
34806
+ noteInjectedUserTurn(injectionSessionID, turn.body, turn.messageId);
34807
+ notedSessionIDs.add(injectionSessionID);
34808
+ pi.sendUserMessage(delivery.content, turn.mode === "aside" ? { deliverAs: "aside" } : undefined);
34809
+ markPicturesShown(shownPictures(injectionSessionID), delivery.shown);
34810
+ }
34811
+ } else {
34812
+ runTypedInput(sendUserInput, accepted.delivery, acceptedSessionID, turn, delivery.content, delivery.shown);
34813
+ }
34579
34814
  }
34580
34815
  }
34581
34816
  } catch (error48) {
@@ -34649,6 +34884,39 @@ function envoyExtension(pi) {
34649
34884
  });
34650
34885
  let agentStreamControl;
34651
34886
  let agentStreamControlSession = "";
34887
+ let agentStreamCommandsRefreshedOnWatchAt = -Infinity;
34888
+ let agentStreamViewerAttached = false;
34889
+ const refreshAgentStreamCommands = (session) => {
34890
+ if (typeof pi.sendUserInput !== "function")
34891
+ return;
34892
+ const builtins = hostBuiltinCommands?.() ?? [];
34893
+ const reserved = new Set(builtins.flatMap((builtin) => [builtin.name, ...builtin.aliases]));
34894
+ const live = host2.AgentRegistry?.global().list().find((ref) => ref.session?.sessionManager.getSessionId() === session)?.session;
34895
+ const commands = builtins.map((builtin) => ({
34896
+ description: builtin.description,
34897
+ name: builtin.name,
34898
+ source: "builtin",
34899
+ ...builtin.terminalOnly && { terminalOnly: true }
34900
+ }));
34901
+ for (const command of [
34902
+ ...pi.getCommands?.() ?? [],
34903
+ ...[...live?.slashCommands ?? [], ...live?.promptTemplates ?? []].map((file2) => ({
34904
+ description: file2.description,
34905
+ name: file2.name,
34906
+ source: "prompt"
34907
+ }))
34908
+ ]) {
34909
+ if (reserved.has(command.name))
34910
+ continue;
34911
+ const description = typeof command.description === "string" ? { description: command.description } : {};
34912
+ commands.push({
34913
+ name: command.name,
34914
+ source: command.source,
34915
+ ...description
34916
+ });
34917
+ }
34918
+ agentStream.setCommands(agentStreamFramesSubject(session), commands);
34919
+ };
34652
34920
  const pumpAgentStreamControl = async (subscription, session) => {
34653
34921
  try {
34654
34922
  for await (const message of subscription) {
@@ -34659,9 +34927,24 @@ function envoyExtension(pi) {
34659
34927
  const request = parsed;
34660
34928
  if (request.type !== "replay" && request.type !== "watch")
34661
34929
  continue;
34930
+ const attached = agentStreamViewerAttached;
34931
+ if (request.type === "replay" || !attached || Date.now() - agentStreamCommandsRefreshedOnWatchAt >= AGENT_STREAM_WATCH_INTERVAL_MS) {
34932
+ try {
34933
+ refreshAgentStreamCommands(session);
34934
+ } catch (error48) {
34935
+ logger3.warn("envoy: could not refresh agent stream commands", {
34936
+ error: messageFor(error48),
34937
+ sessionID: session
34938
+ });
34939
+ }
34940
+ if (request.type === "watch" && attached) {
34941
+ agentStreamCommandsRefreshedOnWatchAt = Date.now();
34942
+ }
34943
+ }
34662
34944
  const reply = message.reply;
34663
34945
  const answer = request.type === "replay" && reply !== undefined ? JSON.stringify(agentStream.replay(session)) : undefined;
34664
34946
  agentStream.noteViewer();
34947
+ agentStreamViewerAttached = true;
34665
34948
  if (answer === undefined || reply === undefined)
34666
34949
  continue;
34667
34950
  connection?.publish(reply, codec2.encode(answer));
@@ -34690,8 +34973,11 @@ function envoyExtension(pi) {
34690
34973
  return;
34691
34974
  agentStreamControl?.unsubscribe();
34692
34975
  agentStreamControl = undefined;
34693
- if (agentStreamControlSession !== session)
34976
+ if (agentStreamControlSession !== session) {
34694
34977
  agentStream.reset();
34978
+ agentStreamViewerAttached = false;
34979
+ agentStreamCommandsRefreshedOnWatchAt = -Infinity;
34980
+ }
34695
34981
  agentStreamControlSession = session;
34696
34982
  const active = await ensureConnection();
34697
34983
  const subscription = active.subscribe(agentStreamControlSubject(session));
@@ -34704,6 +34990,9 @@ function envoyExtension(pi) {
34704
34990
  agentStream.record(agentStreamFramesSubject(sessionID), message, streaming, matchInjectedUserTurn(sessionID, message));
34705
34991
  };
34706
34992
  pi.on("message_start", async (event) => {
34993
+ const shown = typedPictures.started(event.message);
34994
+ if (shown !== undefined)
34995
+ markPicturesShown(shownPictures(sessionID), shown);
34707
34996
  recordAgentStreamMessage(event.message, true);
34708
34997
  });
34709
34998
  pi.on("message_update", async (event) => {
@@ -32,10 +32,19 @@ session with no issue (`POST /api/v1/agents/{session_id}/messages`) stays human-
32
32
  A human can also message you directly from the **Agents** page, with no issue at all. On Oh My
33
33
  Pi, a person's **Send** or **Aside** arrives as their own user message, exactly as if they had
34
34
  typed it at your terminal: your Envoy plugin takes it only once Dispatch accepts it as a person's
35
- own fresh message to you, and injects the text Dispatch stored. Answer it in the conversation as
35
+ own fresh message to you, and runs the text Dispatch stored. Answer it in the conversation as
36
36
  you would anything typed, with no `dispatch message`; the Agents page shows your conversation
37
37
  live, so they read your answer there.
38
38
 
39
+ A Send or Aside that starts with `/` runs as the command it names, as it would at your terminal:
40
+ a prompt template or command file arrives as the user message it expands to, a `/skill:<name>`
41
+ as that skill's prompt, and slash text that names no command as the words themselves; answer each
42
+ as you would typed. A command that runs without reaching you (`/session`, `/compact`), or that
43
+ did not run (one only your own terminal runs, such as `/new`), is answered on Dispatch by your
44
+ plugin, as your reply to that message: what the command printed, or why it did not run. You
45
+ receive nothing for it and write nothing for it. A command that prints and then submits a prompt
46
+ (`/force:<tool> <prompt>`) gets the same reply, and you answer only the prompt it submitted.
47
+
39
48
  Everything else still arrives as a Dispatch frame: a **BTW**, a broadcast, a direct message on a
40
49
  host that takes no user turn from its plugin (Claude Code), and one Dispatch did not accept.
41
50
  That frame names no issue and its `reply_with` hint carries none either; answer it with the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/pi-envoy",
3
- "version": "8.17.0",
3
+ "version": "9.0.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "omp": {