@sjawhar/pi-legion-envoy 5.1.1 → 5.2.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
@@ -30052,7 +30052,7 @@ var dispatchToolSpecs = [
30052
30052
  {
30053
30053
  name: "dispatch_issue",
30054
30054
  example: { project: "DSP", title: "Native workspace" },
30055
- description: "Create a native Dispatch issue for newly tracked work. Search first with dispatch_search; if potentially duplicate issues exist, this returns 409 POSSIBLE_DUPLICATE unless force is true after reading them. " + `Do not use it when an existing issue already covers the work; read or update that issue instead. ${ISSUE_REFERENCE}`,
30055
+ description: "Create a native Dispatch issue for newly tracked work. Search first with dispatch_search; if potentially duplicate issues exist, this returns 409 POSSIBLE_DUPLICATE unless force is true after reading them. " + "A spec holding an ask block whose body breaks its content rule (one or more question paragraphs, then at most one bullet list of options, last) is refused with 400 INVALID_ASK_BLOCK. " + `Do not use it when an existing issue already covers the work; read or update that issue instead. ${ISSUE_REFERENCE}`,
30056
30056
  arguments: (z2) => ({
30057
30057
  project: z2.string().describe("Project key for the new issue."),
30058
30058
  title: z2.string().describe("Concise issue title."),
@@ -30298,7 +30298,7 @@ var dispatchToolSpecs = [
30298
30298
  name: "dispatch_artifact",
30299
30299
  example: { issue: "DSP-1", name: "design.md", content: `# Design
30300
30300
  ` },
30301
- description: "Attach a local file or inline text as an issue artifact or project document. Do not use it to edit a live document; use " + `dispatch_doc_edit instead. Exactly one of path or content is required; artifacts are limited to 25 MiB. ${OWNER_REFERENCE}`,
30301
+ description: "Attach a local file or inline text as an issue artifact or project document. Do not use it to edit a live document; use " + "dispatch_doc_edit instead. Exactly one of path or content is required; artifacts are limited to 25 MiB. " + "Markdown holding an ask block whose body breaks its content rule (one or more question paragraphs, then at most one bullet list of options, last) is refused with 400 INVALID_ASK_BLOCK; a new version of a document is held to it only for the asks it writes or changes. " + `${OWNER_REFERENCE}`,
30302
30302
  arguments: (z2) => ({
30303
30303
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
30304
30304
  project: z2.string().describe("Project key for an unlinked document.").optional(),
@@ -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.1",
16146
16146
  type: "module",
16147
16147
  omp: {
16148
16148
  extensions: [
@@ -30125,7 +30125,7 @@ var dispatchToolSpecs = [
30125
30125
  {
30126
30126
  name: "dispatch_issue",
30127
30127
  example: { project: "DSP", title: "Native workspace" },
30128
- description: "Create a native Dispatch issue for newly tracked work. Search first with dispatch_search; if potentially duplicate issues exist, this returns 409 POSSIBLE_DUPLICATE unless force is true after reading them. " + `Do not use it when an existing issue already covers the work; read or update that issue instead. ${ISSUE_REFERENCE}`,
30128
+ description: "Create a native Dispatch issue for newly tracked work. Search first with dispatch_search; if potentially duplicate issues exist, this returns 409 POSSIBLE_DUPLICATE unless force is true after reading them. " + "A spec holding an ask block whose body breaks its content rule (one or more question paragraphs, then at most one bullet list of options, last) is refused with 400 INVALID_ASK_BLOCK. " + `Do not use it when an existing issue already covers the work; read or update that issue instead. ${ISSUE_REFERENCE}`,
30129
30129
  arguments: (z2) => ({
30130
30130
  project: z2.string().describe("Project key for the new issue."),
30131
30131
  title: z2.string().describe("Concise issue title."),
@@ -30371,7 +30371,7 @@ var dispatchToolSpecs = [
30371
30371
  name: "dispatch_artifact",
30372
30372
  example: { issue: "DSP-1", name: "design.md", content: `# Design
30373
30373
  ` },
30374
- description: "Attach a local file or inline text as an issue artifact or project document. Do not use it to edit a live document; use " + `dispatch_doc_edit instead. Exactly one of path or content is required; artifacts are limited to 25 MiB. ${OWNER_REFERENCE}`,
30374
+ description: "Attach a local file or inline text as an issue artifact or project document. Do not use it to edit a live document; use " + "dispatch_doc_edit instead. Exactly one of path or content is required; artifacts are limited to 25 MiB. " + "Markdown holding an ask block whose body breaks its content rule (one or more question paragraphs, then at most one bullet list of options, last) is refused with 400 INVALID_ASK_BLOCK; a new version of a document is held to it only for the asks it writes or changes. " + `${OWNER_REFERENCE}`,
30375
30375
  arguments: (z2) => ({
30376
30376
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
30377
30377
  project: z2.string().describe("Project key for an unlinked document.").optional(),
@@ -688,7 +688,10 @@ its authoritative value at settlement.
688
688
 
689
689
  Questions about a document must be `ask` blocks, never an `Open questions` prose section. An ask
690
690
  body is one or more question paragraphs followed by an optional bullet list of options, where each
691
- item is `Label: description`. For example:
691
+ item is `Label: description`. A spec, an uploaded document or an uploaded version holding an ask that
692
+ breaks that shape - a code block, heading or quote in it, a paragraph after its options, a second
693
+ list - is refused with `INVALID_ASK_BLOCK`; so is an edit that writes one. An ask someone left
694
+ unreadable in the browser refuses nothing it is carried through unchanged by. For example:
692
695
 
693
696
  ```md
694
697
  :::ask{urgency="high" multiple="false"}
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.1",
4
4
  "type": "module",
5
5
  "omp": {
6
6
  "extensions": [