@sjawhar/pi-legion-envoy 5.1.1 → 5.2.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
@@ -34979,8 +34979,15 @@ var ROLE_CLAIM_ENTRY = "envoy-role-claim";
34979
34979
  var OPEN_ASKS_TIMEOUT_MS = 3000;
34980
34980
  var LEGION_MANAGED_ENTRY = "legion-managed-session";
34981
34981
  var ASK_REMINDER_MESSAGE = "dispatch-ask-reminder";
34982
- var OPEN_ASKS_REMINDER = "You have no unanswered asks in Dispatch. If you are waiting for human input, open an ask. Otherwise ignore this reminder and continue with any remaining work. Do not reply just to acknowledge this reminder.";
34982
+ var ASK_SELF_CHECK_PROMPT = "Your run has just ended. 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.";
34983
+ var WAITING_VERDICT = /^\W*WAITING\b/;
34984
+ var ECHOED_CHOICE = /\bPROCEEDING\b/;
34985
+ var isWaitingVerdict = (reply) => WAITING_VERDICT.test(reply) && !ECHOED_CHOICE.test(reply);
34986
+ var ASK_SELF_CHECK_TIMEOUT_MS = 60000;
34987
+ var ASK_CHECKS_PER_PERIOD = 5;
34988
+ var UNASKED_WAIT_REMINDER = "You just said you are waiting on a human, but you have no open ask in Dispatch, so nobody knows you are waiting. Open it now with dispatch_ask \u2014 or dispatch_request_approval when what you need is approval of a document \u2014 naming exactly what you need and from whom. Do not reply just to acknowledge this reminder.";
34983
34989
  var ASK_OPENING_TOOLS = ["dispatch_ask", "dispatch_request_approval"];
34990
+ var DISPATCH_TOOL_PREFIX = "dispatch_";
34984
34991
  function isLegionManagedEntry(entry) {
34985
34992
  if (typeof entry !== "object" || entry === null)
34986
34993
  return false;
@@ -35051,11 +35058,23 @@ function envoyExtension(pi) {
35051
35058
  session_id: "",
35052
35059
  period: 0,
35053
35060
  baseline_as_of: null,
35054
- fired: false,
35055
- saw_ask: false
35061
+ check_due: false,
35062
+ checks: 0
35056
35063
  };
35057
35064
  let awarenessGeneration = 0;
35058
35065
  let askCheckInFlight = false;
35066
+ let settledDuringCheck;
35067
+ const takeSettledDuringCheck = () => {
35068
+ const settle = settledDuringCheck;
35069
+ settledDuringCheck = undefined;
35070
+ return settle;
35071
+ };
35072
+ let askCheckAbort;
35073
+ const abortSelfCheck = (reason) => {
35074
+ askCheckAbort?.abort(new Error(reason));
35075
+ askCheckAbort = undefined;
35076
+ };
35077
+ let runSeq = 0;
35059
35078
  let legionManagedTranscript = false;
35060
35079
  const legionManaged = (id) => legionManagedTranscript || legionRoleClaimBridge().managedSessions.has(id);
35061
35080
  const availabilityWarningSessionIDs = new Set;
@@ -35066,6 +35085,19 @@ function envoyExtension(pi) {
35066
35085
  availabilityWarningSessionIDs.add(sessionID2);
35067
35086
  context.ui.notify(`envoy: Dispatch open-ask check unavailable (${messageFor(error48)}); the stop-time ask reminder is off until it recovers`, "warning");
35068
35087
  };
35088
+ const askEphemeral = pi.askEphemeral;
35089
+ const overriddenTimeout = Number(process.env.ENVOY_SELF_CHECK_TIMEOUT_MS);
35090
+ const selfCheckTimeoutMs = Number.isFinite(overriddenTimeout) && overriddenTimeout > 0 ? overriddenTimeout : ASK_SELF_CHECK_TIMEOUT_MS;
35091
+ const selfCheckWarningSessionIDs = new Set;
35092
+ const logSelfCheckFailure = (failedSessionID, error48) => {
35093
+ if (selfCheckWarningSessionIDs.has(failedSessionID))
35094
+ return;
35095
+ selfCheckWarningSessionIDs.add(failedSessionID);
35096
+ logger.warn("envoy: run-end waiting self-check failed; no reminder was sent", {
35097
+ sessionID: failedSessionID,
35098
+ error: messageFor(error48)
35099
+ });
35100
+ };
35069
35101
  const queryOpenAsks = async (requestedSessionID, since) => {
35070
35102
  const config2 = activeDispatchConfig();
35071
35103
  if (config2 === null)
@@ -35086,15 +35118,18 @@ function envoyExtension(pi) {
35086
35118
  sessionID = context.sessionManager.getSessionId();
35087
35119
  activeSessionContext = context;
35088
35120
  const branch = context.sessionManager.getBranch?.() ?? [];
35089
- askAwareness = {
35090
- session_id: sessionID,
35091
- period: 0,
35092
- baseline_as_of: null,
35093
- fired: false,
35094
- saw_ask: false
35095
- };
35121
+ if (askAwareness.session_id !== sessionID) {
35122
+ abortSelfCheck(`session changed from ${askAwareness.session_id || "none"} to ${sessionID}`);
35123
+ askAwareness = {
35124
+ session_id: sessionID,
35125
+ period: 0,
35126
+ baseline_as_of: null,
35127
+ check_due: false,
35128
+ checks: 0
35129
+ };
35130
+ awarenessGeneration++;
35131
+ }
35096
35132
  legionManagedTranscript = branch.some(isLegionManagedEntry);
35097
- awarenessGeneration++;
35098
35133
  };
35099
35134
  pi.on("resources_discover", async () => ({ skillPaths: [SKILLS_DIRECTORY] }));
35100
35135
  registerEnvoyMessageRenderer(pi);
@@ -35610,69 +35645,148 @@ function envoyExtension(pi) {
35610
35645
  registerEnvoyWhoamiCommand(pi, () => sessionID);
35611
35646
  pi.on("before_agent_start", async (event, context) => {
35612
35647
  const id = context.sessionManager.getSessionId();
35613
- if (id === "" || event.prompt.trim() === "" || !context.hasUI || legionManaged(id)) {
35648
+ if (id === "" || event.prompt.trim() === "" || !context.hasUI || askEphemeral === undefined || legionManaged(id)) {
35614
35649
  return;
35615
35650
  }
35616
35651
  if (await isSubagent(context))
35617
35652
  return;
35653
+ awarenessGeneration++;
35654
+ abortSelfCheck("a new user turn superseded the self-check");
35618
35655
  try {
35619
35656
  const open = await queryOpenAsks(id);
35620
35657
  if (open !== null)
35621
- armAskAwareness(id, event.prompt, open.snapshot.as_of);
35658
+ armAskAwareness(id, open.snapshot.as_of);
35622
35659
  } catch (error48) {
35623
35660
  warnAskAvailability(context, error48);
35624
35661
  }
35625
35662
  return;
35626
35663
  });
35627
- function armAskAwareness(id, prompt, asOf) {
35628
- if (prompt.trim() === "")
35629
- return;
35630
- awarenessGeneration++;
35664
+ pi.on("agent_start", async () => {
35665
+ runSeq += 1;
35666
+ });
35667
+ function armAskAwareness(id, asOf) {
35631
35668
  askAwareness = {
35632
35669
  session_id: id,
35633
35670
  period: (askAwareness.session_id === id ? askAwareness.period : 0) + 1,
35634
35671
  baseline_as_of: asOf,
35635
- fired: false,
35636
- saw_ask: false
35672
+ check_due: true,
35673
+ checks: 0
35637
35674
  };
35638
35675
  }
35639
35676
  pi.on("agent_end", async (event, context) => {
35640
35677
  const id = context.sessionManager.getSessionId();
35641
35678
  const lastReply = event.messages?.findLast((message) => message.role === "assistant");
35642
- if (event.willContinue === true || lastReply?.stopReason !== "stop" || shuttingDown || !context.hasUI || id === "" || id !== sessionID || legionManaged(id) || askAwareness.session_id !== id || askAwareness.period === 0 || askAwareness.fired || askAwareness.saw_ask || askAwareness.baseline_as_of === null || askCheckInFlight) {
35679
+ if (event.willContinue === true || lastReply?.stopReason !== "stop" || !context.hasUI || id === "" || !checkOwed(id)) {
35680
+ return;
35681
+ }
35682
+ if (askCheckInFlight) {
35683
+ settledDuringCheck = { context, seenRun: runSeq };
35643
35684
  return;
35644
35685
  }
35645
- const period = askAwareness.period;
35646
- const generation = awarenessGeneration;
35647
35686
  askCheckInFlight = true;
35648
35687
  try {
35649
- let open;
35650
- try {
35651
- open = await queryOpenAsks(id, askAwareness.baseline_as_of);
35652
- } catch (error48) {
35653
- if (generation === awarenessGeneration)
35654
- warnAskAvailability(context, error48);
35655
- return;
35656
- }
35657
- if (open === null || shuttingDown || generation !== awarenessGeneration || sessionID !== id || context.sessionManager.getSessionId() !== id || legionManaged(id) || askAwareness.session_id !== id || askAwareness.period !== period || askAwareness.fired || askAwareness.saw_ask) {
35658
- return;
35688
+ let settle = { context, seenRun: runSeq };
35689
+ while (settle !== undefined) {
35690
+ await checkAtSettle(id, settle);
35691
+ settle = takeSettledDuringCheck();
35692
+ if (settle?.context.sessionManager.getSessionId() !== id || settle.seenRun !== runSeq || !checkOwed(id)) {
35693
+ settle = undefined;
35694
+ }
35659
35695
  }
35660
- if (open.snapshot.count > 0 || open.snapshot.opened_since)
35661
- return;
35662
- askAwareness = { ...askAwareness, fired: true };
35663
- pi.sendMessage({ customType: ASK_REMINDER_MESSAGE, content: OPEN_ASKS_REMINDER, display: false }, { deliverAs: "steer", triggerTurn: true });
35664
35696
  } finally {
35665
35697
  askCheckInFlight = false;
35698
+ settledDuringCheck = undefined;
35666
35699
  }
35667
35700
  });
35701
+ function checkOwed(id) {
35702
+ return !(shuttingDown || legionManaged(id) || askAwareness.session_id !== id || askAwareness.period === 0 || !askAwareness.check_due || askAwareness.checks >= ASK_CHECKS_PER_PERIOD || askAwareness.baseline_as_of === null || askEphemeral === undefined);
35703
+ }
35704
+ async function checkAtSettle(id, settle) {
35705
+ const { context, seenRun } = settle;
35706
+ const period = askAwareness.period;
35707
+ const generation = awarenessGeneration;
35708
+ const baseline = askAwareness.baseline_as_of;
35709
+ if (baseline === null || askEphemeral === undefined)
35710
+ return;
35711
+ const stale = () => shuttingDown || generation !== awarenessGeneration || context.sessionManager.getSessionId() !== id || legionManaged(id) || askAwareness.session_id !== id || askAwareness.period !== period || !askAwareness.check_due;
35712
+ let open;
35713
+ try {
35714
+ open = await queryOpenAsks(id, baseline);
35715
+ } catch (error48) {
35716
+ if (generation === awarenessGeneration)
35717
+ warnAskAvailability(context, error48);
35718
+ return;
35719
+ }
35720
+ if (open === null || stale())
35721
+ return;
35722
+ if (runSeq !== seenRun)
35723
+ return;
35724
+ if (open.snapshot.count > 0 || open.snapshot.opened_since) {
35725
+ askAwareness = {
35726
+ ...askAwareness,
35727
+ check_due: false,
35728
+ baseline_as_of: open.snapshot.as_of
35729
+ };
35730
+ return;
35731
+ }
35732
+ const abort = new AbortController;
35733
+ askCheckAbort = abort;
35734
+ let answered;
35735
+ try {
35736
+ answered = askEphemeral({ prompt: ASK_SELF_CHECK_PROMPT, signal: abort.signal }).then((reply) => reply.replyText, (error48) => {
35737
+ if (!abort.signal.aborted)
35738
+ logSelfCheckFailure(id, error48);
35739
+ return;
35740
+ });
35741
+ } catch (error48) {
35742
+ logSelfCheckFailure(id, error48);
35743
+ answered = Promise.resolve(undefined);
35744
+ }
35745
+ const expiry = Promise.withResolvers();
35746
+ const expire = setTimeout(() => {
35747
+ const timedOut = new Error(`self-check timed out after ${selfCheckTimeoutMs} ms`);
35748
+ abort.abort(timedOut);
35749
+ logSelfCheckFailure(id, timedOut);
35750
+ expiry.resolve(undefined);
35751
+ }, selfCheckTimeoutMs);
35752
+ let verdict;
35753
+ try {
35754
+ verdict = await Promise.race([answered, expiry.promise]);
35755
+ } finally {
35756
+ clearTimeout(expire);
35757
+ if (askCheckAbort === abort)
35758
+ askCheckAbort = undefined;
35759
+ }
35760
+ if (stale()) {
35761
+ abort.abort(new Error("the self-check was superseded before its verdict arrived"));
35762
+ return;
35763
+ }
35764
+ const superseded = runSeq !== seenRun;
35765
+ askAwareness = {
35766
+ ...askAwareness,
35767
+ check_due: superseded,
35768
+ checks: askAwareness.checks + 1,
35769
+ baseline_as_of: open.snapshot.as_of
35770
+ };
35771
+ if (superseded)
35772
+ return;
35773
+ if (verdict === undefined || !isWaitingVerdict(verdict.trim()))
35774
+ return;
35775
+ pi.sendMessage({ customType: ASK_REMINDER_MESSAGE, content: UNASKED_WAIT_REMINDER, display: false }, { deliverAs: "steer", triggerTurn: true });
35776
+ }
35668
35777
  const announceFollow = createFollowAnnouncer((text) => {
35669
35778
  pi.sendMessage({ customType: "envoy-message", content: text, display: true, details: LOCAL_ENVOY_NOTICE }, { deliverAs: "steer", triggerTurn: false });
35670
35779
  });
35671
- pi.on("tool_result", async (event) => {
35780
+ pi.on("tool_result", async (event, context) => {
35672
35781
  if (event.isError)
35673
35782
  return;
35674
- if (ASK_OPENING_TOOLS.includes(event.toolName) && askAwareness.session_id === sessionID && askAwareness.period > 0 && !askAwareness.saw_ask) {
35675
- askAwareness = { ...askAwareness, saw_ask: true };
35783
+ if (askAwareness.session_id === context.sessionManager.getSessionId() && askAwareness.period > 0) {
35784
+ if (ASK_OPENING_TOOLS.includes(event.toolName)) {
35785
+ askAwareness = { ...askAwareness, check_due: false };
35786
+ abortSelfCheck("the agent opened the ask itself");
35787
+ } else if (!event.toolName.startsWith(DISPATCH_TOOL_PREFIX)) {
35788
+ askAwareness = { ...askAwareness, check_due: true };
35789
+ }
35676
35790
  }
35677
35791
  announceFollow(event.details);
35678
35792
  });
package/dist/legion.js CHANGED
@@ -16142,7 +16142,7 @@ import { logger } from "@oh-my-pi/pi-utils";
16142
16142
  // package.json
16143
16143
  var package_default = {
16144
16144
  name: "@sjawhar/pi-legion-envoy",
16145
- version: "5.1.1",
16145
+ version: "5.2.0",
16146
16146
  type: "module",
16147
16147
  omp: {
16148
16148
  extensions: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/pi-legion-envoy",
3
- "version": "5.1.1",
3
+ "version": "5.2.0",
4
4
  "type": "module",
5
5
  "omp": {
6
6
  "extensions": [