@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)});
|
|
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 (
|
|
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
|
|
35572
|
-
|
|
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
|
-
|
|
35585
|
-
|
|
35586
|
-
|
|
35587
|
-
|
|
35588
|
-
|
|
35589
|
-
|
|
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
|
|
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.
|