@sjawhar/pi-legion-envoy 5.0.1 → 5.1.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
@@ -34851,6 +34851,7 @@ function legionRoleClaimBridge() {
34851
34851
  return bridge;
34852
34852
  const createdBridge = {
34853
34853
  instances: [],
34854
+ managedSessions: new Set,
34854
34855
  regained: undefined
34855
34856
  };
34856
34857
  store[LEGION_ROLE_CLAIM_BRIDGE] = createdBridge;
@@ -34858,6 +34859,7 @@ function legionRoleClaimBridge() {
34858
34859
  }
34859
34860
  async function claimEnvoyRole(sessionID, role, context) {
34860
34861
  const bridge = legionRoleClaimBridge();
34862
+ bridge.managedSessions.add(sessionID);
34861
34863
  const instance = bridge.instances.findLast((candidate) => candidate.sessionID() === sessionID) ?? bridge.instances.at(-1);
34862
34864
  if (instance === undefined)
34863
34865
  throw new Error("Envoy has no bound instance for a role claim");
@@ -34975,6 +34977,21 @@ var NATS_RETRY_INTERVAL_MS = 15000;
34975
34977
  var CAPABILITIES_WITHOUT_BTW = DELIVERY_CAPABILITIES.filter((capability) => capability !== "btw");
34976
34978
  var ROLE_CLAIM_ENTRY = "envoy-role-claim";
34977
34979
  var OPEN_ASKS_TIMEOUT_MS = 3000;
34980
+ var LEGION_MANAGED_ENTRY = "legion-managed-session";
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.";
34983
+ var ASK_OPENING_TOOLS = ["dispatch_ask", "dispatch_request_approval"];
34984
+ function isLegionManagedEntry(entry) {
34985
+ if (typeof entry !== "object" || entry === null)
34986
+ return false;
34987
+ if (!("type" in entry) || entry.type !== "custom")
34988
+ return false;
34989
+ if (!("customType" in entry) || entry.customType !== LEGION_MANAGED_ENTRY)
34990
+ return false;
34991
+ if (!("data" in entry) || typeof entry.data !== "object" || entry.data === null)
34992
+ return false;
34993
+ return "session_id" in entry.data && typeof entry.data.session_id === "string";
34994
+ }
34978
34995
  function isRoleClaimEntry(entry) {
34979
34996
  if (typeof entry !== "object" || entry === null)
34980
34997
  return false;
@@ -35030,26 +35047,54 @@ function envoyExtension(pi) {
35030
35047
  let claimedRoleTopic;
35031
35048
  let activeSessionContext;
35032
35049
  const inbox = [];
35050
+ let askAwareness = {
35051
+ session_id: "",
35052
+ period: 0,
35053
+ baseline_as_of: null,
35054
+ fired: false,
35055
+ saw_ask: false
35056
+ };
35057
+ let awarenessGeneration = 0;
35058
+ let askCheckInFlight = false;
35059
+ let legionManagedTranscript = false;
35060
+ const legionManaged = (id) => legionManagedTranscript || legionRoleClaimBridge().managedSessions.has(id);
35033
35061
  const availabilityWarningSessionIDs = new Set;
35034
35062
  const warnAskAvailability = (context, error48) => {
35035
35063
  const sessionID2 = context.sessionManager.getSessionId();
35036
35064
  if (availabilityWarningSessionIDs.has(sessionID2))
35037
35065
  return;
35038
35066
  availabilityWarningSessionIDs.add(sessionID2);
35039
- context.ui.notify(`envoy: Dispatch open-ask check unavailable (${messageFor(error48)}); summary unavailable`, "warning");
35067
+ context.ui.notify(`envoy: Dispatch open-ask check unavailable (${messageFor(error48)}); the stop-time ask reminder is off until it recovers`, "warning");
35040
35068
  };
35041
- const queryOpenAsks = async (requestedSessionID) => {
35069
+ const queryOpenAsks = async (requestedSessionID, since) => {
35042
35070
  const config2 = activeDispatchConfig();
35043
35071
  if (config2 === null)
35044
35072
  return null;
35045
- const snapshot = await new DispatchClient(config2.url, config2.token, fetch, AbortSignal.timeout(OPEN_ASKS_TIMEOUT_MS)).openAsks(requestedSessionID);
35073
+ const snapshot = await new DispatchClient(config2.url, config2.token, fetch, AbortSignal.timeout(OPEN_ASKS_TIMEOUT_MS)).openAsks(requestedSessionID, since);
35046
35074
  availabilityWarningSessionIDs.delete(requestedSessionID);
35047
35075
  return { snapshot, url: config2.url };
35048
35076
  };
35077
+ const markLegionManagedSession = (targetSessionID) => {
35078
+ legionRoleClaimBridge().managedSessions.add(targetSessionID);
35079
+ if (legionManagedTranscript)
35080
+ return;
35081
+ legionManagedTranscript = true;
35082
+ pi.appendEntry(LEGION_MANAGED_ENTRY, { session_id: targetSessionID });
35083
+ };
35049
35084
  const restoreLocalSessionState = (context) => {
35050
35085
  sessionDirectory = context.cwd;
35051
35086
  sessionID = context.sessionManager.getSessionId();
35052
35087
  activeSessionContext = context;
35088
+ 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
+ };
35096
+ legionManagedTranscript = branch.some(isLegionManagedEntry);
35097
+ awarenessGeneration++;
35053
35098
  };
35054
35099
  pi.on("resources_discover", async () => ({ skillPaths: [SKILLS_DIRECTORY] }));
35055
35100
  registerEnvoyMessageRenderer(pi);
@@ -35399,6 +35444,7 @@ function envoyExtension(pi) {
35399
35444
  if (context === undefined || context.sessionManager.getSessionId() !== targetSessionID) {
35400
35445
  throw new Error(`Envoy has no active session for Legion role claim: ${targetSessionID}`);
35401
35446
  }
35447
+ markLegionManagedSession(targetSessionID);
35402
35448
  if (sessionID !== targetSessionID)
35403
35449
  await establishSession(context);
35404
35450
  await registerSession();
@@ -35562,33 +35608,61 @@ function envoyExtension(pi) {
35562
35608
  }
35563
35609
  }
35564
35610
  registerEnvoyWhoamiCommand(pi, () => sessionID);
35565
- pi.on("before_agent_start", async (_event, context) => {
35611
+ pi.on("before_agent_start", async (event, context) => {
35566
35612
  const id = context.sessionManager.getSessionId();
35567
- if (id === "")
35613
+ if (id === "" || event.prompt.trim() === "" || !context.hasUI || legionManaged(id)) {
35614
+ return;
35615
+ }
35616
+ if (await isSubagent(context))
35568
35617
  return;
35569
35618
  try {
35570
35619
  const open = await queryOpenAsks(id);
35571
- if (open === null)
35572
- return;
35573
- return {
35574
- message: {
35575
- customType: "dispatch-open-asks",
35576
- content: `Dispatch authored-ask summary:
35577
- ${formatOpenAsksSummary(open.snapshot, open.url)}`,
35578
- display: false,
35579
- attribution: "agent"
35580
- }
35581
- };
35620
+ if (open !== null)
35621
+ armAskAwareness(id, event.prompt, open.snapshot.as_of);
35582
35622
  } catch (error48) {
35583
35623
  warnAskAvailability(context, error48);
35584
- return {
35585
- message: {
35586
- customType: "dispatch-open-asks",
35587
- content: `Dispatch authored-ask summary unavailable: ${messageFor(error48)}`,
35588
- display: false,
35589
- attribution: "agent"
35590
- }
35591
- };
35624
+ }
35625
+ return;
35626
+ });
35627
+ function armAskAwareness(id, prompt, asOf) {
35628
+ if (prompt.trim() === "")
35629
+ return;
35630
+ awarenessGeneration++;
35631
+ askAwareness = {
35632
+ session_id: id,
35633
+ period: (askAwareness.session_id === id ? askAwareness.period : 0) + 1,
35634
+ baseline_as_of: asOf,
35635
+ fired: false,
35636
+ saw_ask: false
35637
+ };
35638
+ }
35639
+ pi.on("agent_end", async (event, context) => {
35640
+ const id = context.sessionManager.getSessionId();
35641
+ 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) {
35643
+ return;
35644
+ }
35645
+ const period = askAwareness.period;
35646
+ const generation = awarenessGeneration;
35647
+ askCheckInFlight = true;
35648
+ 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;
35659
+ }
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
+ } finally {
35665
+ askCheckInFlight = false;
35592
35666
  }
35593
35667
  });
35594
35668
  const announceFollow = createFollowAnnouncer((text) => {
@@ -35597,6 +35671,9 @@ ${formatOpenAsksSummary(open.snapshot, open.url)}`,
35597
35671
  pi.on("tool_result", async (event) => {
35598
35672
  if (event.isError)
35599
35673
  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 };
35676
+ }
35600
35677
  announceFollow(event.details);
35601
35678
  });
35602
35679
  async function execute(operation, rawParameters) {
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.0.1",
16145
+ version: "5.1.0",
16146
16146
  type: "module",
16147
16147
  omp: {
16148
16148
  extensions: [
@@ -31220,6 +31220,7 @@ function legionRoleClaimBridge() {
31220
31220
  return bridge;
31221
31221
  const createdBridge = {
31222
31222
  instances: [],
31223
+ managedSessions: new Set,
31223
31224
  regained: undefined
31224
31225
  };
31225
31226
  store[LEGION_ROLE_CLAIM_BRIDGE] = createdBridge;
@@ -31227,6 +31228,7 @@ function legionRoleClaimBridge() {
31227
31228
  }
31228
31229
  async function claimEnvoyRole(sessionID, role, context) {
31229
31230
  const bridge = legionRoleClaimBridge();
31231
+ bridge.managedSessions.add(sessionID);
31230
31232
  const instance = bridge.instances.findLast((candidate) => candidate.sessionID() === sessionID) ?? bridge.instances.at(-1);
31231
31233
  if (instance === undefined)
31232
31234
  throw new Error("Envoy has no bound instance for a role claim");
@@ -327,6 +327,11 @@ everything else in the tree, or use `dispatch_ask` for a human question; workers
327
327
  Sami directly with `dispatch_ask` the same way. Do not create a wait loop for any wake
328
328
  source.
329
329
 
330
+ Never yield while waiting on a human. A human is waiting on you only where an open ask sits
331
+ in their inbox, so open it — `dispatch_ask`, or `dispatch_request_approval` for the spec
332
+ gate — before you stop. Otherwise proceed: proceeding is the default, and a stop that waits
333
+ on nobody stalls the tree until someone notices.
334
+
330
335
  ## Architecture components
331
336
 
332
337
  Bootstrap on a root whose project has an architecture source: in the root's first PR (the implement worker pushes it), write `.dispatch/architecture/<id>.md` files for the planned components — front matter `title`, `parent`, `depends_on`, `external`; no `paths` yet. The importer reads the source branch's head, so the sync and the attach below work only once that PR has merged to the source branch: until then leave the root on `inherit` (the attach would answer `400 COMPONENTS_INPUT`, the component does not exist yet). After the merge, `dispatch_architecture_sync({ project })` and attach the root with `dispatch_issue_update({ issue, components: { mode: "explicit", ids: [...] } })`. Children inherit the root's attachment; give a child its own `components` only when it changes a narrower set, and `{ mode: "none", reason }` when it is not architectural work. Attach before decomposing, and require every implementer to change the component file beside the code it describes in the same review.
@@ -590,3 +590,9 @@ When blocked on lifecycle, scope, or cross-phase matters, `envoy_publish` the ow
590
590
  architect a concise message: issue, phase, verified observation, what you tried, and the
591
591
  decision required. Reach for `dispatch_ask` yourself only for a standalone human question
592
592
  outside that coordination.
593
+
594
+ Never yield while blocked on a decision someone else owns. Before you stop, make the block
595
+ visible where its owner will see it: a lifecycle, scope, or cross-phase decision goes to the
596
+ owning architect as above, and a standalone human question goes in `dispatch_ask`. Otherwise
597
+ proceed: proceeding is the default, and a phase that stops silently holds its issue until
598
+ someone notices.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/pi-legion-envoy",
3
- "version": "5.0.1",
3
+ "version": "5.1.0",
4
4
  "type": "module",
5
5
  "omp": {
6
6
  "extensions": [