@sjawhar/pi-legion-envoy 1.27.1 → 1.28.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 +35 -211
- package/dist/legion.js +35 -211
- package/dist/skills/legion-retro/SKILL.md +1 -1
- package/dist/skills/legion-worker/SKILL.md +10 -8
- package/package.json +1 -1
package/dist/envoy.js
CHANGED
|
@@ -29746,7 +29746,7 @@ var MessageEventPayloadSchema = object({
|
|
|
29746
29746
|
});
|
|
29747
29747
|
var DispatchTargetedMessagePayloadSchema = MessageEventPayloadSchema.extend({
|
|
29748
29748
|
id: string2(),
|
|
29749
|
-
issue_key: string2(),
|
|
29749
|
+
issue_key: string2().nullable(),
|
|
29750
29750
|
author: object({ kind: string2(), id: string2() }),
|
|
29751
29751
|
body: string2(),
|
|
29752
29752
|
target: string2(),
|
|
@@ -29759,6 +29759,7 @@ var MessageDeliveryEventPayloadSchema = object({
|
|
|
29759
29759
|
attempt: number2().int().positive().optional(),
|
|
29760
29760
|
delivery: _enum2(["btw", "aside", "steer"]).optional(),
|
|
29761
29761
|
session_id: string2().optional(),
|
|
29762
|
+
target: string2().optional(),
|
|
29762
29763
|
title: string2().optional(),
|
|
29763
29764
|
state: _enum2(["sent", "failed"]).optional(),
|
|
29764
29765
|
error: string2().optional()
|
|
@@ -31095,7 +31096,7 @@ var DispatchTargetedFrameSchema = object({
|
|
|
31095
31096
|
}).refine(({ event }) => event.type === "message.created" && DispatchTargetedMessagePayloadSchema.safeParse(event.payload).success, { message: "targeted delivery requires a message.created event" });
|
|
31096
31097
|
var RecoverableDispatchDeliveryFailureSchema = object({
|
|
31097
31098
|
event: object({
|
|
31098
|
-
issue_key: string2(),
|
|
31099
|
+
issue_key: string2().nullable(),
|
|
31099
31100
|
payload: object({ id: string2(), body: string2().optional() }).passthrough()
|
|
31100
31101
|
}).passthrough(),
|
|
31101
31102
|
delivery: DispatchDeliveryRequestSchema
|
|
@@ -31298,7 +31299,7 @@ function renderInbound(raw, sessionID, subject) {
|
|
|
31298
31299
|
askQuestion = dispatchAskQuestion(frame.event);
|
|
31299
31300
|
messageReplyPreview = dispatchMessageReplyPreview(frame.event);
|
|
31300
31301
|
dispatchReply = dispatchCommentReplyWith(frame.event, subject ?? envelope.topic);
|
|
31301
|
-
if (frame.event.type === "message.created"
|
|
31302
|
+
if (frame.event.type === "message.created") {
|
|
31302
31303
|
const message = MessageEventPayloadSchema.safeParse(frame.event.payload);
|
|
31303
31304
|
const requested = DispatchDeliveryRequestSchema.safeParse(frame.delivery);
|
|
31304
31305
|
if (message.success && requested.success && message.data.id !== undefined) {
|
|
@@ -31309,7 +31310,9 @@ function renderInbound(raw, sessionID, subject) {
|
|
|
31309
31310
|
issueKey: frame.event.issue_key,
|
|
31310
31311
|
body: message.data.body ?? envelope.payload_summary ?? ""
|
|
31311
31312
|
};
|
|
31312
|
-
|
|
31313
|
+
if (frame.event.issue_key !== null) {
|
|
31314
|
+
dispatchReply = `dispatch_message(issue="${frame.event.issue_key}", in_reply_to="${message.data.id}", body="...")`;
|
|
31315
|
+
}
|
|
31313
31316
|
}
|
|
31314
31317
|
}
|
|
31315
31318
|
dispatchEvent = {
|
|
@@ -32402,12 +32405,23 @@ async function executeDispatchTool(input) {
|
|
|
32402
32405
|
if (!input.config.enabled || !configUrl || !configToken) {
|
|
32403
32406
|
throw new Error("Dispatch is disabled; resolve both DISPATCH_URL and DISPATCH_TOKEN");
|
|
32404
32407
|
}
|
|
32408
|
+
const baseFetch = input.fetchImpl ?? fetch;
|
|
32409
|
+
const fetchImpl = Object.assign(async (...args) => {
|
|
32410
|
+
try {
|
|
32411
|
+
return await baseFetch(...args);
|
|
32412
|
+
} catch (error) {
|
|
32413
|
+
if (error instanceof TypeError) {
|
|
32414
|
+
throw new Error(`Dispatch at ${configUrl} is unreachable: ${error.message}. If the Dispatch URL changed, restart this agent process so it picks up the new configuration.`);
|
|
32415
|
+
}
|
|
32416
|
+
throw error;
|
|
32417
|
+
}
|
|
32418
|
+
}, { preconnect: baseFetch.preconnect });
|
|
32405
32419
|
if (input.tool === "dispatch_open_asks") {
|
|
32406
32420
|
const sessionId = input.sessionId?.trim();
|
|
32407
32421
|
if (!sessionId)
|
|
32408
32422
|
throw new Error("host session id is required for dispatch_open_asks");
|
|
32409
32423
|
toolSchema(input.tool).parse(input.args);
|
|
32410
|
-
const client = new DispatchClient(configUrl, configToken,
|
|
32424
|
+
const client = new DispatchClient(configUrl, configToken, fetchImpl, input.signal);
|
|
32411
32425
|
const response = await client.openAsks(sessionId);
|
|
32412
32426
|
return { text: formatOpenAsksSummary(response, configUrl), details: { ...response } };
|
|
32413
32427
|
}
|
|
@@ -32416,7 +32430,7 @@ async function executeDispatchTool(input) {
|
|
|
32416
32430
|
const ownerArguments = await resolveOwnerArguments(input.tool, input.args, input.cwd, env, exec);
|
|
32417
32431
|
const args = toolSchema(input.tool).parse(ownerArguments.args);
|
|
32418
32432
|
const actor = toolActor(await resolveOrigin(env, exec, input.cwd), input);
|
|
32419
|
-
const client = new DispatchClient(configUrl, configToken,
|
|
32433
|
+
const client = new DispatchClient(configUrl, configToken, fetchImpl, input.signal);
|
|
32420
32434
|
const owner = ownerArguments.owner?.kind === "issue" ? {
|
|
32421
32435
|
kind: "issue",
|
|
32422
32436
|
issue: await ensureIssue(client, ownerArguments.owner.issue, actor)
|
|
@@ -32603,17 +32617,18 @@ async function executeDispatchTool(input) {
|
|
|
32603
32617
|
}
|
|
32604
32618
|
case "dispatch_message": {
|
|
32605
32619
|
const inReplyTo = messageInReplyTo(args);
|
|
32606
|
-
const
|
|
32620
|
+
const issueKey = issue();
|
|
32621
|
+
const message = await client.message(issueKey, {
|
|
32607
32622
|
body: stringArg(args, "body"),
|
|
32608
32623
|
...inReplyTo === undefined ? {} : { in_reply_to: inReplyTo },
|
|
32609
32624
|
actor
|
|
32610
32625
|
});
|
|
32611
|
-
const messageRef = `dispatch://${
|
|
32626
|
+
const messageRef = `dispatch://${issueKey}/message/${message.id}`;
|
|
32612
32627
|
return {
|
|
32613
32628
|
text: `Posted message ${message.id} (${messageRef})`,
|
|
32614
32629
|
details: {
|
|
32615
|
-
issue:
|
|
32616
|
-
topic: dispatchIssueSubject(
|
|
32630
|
+
issue: issueKey,
|
|
32631
|
+
topic: dispatchIssueSubject(issueKey, ">"),
|
|
32617
32632
|
message: message.id
|
|
32618
32633
|
}
|
|
32619
32634
|
};
|
|
@@ -33255,59 +33270,7 @@ async function copySessionID(copyToClipboard, sessionID) {
|
|
|
33255
33270
|
var codec2 = import_nats.StringCodec();
|
|
33256
33271
|
var NATS_RETRY_INTERVAL_MS = 15000;
|
|
33257
33272
|
var ROLE_CLAIM_ENTRY = "envoy-role-claim";
|
|
33258
|
-
var ASK_AWARENESS_ENTRY = "dispatch-ask-awareness";
|
|
33259
|
-
var LEGION_MANAGED_ENTRY = "legion-managed-session";
|
|
33260
33273
|
var OPEN_ASKS_TIMEOUT_MS = 3000;
|
|
33261
|
-
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.";
|
|
33262
|
-
function isAskAwarenessEntry(entry) {
|
|
33263
|
-
if (typeof entry !== "object" || entry === null)
|
|
33264
|
-
return false;
|
|
33265
|
-
if (!("type" in entry) || entry.type !== "custom")
|
|
33266
|
-
return false;
|
|
33267
|
-
if (!("customType" in entry) || entry.customType !== ASK_AWARENESS_ENTRY)
|
|
33268
|
-
return false;
|
|
33269
|
-
if (!("data" in entry) || typeof entry.data !== "object" || entry.data === null)
|
|
33270
|
-
return false;
|
|
33271
|
-
const data = entry.data;
|
|
33272
|
-
return "session_id" in data && typeof data.session_id === "string" && "period" in data && typeof data.period === "number" && "baseline_as_of" in data && (typeof data.baseline_as_of === "string" || data.baseline_as_of === null) && "checked" in data && typeof data.checked === "boolean" && "fired" in data && typeof data.fired === "boolean" && "saw_ask" in data && typeof data.saw_ask === "boolean";
|
|
33273
|
-
}
|
|
33274
|
-
function isLegionManagedEntry(entry) {
|
|
33275
|
-
if (typeof entry !== "object" || entry === null)
|
|
33276
|
-
return false;
|
|
33277
|
-
if (!("type" in entry) || entry.type !== "custom")
|
|
33278
|
-
return false;
|
|
33279
|
-
if (!("customType" in entry) || entry.customType !== LEGION_MANAGED_ENTRY)
|
|
33280
|
-
return false;
|
|
33281
|
-
if (!("data" in entry) || typeof entry.data !== "object" || entry.data === null)
|
|
33282
|
-
return false;
|
|
33283
|
-
return "session_id" in entry.data && typeof entry.data.session_id === "string";
|
|
33284
|
-
}
|
|
33285
|
-
function emptyAskAwareness(sessionID) {
|
|
33286
|
-
return {
|
|
33287
|
-
session_id: sessionID,
|
|
33288
|
-
period: 0,
|
|
33289
|
-
baseline_as_of: null,
|
|
33290
|
-
checked: false,
|
|
33291
|
-
fired: false,
|
|
33292
|
-
saw_ask: false
|
|
33293
|
-
};
|
|
33294
|
-
}
|
|
33295
|
-
function transcriptAskAwareness(branch, sessionID) {
|
|
33296
|
-
let state;
|
|
33297
|
-
for (const entry of branch) {
|
|
33298
|
-
if (isAskAwarenessEntry(entry) && entry.data.session_id === sessionID)
|
|
33299
|
-
state = entry.data;
|
|
33300
|
-
}
|
|
33301
|
-
return state;
|
|
33302
|
-
}
|
|
33303
|
-
function transcriptMarksLegionSession(branch, sessionID) {
|
|
33304
|
-
return branch.some((entry) => isLegionManagedEntry(entry) && entry.data.session_id === sessionID);
|
|
33305
|
-
}
|
|
33306
|
-
function isHumanTurnInitiator(message) {
|
|
33307
|
-
if (message.attribution !== "user")
|
|
33308
|
-
return false;
|
|
33309
|
-
return message.role === "user" || message.role === "custom" && (message.customType === "skill-prompt" || message.customType === "collab-prompt");
|
|
33310
|
-
}
|
|
33311
33274
|
function isRoleClaimEntry(entry) {
|
|
33312
33275
|
if (typeof entry !== "object" || entry === null)
|
|
33313
33276
|
return false;
|
|
@@ -33327,15 +33290,13 @@ function legionRoleClaimBridge() {
|
|
|
33327
33290
|
let bridge = store[LEGION_ROLE_CLAIM_BRIDGE];
|
|
33328
33291
|
if (bridge === undefined) {
|
|
33329
33292
|
const ready = Promise.withResolvers();
|
|
33330
|
-
bridge = { ready
|
|
33293
|
+
bridge = { ready };
|
|
33331
33294
|
store[LEGION_ROLE_CLAIM_BRIDGE] = bridge;
|
|
33332
33295
|
}
|
|
33333
33296
|
return bridge;
|
|
33334
33297
|
}
|
|
33335
33298
|
async function claimEnvoyRole(sessionID, role, context) {
|
|
33336
33299
|
const bridge = legionRoleClaimBridge();
|
|
33337
|
-
bridge.managedSessions.add(sessionID);
|
|
33338
|
-
bridge.markManagedSession?.(sessionID, context);
|
|
33339
33300
|
const claim = bridge.claim ?? await bridge.ready.promise;
|
|
33340
33301
|
await claim(sessionID, role, context);
|
|
33341
33302
|
}
|
|
@@ -33384,87 +33345,26 @@ function envoyExtension(pi) {
|
|
|
33384
33345
|
let claimedRoleTopic;
|
|
33385
33346
|
let activeSessionContext;
|
|
33386
33347
|
const inbox = [];
|
|
33387
|
-
|
|
33388
|
-
let candidateBaseline;
|
|
33389
|
-
let awarenessGeneration = 0;
|
|
33390
|
-
let availabilityWarningPeriod;
|
|
33391
|
-
let agentRunning = false;
|
|
33392
|
-
let agentWillContinue = false;
|
|
33393
|
-
let legionManagedSessionID = "";
|
|
33394
|
-
let terminalInputUnsubscribe;
|
|
33395
|
-
const persistAskAwareness = (state) => {
|
|
33396
|
-
askAwareness = state;
|
|
33397
|
-
pi.appendEntry(ASK_AWARENESS_ENTRY, state);
|
|
33398
|
-
};
|
|
33348
|
+
const availabilityWarningSessionIDs = new Set;
|
|
33399
33349
|
const warnAskAvailability = (context, error) => {
|
|
33400
|
-
const
|
|
33401
|
-
if (
|
|
33350
|
+
const sessionID = context.sessionManager.getSessionId();
|
|
33351
|
+
if (availabilityWarningSessionIDs.has(sessionID))
|
|
33402
33352
|
return;
|
|
33403
|
-
|
|
33404
|
-
context.ui.notify(`envoy: Dispatch open-ask check unavailable (${messageFor(error)});
|
|
33353
|
+
availabilityWarningSessionIDs.add(sessionID);
|
|
33354
|
+
context.ui.notify(`envoy: Dispatch open-ask check unavailable (${messageFor(error)}); summary unavailable`, "warning");
|
|
33405
33355
|
};
|
|
33406
|
-
const queryOpenAsks = async (requestedSessionID
|
|
33356
|
+
const queryOpenAsks = async (requestedSessionID) => {
|
|
33407
33357
|
const config = activeDispatchConfig();
|
|
33408
33358
|
if (config === null)
|
|
33409
33359
|
return null;
|
|
33410
|
-
const
|
|
33411
|
-
|
|
33412
|
-
return
|
|
33413
|
-
};
|
|
33414
|
-
const setReminderBaseline = (requestedSessionID, period, asOf) => {
|
|
33415
|
-
candidateBaseline = { sessionID: requestedSessionID, asOf };
|
|
33416
|
-
if (askAwareness.session_id !== requestedSessionID || askAwareness.period !== period || askAwareness.checked || askAwareness.fired || askAwareness.baseline_as_of !== null) {
|
|
33417
|
-
return;
|
|
33418
|
-
}
|
|
33419
|
-
persistAskAwareness({ ...askAwareness, baseline_as_of: asOf });
|
|
33420
|
-
};
|
|
33421
|
-
const refreshReminderBaseline = async (requestedSessionID, period, context) => {
|
|
33422
|
-
try {
|
|
33423
|
-
const snapshot = await queryOpenAsks(requestedSessionID);
|
|
33424
|
-
if (snapshot === null)
|
|
33425
|
-
return;
|
|
33426
|
-
setReminderBaseline(requestedSessionID, period, snapshot.as_of);
|
|
33427
|
-
} catch (error) {
|
|
33428
|
-
warnAskAvailability(context, error);
|
|
33429
|
-
}
|
|
33430
|
-
};
|
|
33431
|
-
const armAskReminder = async (context, refreshBaseline = false) => {
|
|
33432
|
-
const id = context.sessionManager.getSessionId();
|
|
33433
|
-
if (id === "" || activeDispatchConfig() === null)
|
|
33434
|
-
return;
|
|
33435
|
-
awarenessGeneration++;
|
|
33436
|
-
availabilityWarningPeriod = undefined;
|
|
33437
|
-
const baseline = !agentRunning && !refreshBaseline && candidateBaseline !== undefined && candidateBaseline.sessionID === id ? candidateBaseline.asOf : null;
|
|
33438
|
-
const previousPeriod = askAwareness.session_id === id ? askAwareness.period : 0;
|
|
33439
|
-
persistAskAwareness({
|
|
33440
|
-
session_id: id,
|
|
33441
|
-
period: previousPeriod + 1,
|
|
33442
|
-
baseline_as_of: baseline,
|
|
33443
|
-
checked: false,
|
|
33444
|
-
fired: false,
|
|
33445
|
-
saw_ask: false
|
|
33446
|
-
});
|
|
33447
|
-
if (agentRunning || refreshBaseline) {
|
|
33448
|
-
await refreshReminderBaseline(id, askAwareness.period, context);
|
|
33449
|
-
}
|
|
33450
|
-
};
|
|
33451
|
-
const cancelPendingAskReminder = () => {
|
|
33452
|
-
awarenessGeneration++;
|
|
33453
|
-
};
|
|
33454
|
-
const installTerminalInputCancellation = (context) => {
|
|
33455
|
-
terminalInputUnsubscribe?.();
|
|
33456
|
-
terminalInputUnsubscribe = context.ui.onTerminalInput(cancelPendingAskReminder);
|
|
33360
|
+
const snapshot = await new DispatchClient(config.url, config.token, fetch, AbortSignal.timeout(OPEN_ASKS_TIMEOUT_MS)).openAsks(requestedSessionID);
|
|
33361
|
+
availabilityWarningSessionIDs.delete(requestedSessionID);
|
|
33362
|
+
return snapshot;
|
|
33457
33363
|
};
|
|
33458
33364
|
const restoreLocalSessionState = (context) => {
|
|
33459
33365
|
sessionDirectory = context.cwd;
|
|
33460
33366
|
sessionID = context.sessionManager.getSessionId();
|
|
33461
33367
|
activeSessionContext = context;
|
|
33462
|
-
candidateBaseline = undefined;
|
|
33463
|
-
const branch = context.sessionManager.getBranch?.() ?? [];
|
|
33464
|
-
askAwareness = transcriptAskAwareness(branch, sessionID) ?? emptyAskAwareness(sessionID);
|
|
33465
|
-
const persistedLegionSession = transcriptMarksLegionSession(branch, sessionID);
|
|
33466
|
-
const managed = persistedLegionSession || legionRoleClaimBridge().managedSessions.has(sessionID);
|
|
33467
|
-
legionManagedSessionID = managed ? sessionID : "";
|
|
33468
33368
|
};
|
|
33469
33369
|
pi.on("resources_discover", async () => ({ skillPaths: [SKILLS_DIRECTORY] }));
|
|
33470
33370
|
registerEnvoyMessageRenderer(pi);
|
|
@@ -33505,13 +33405,6 @@ function envoyExtension(pi) {
|
|
|
33505
33405
|
const dedupeKey = rendered.envelope?.dedupe_key;
|
|
33506
33406
|
const duplicate = dedupeKey !== undefined && dedupeKeys.has(dedupeKey);
|
|
33507
33407
|
if (!duplicate && !rendered.skip) {
|
|
33508
|
-
if (rendered.dispatchActor?.kind === "user" && activeSessionContext !== undefined) {
|
|
33509
|
-
try {
|
|
33510
|
-
await armAskReminder(activeSessionContext, true);
|
|
33511
|
-
} catch (error) {
|
|
33512
|
-
warnAskAvailability(activeSessionContext, error);
|
|
33513
|
-
}
|
|
33514
|
-
}
|
|
33515
33408
|
const envelope = rendered.envelope;
|
|
33516
33409
|
if (envelope !== undefined) {
|
|
33517
33410
|
inbox.unshift({
|
|
@@ -33831,17 +33724,6 @@ function envoyExtension(pi) {
|
|
|
33831
33724
|
return run;
|
|
33832
33725
|
};
|
|
33833
33726
|
const bridge = legionRoleClaimBridge();
|
|
33834
|
-
bridge.markManagedSession = (targetSessionID, context) => {
|
|
33835
|
-
bridge.managedSessions.add(targetSessionID);
|
|
33836
|
-
if (targetSessionID !== sessionID)
|
|
33837
|
-
return;
|
|
33838
|
-
if (legionManagedSessionID === targetSessionID)
|
|
33839
|
-
return;
|
|
33840
|
-
legionManagedSessionID = targetSessionID;
|
|
33841
|
-
if (context !== undefined || activeSessionContext !== undefined) {
|
|
33842
|
-
pi.appendEntry(LEGION_MANAGED_ENTRY, { session_id: targetSessionID });
|
|
33843
|
-
}
|
|
33844
|
-
};
|
|
33845
33727
|
const claim = async (targetSessionID, role, callerContext) => {
|
|
33846
33728
|
const context = callerContext ?? activeSessionContext;
|
|
33847
33729
|
if (context === undefined || context.sessionManager.getSessionId() !== targetSessionID) {
|
|
@@ -33855,7 +33737,6 @@ function envoyExtension(pi) {
|
|
|
33855
33737
|
bridge.claim = claim;
|
|
33856
33738
|
bridge.ready.resolve(claim);
|
|
33857
33739
|
pi.on("session_start", async (_event, context) => {
|
|
33858
|
-
installTerminalInputCancellation(context);
|
|
33859
33740
|
const previousSessionID = sessionID;
|
|
33860
33741
|
restoreLocalSessionState(context);
|
|
33861
33742
|
if (dispatchConfig.error !== null) {
|
|
@@ -33891,7 +33772,6 @@ function envoyExtension(pi) {
|
|
|
33891
33772
|
}
|
|
33892
33773
|
});
|
|
33893
33774
|
const rebind = async (reason, context) => {
|
|
33894
|
-
installTerminalInputCancellation(context);
|
|
33895
33775
|
const previousID = sessionID;
|
|
33896
33776
|
restoreLocalSessionState(context);
|
|
33897
33777
|
if (defaults.natsUrls.length === 0)
|
|
@@ -33941,8 +33821,6 @@ function envoyExtension(pi) {
|
|
|
33941
33821
|
]);
|
|
33942
33822
|
} finally {
|
|
33943
33823
|
clearTimeout(timer);
|
|
33944
|
-
terminalInputUnsubscribe?.();
|
|
33945
|
-
terminalInputUnsubscribe = undefined;
|
|
33946
33824
|
connection = undefined;
|
|
33947
33825
|
activeSessionContext = undefined;
|
|
33948
33826
|
subscriptions.clear();
|
|
@@ -33996,7 +33874,6 @@ function envoyExtension(pi) {
|
|
|
33996
33874
|
const snapshot = await queryOpenAsks(id);
|
|
33997
33875
|
if (snapshot === null)
|
|
33998
33876
|
return;
|
|
33999
|
-
setReminderBaseline(id, askAwareness.session_id === id ? askAwareness.period : 0, snapshot.as_of);
|
|
34000
33877
|
return {
|
|
34001
33878
|
message: {
|
|
34002
33879
|
customType: "dispatch-open-asks",
|
|
@@ -34018,60 +33895,7 @@ ${formatOpenAsksSummary(snapshot, currentDispatchConfig().url)}`,
|
|
|
34018
33895
|
};
|
|
34019
33896
|
}
|
|
34020
33897
|
});
|
|
34021
|
-
pi.on("agent_start", async () => {
|
|
34022
|
-
agentRunning = true;
|
|
34023
|
-
agentWillContinue = false;
|
|
34024
|
-
});
|
|
34025
|
-
pi.on("agent_end", async (event) => {
|
|
34026
|
-
agentRunning = false;
|
|
34027
|
-
agentWillContinue = event.willContinue === true;
|
|
34028
|
-
});
|
|
34029
|
-
pi.on("input", async () => {
|
|
34030
|
-
cancelPendingAskReminder();
|
|
34031
|
-
});
|
|
34032
|
-
pi.on("message_start", async (event, context) => {
|
|
34033
|
-
if (!isHumanTurnInitiator(event.message))
|
|
34034
|
-
return;
|
|
34035
|
-
try {
|
|
34036
|
-
await armAskReminder(context);
|
|
34037
|
-
} catch (error) {
|
|
34038
|
-
warnAskAvailability(context, error);
|
|
34039
|
-
}
|
|
34040
|
-
});
|
|
34041
|
-
pi.on("session_stop", async (event, context) => {
|
|
34042
|
-
if (event.stop_hook_active || event.signal.aborted || shuttingDown || agentWillContinue || event.last_assistant_message?.role !== "assistant" || event.session_id !== sessionID || legionManagedSessionID === event.session_id || askAwareness.session_id !== event.session_id || askAwareness.period === 0 || askAwareness.checked || askAwareness.fired) {
|
|
34043
|
-
return;
|
|
34044
|
-
}
|
|
34045
|
-
const baseline = askAwareness.baseline_as_of;
|
|
34046
|
-
if (baseline === null)
|
|
34047
|
-
return;
|
|
34048
|
-
if (context.ui.getEditorText().trim() !== "")
|
|
34049
|
-
return;
|
|
34050
|
-
const claimed = { ...askAwareness, checked: true };
|
|
34051
|
-
const generation = awarenessGeneration;
|
|
34052
|
-
persistAskAwareness(claimed);
|
|
34053
|
-
let snapshot;
|
|
34054
|
-
try {
|
|
34055
|
-
snapshot = await queryOpenAsks(event.session_id, baseline, event.signal);
|
|
34056
|
-
} catch (error) {
|
|
34057
|
-
if (!event.signal.aborted && generation === awarenessGeneration) {
|
|
34058
|
-
warnAskAvailability(context, error);
|
|
34059
|
-
}
|
|
34060
|
-
return;
|
|
34061
|
-
}
|
|
34062
|
-
if (snapshot === null || event.signal.aborted || generation !== awarenessGeneration || shuttingDown || agentWillContinue || context.sessionManager.getSessionId() !== event.session_id || sessionID !== event.session_id || legionManagedSessionID === event.session_id || context.ui.getEditorText().trim() !== "" || askAwareness.session_id !== claimed.session_id || askAwareness.period !== claimed.period || !askAwareness.checked) {
|
|
34063
|
-
return;
|
|
34064
|
-
}
|
|
34065
|
-
if (snapshot.count > 0 || snapshot.opened_since || askAwareness.saw_ask) {
|
|
34066
|
-
return;
|
|
34067
|
-
}
|
|
34068
|
-
persistAskAwareness({ ...askAwareness, fired: true });
|
|
34069
|
-
return { continue: true, additionalContext: OPEN_ASKS_REMINDER };
|
|
34070
|
-
});
|
|
34071
33898
|
pi.on("tool_result", async (event) => {
|
|
34072
|
-
if (!event.isError && event.toolName === "dispatch_ask" && typeof event.details === "object" && event.details !== null && "ask" in event.details && typeof event.details.ask === "string" && askAwareness.session_id === sessionID && askAwareness.period > 0 && !askAwareness.saw_ask) {
|
|
34073
|
-
persistAskAwareness({ ...askAwareness, saw_ask: true });
|
|
34074
|
-
}
|
|
34075
33899
|
if (event.isError)
|
|
34076
33900
|
return;
|
|
34077
33901
|
const topic = dispatchSubscriptionTopic(event.details);
|
package/dist/legion.js
CHANGED
|
@@ -29746,7 +29746,7 @@ var MessageEventPayloadSchema = object({
|
|
|
29746
29746
|
});
|
|
29747
29747
|
var DispatchTargetedMessagePayloadSchema = MessageEventPayloadSchema.extend({
|
|
29748
29748
|
id: string2(),
|
|
29749
|
-
issue_key: string2(),
|
|
29749
|
+
issue_key: string2().nullable(),
|
|
29750
29750
|
author: object({ kind: string2(), id: string2() }),
|
|
29751
29751
|
body: string2(),
|
|
29752
29752
|
target: string2(),
|
|
@@ -29759,6 +29759,7 @@ var MessageDeliveryEventPayloadSchema = object({
|
|
|
29759
29759
|
attempt: number2().int().positive().optional(),
|
|
29760
29760
|
delivery: _enum2(["btw", "aside", "steer"]).optional(),
|
|
29761
29761
|
session_id: string2().optional(),
|
|
29762
|
+
target: string2().optional(),
|
|
29762
29763
|
title: string2().optional(),
|
|
29763
29764
|
state: _enum2(["sent", "failed"]).optional(),
|
|
29764
29765
|
error: string2().optional()
|
|
@@ -31548,7 +31549,7 @@ var DispatchTargetedFrameSchema = object({
|
|
|
31548
31549
|
}).refine(({ event }) => event.type === "message.created" && DispatchTargetedMessagePayloadSchema.safeParse(event.payload).success, { message: "targeted delivery requires a message.created event" });
|
|
31549
31550
|
var RecoverableDispatchDeliveryFailureSchema = object({
|
|
31550
31551
|
event: object({
|
|
31551
|
-
issue_key: string2(),
|
|
31552
|
+
issue_key: string2().nullable(),
|
|
31552
31553
|
payload: object({ id: string2(), body: string2().optional() }).passthrough()
|
|
31553
31554
|
}).passthrough(),
|
|
31554
31555
|
delivery: DispatchDeliveryRequestSchema
|
|
@@ -31751,7 +31752,7 @@ function renderInbound(raw, sessionID, subject) {
|
|
|
31751
31752
|
askQuestion = dispatchAskQuestion(frame.event);
|
|
31752
31753
|
messageReplyPreview = dispatchMessageReplyPreview(frame.event);
|
|
31753
31754
|
dispatchReply = dispatchCommentReplyWith(frame.event, subject ?? envelope.topic);
|
|
31754
|
-
if (frame.event.type === "message.created"
|
|
31755
|
+
if (frame.event.type === "message.created") {
|
|
31755
31756
|
const message = MessageEventPayloadSchema.safeParse(frame.event.payload);
|
|
31756
31757
|
const requested = DispatchDeliveryRequestSchema.safeParse(frame.delivery);
|
|
31757
31758
|
if (message.success && requested.success && message.data.id !== undefined) {
|
|
@@ -31762,7 +31763,9 @@ function renderInbound(raw, sessionID, subject) {
|
|
|
31762
31763
|
issueKey: frame.event.issue_key,
|
|
31763
31764
|
body: message.data.body ?? envelope.payload_summary ?? ""
|
|
31764
31765
|
};
|
|
31765
|
-
|
|
31766
|
+
if (frame.event.issue_key !== null) {
|
|
31767
|
+
dispatchReply = `dispatch_message(issue="${frame.event.issue_key}", in_reply_to="${message.data.id}", body="...")`;
|
|
31768
|
+
}
|
|
31766
31769
|
}
|
|
31767
31770
|
}
|
|
31768
31771
|
dispatchEvent = {
|
|
@@ -32833,12 +32836,23 @@ async function executeDispatchTool(input) {
|
|
|
32833
32836
|
if (!input.config.enabled || !configUrl || !configToken) {
|
|
32834
32837
|
throw new Error("Dispatch is disabled; resolve both DISPATCH_URL and DISPATCH_TOKEN");
|
|
32835
32838
|
}
|
|
32839
|
+
const baseFetch = input.fetchImpl ?? fetch;
|
|
32840
|
+
const fetchImpl = Object.assign(async (...args) => {
|
|
32841
|
+
try {
|
|
32842
|
+
return await baseFetch(...args);
|
|
32843
|
+
} catch (error) {
|
|
32844
|
+
if (error instanceof TypeError) {
|
|
32845
|
+
throw new Error(`Dispatch at ${configUrl} is unreachable: ${error.message}. If the Dispatch URL changed, restart this agent process so it picks up the new configuration.`);
|
|
32846
|
+
}
|
|
32847
|
+
throw error;
|
|
32848
|
+
}
|
|
32849
|
+
}, { preconnect: baseFetch.preconnect });
|
|
32836
32850
|
if (input.tool === "dispatch_open_asks") {
|
|
32837
32851
|
const sessionId = input.sessionId?.trim();
|
|
32838
32852
|
if (!sessionId)
|
|
32839
32853
|
throw new Error("host session id is required for dispatch_open_asks");
|
|
32840
32854
|
toolSchema(input.tool).parse(input.args);
|
|
32841
|
-
const client = new DispatchClient(configUrl, configToken,
|
|
32855
|
+
const client = new DispatchClient(configUrl, configToken, fetchImpl, input.signal);
|
|
32842
32856
|
const response = await client.openAsks(sessionId);
|
|
32843
32857
|
return { text: formatOpenAsksSummary(response, configUrl), details: { ...response } };
|
|
32844
32858
|
}
|
|
@@ -32847,7 +32861,7 @@ async function executeDispatchTool(input) {
|
|
|
32847
32861
|
const ownerArguments = await resolveOwnerArguments(input.tool, input.args, input.cwd, env, exec);
|
|
32848
32862
|
const args = toolSchema(input.tool).parse(ownerArguments.args);
|
|
32849
32863
|
const actor = toolActor(await resolveOrigin(env, exec, input.cwd), input);
|
|
32850
|
-
const client = new DispatchClient(configUrl, configToken,
|
|
32864
|
+
const client = new DispatchClient(configUrl, configToken, fetchImpl, input.signal);
|
|
32851
32865
|
const owner = ownerArguments.owner?.kind === "issue" ? {
|
|
32852
32866
|
kind: "issue",
|
|
32853
32867
|
issue: await ensureIssue(client, ownerArguments.owner.issue, actor)
|
|
@@ -33034,17 +33048,18 @@ async function executeDispatchTool(input) {
|
|
|
33034
33048
|
}
|
|
33035
33049
|
case "dispatch_message": {
|
|
33036
33050
|
const inReplyTo = messageInReplyTo(args);
|
|
33037
|
-
const
|
|
33051
|
+
const issueKey = issue();
|
|
33052
|
+
const message = await client.message(issueKey, {
|
|
33038
33053
|
body: stringArg(args, "body"),
|
|
33039
33054
|
...inReplyTo === undefined ? {} : { in_reply_to: inReplyTo },
|
|
33040
33055
|
actor
|
|
33041
33056
|
});
|
|
33042
|
-
const messageRef = `dispatch://${
|
|
33057
|
+
const messageRef = `dispatch://${issueKey}/message/${message.id}`;
|
|
33043
33058
|
return {
|
|
33044
33059
|
text: `Posted message ${message.id} (${messageRef})`,
|
|
33045
33060
|
details: {
|
|
33046
|
-
issue:
|
|
33047
|
-
topic: dispatchIssueSubject(
|
|
33061
|
+
issue: issueKey,
|
|
33062
|
+
topic: dispatchIssueSubject(issueKey, ">"),
|
|
33048
33063
|
message: message.id
|
|
33049
33064
|
}
|
|
33050
33065
|
};
|
|
@@ -33678,59 +33693,7 @@ async function copySessionID(copyToClipboard, sessionID) {
|
|
|
33678
33693
|
var codec2 = import_nats.StringCodec();
|
|
33679
33694
|
var NATS_RETRY_INTERVAL_MS = 15000;
|
|
33680
33695
|
var ROLE_CLAIM_ENTRY = "envoy-role-claim";
|
|
33681
|
-
var ASK_AWARENESS_ENTRY = "dispatch-ask-awareness";
|
|
33682
|
-
var LEGION_MANAGED_ENTRY = "legion-managed-session";
|
|
33683
33696
|
var OPEN_ASKS_TIMEOUT_MS = 3000;
|
|
33684
|
-
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.";
|
|
33685
|
-
function isAskAwarenessEntry(entry) {
|
|
33686
|
-
if (typeof entry !== "object" || entry === null)
|
|
33687
|
-
return false;
|
|
33688
|
-
if (!("type" in entry) || entry.type !== "custom")
|
|
33689
|
-
return false;
|
|
33690
|
-
if (!("customType" in entry) || entry.customType !== ASK_AWARENESS_ENTRY)
|
|
33691
|
-
return false;
|
|
33692
|
-
if (!("data" in entry) || typeof entry.data !== "object" || entry.data === null)
|
|
33693
|
-
return false;
|
|
33694
|
-
const data = entry.data;
|
|
33695
|
-
return "session_id" in data && typeof data.session_id === "string" && "period" in data && typeof data.period === "number" && "baseline_as_of" in data && (typeof data.baseline_as_of === "string" || data.baseline_as_of === null) && "checked" in data && typeof data.checked === "boolean" && "fired" in data && typeof data.fired === "boolean" && "saw_ask" in data && typeof data.saw_ask === "boolean";
|
|
33696
|
-
}
|
|
33697
|
-
function isLegionManagedEntry(entry) {
|
|
33698
|
-
if (typeof entry !== "object" || entry === null)
|
|
33699
|
-
return false;
|
|
33700
|
-
if (!("type" in entry) || entry.type !== "custom")
|
|
33701
|
-
return false;
|
|
33702
|
-
if (!("customType" in entry) || entry.customType !== LEGION_MANAGED_ENTRY)
|
|
33703
|
-
return false;
|
|
33704
|
-
if (!("data" in entry) || typeof entry.data !== "object" || entry.data === null)
|
|
33705
|
-
return false;
|
|
33706
|
-
return "session_id" in entry.data && typeof entry.data.session_id === "string";
|
|
33707
|
-
}
|
|
33708
|
-
function emptyAskAwareness(sessionID) {
|
|
33709
|
-
return {
|
|
33710
|
-
session_id: sessionID,
|
|
33711
|
-
period: 0,
|
|
33712
|
-
baseline_as_of: null,
|
|
33713
|
-
checked: false,
|
|
33714
|
-
fired: false,
|
|
33715
|
-
saw_ask: false
|
|
33716
|
-
};
|
|
33717
|
-
}
|
|
33718
|
-
function transcriptAskAwareness(branch, sessionID) {
|
|
33719
|
-
let state;
|
|
33720
|
-
for (const entry of branch) {
|
|
33721
|
-
if (isAskAwarenessEntry(entry) && entry.data.session_id === sessionID)
|
|
33722
|
-
state = entry.data;
|
|
33723
|
-
}
|
|
33724
|
-
return state;
|
|
33725
|
-
}
|
|
33726
|
-
function transcriptMarksLegionSession(branch, sessionID) {
|
|
33727
|
-
return branch.some((entry) => isLegionManagedEntry(entry) && entry.data.session_id === sessionID);
|
|
33728
|
-
}
|
|
33729
|
-
function isHumanTurnInitiator(message) {
|
|
33730
|
-
if (message.attribution !== "user")
|
|
33731
|
-
return false;
|
|
33732
|
-
return message.role === "user" || message.role === "custom" && (message.customType === "skill-prompt" || message.customType === "collab-prompt");
|
|
33733
|
-
}
|
|
33734
33697
|
function isRoleClaimEntry(entry) {
|
|
33735
33698
|
if (typeof entry !== "object" || entry === null)
|
|
33736
33699
|
return false;
|
|
@@ -33750,15 +33713,13 @@ function legionRoleClaimBridge() {
|
|
|
33750
33713
|
let bridge = store[LEGION_ROLE_CLAIM_BRIDGE];
|
|
33751
33714
|
if (bridge === undefined) {
|
|
33752
33715
|
const ready = Promise.withResolvers();
|
|
33753
|
-
bridge = { ready
|
|
33716
|
+
bridge = { ready };
|
|
33754
33717
|
store[LEGION_ROLE_CLAIM_BRIDGE] = bridge;
|
|
33755
33718
|
}
|
|
33756
33719
|
return bridge;
|
|
33757
33720
|
}
|
|
33758
33721
|
async function claimEnvoyRole(sessionID, role, context) {
|
|
33759
33722
|
const bridge = legionRoleClaimBridge();
|
|
33760
|
-
bridge.managedSessions.add(sessionID);
|
|
33761
|
-
bridge.markManagedSession?.(sessionID, context);
|
|
33762
33723
|
const claim = bridge.claim ?? await bridge.ready.promise;
|
|
33763
33724
|
await claim(sessionID, role, context);
|
|
33764
33725
|
}
|
|
@@ -33807,87 +33768,26 @@ function envoyExtension(pi) {
|
|
|
33807
33768
|
let claimedRoleTopic;
|
|
33808
33769
|
let activeSessionContext;
|
|
33809
33770
|
const inbox = [];
|
|
33810
|
-
|
|
33811
|
-
let candidateBaseline;
|
|
33812
|
-
let awarenessGeneration = 0;
|
|
33813
|
-
let availabilityWarningPeriod;
|
|
33814
|
-
let agentRunning = false;
|
|
33815
|
-
let agentWillContinue = false;
|
|
33816
|
-
let legionManagedSessionID = "";
|
|
33817
|
-
let terminalInputUnsubscribe;
|
|
33818
|
-
const persistAskAwareness = (state) => {
|
|
33819
|
-
askAwareness = state;
|
|
33820
|
-
pi.appendEntry(ASK_AWARENESS_ENTRY, state);
|
|
33821
|
-
};
|
|
33771
|
+
const availabilityWarningSessionIDs = new Set;
|
|
33822
33772
|
const warnAskAvailability = (context, error) => {
|
|
33823
|
-
const
|
|
33824
|
-
if (
|
|
33773
|
+
const sessionID = context.sessionManager.getSessionId();
|
|
33774
|
+
if (availabilityWarningSessionIDs.has(sessionID))
|
|
33825
33775
|
return;
|
|
33826
|
-
|
|
33827
|
-
context.ui.notify(`envoy: Dispatch open-ask check unavailable (${messageFor(error)});
|
|
33776
|
+
availabilityWarningSessionIDs.add(sessionID);
|
|
33777
|
+
context.ui.notify(`envoy: Dispatch open-ask check unavailable (${messageFor(error)}); summary unavailable`, "warning");
|
|
33828
33778
|
};
|
|
33829
|
-
const queryOpenAsks = async (requestedSessionID
|
|
33779
|
+
const queryOpenAsks = async (requestedSessionID) => {
|
|
33830
33780
|
const config = activeDispatchConfig();
|
|
33831
33781
|
if (config === null)
|
|
33832
33782
|
return null;
|
|
33833
|
-
const
|
|
33834
|
-
|
|
33835
|
-
return
|
|
33836
|
-
};
|
|
33837
|
-
const setReminderBaseline = (requestedSessionID, period, asOf) => {
|
|
33838
|
-
candidateBaseline = { sessionID: requestedSessionID, asOf };
|
|
33839
|
-
if (askAwareness.session_id !== requestedSessionID || askAwareness.period !== period || askAwareness.checked || askAwareness.fired || askAwareness.baseline_as_of !== null) {
|
|
33840
|
-
return;
|
|
33841
|
-
}
|
|
33842
|
-
persistAskAwareness({ ...askAwareness, baseline_as_of: asOf });
|
|
33843
|
-
};
|
|
33844
|
-
const refreshReminderBaseline = async (requestedSessionID, period, context) => {
|
|
33845
|
-
try {
|
|
33846
|
-
const snapshot = await queryOpenAsks(requestedSessionID);
|
|
33847
|
-
if (snapshot === null)
|
|
33848
|
-
return;
|
|
33849
|
-
setReminderBaseline(requestedSessionID, period, snapshot.as_of);
|
|
33850
|
-
} catch (error) {
|
|
33851
|
-
warnAskAvailability(context, error);
|
|
33852
|
-
}
|
|
33853
|
-
};
|
|
33854
|
-
const armAskReminder = async (context, refreshBaseline = false) => {
|
|
33855
|
-
const id = context.sessionManager.getSessionId();
|
|
33856
|
-
if (id === "" || activeDispatchConfig() === null)
|
|
33857
|
-
return;
|
|
33858
|
-
awarenessGeneration++;
|
|
33859
|
-
availabilityWarningPeriod = undefined;
|
|
33860
|
-
const baseline = !agentRunning && !refreshBaseline && candidateBaseline !== undefined && candidateBaseline.sessionID === id ? candidateBaseline.asOf : null;
|
|
33861
|
-
const previousPeriod = askAwareness.session_id === id ? askAwareness.period : 0;
|
|
33862
|
-
persistAskAwareness({
|
|
33863
|
-
session_id: id,
|
|
33864
|
-
period: previousPeriod + 1,
|
|
33865
|
-
baseline_as_of: baseline,
|
|
33866
|
-
checked: false,
|
|
33867
|
-
fired: false,
|
|
33868
|
-
saw_ask: false
|
|
33869
|
-
});
|
|
33870
|
-
if (agentRunning || refreshBaseline) {
|
|
33871
|
-
await refreshReminderBaseline(id, askAwareness.period, context);
|
|
33872
|
-
}
|
|
33873
|
-
};
|
|
33874
|
-
const cancelPendingAskReminder = () => {
|
|
33875
|
-
awarenessGeneration++;
|
|
33876
|
-
};
|
|
33877
|
-
const installTerminalInputCancellation = (context) => {
|
|
33878
|
-
terminalInputUnsubscribe?.();
|
|
33879
|
-
terminalInputUnsubscribe = context.ui.onTerminalInput(cancelPendingAskReminder);
|
|
33783
|
+
const snapshot = await new DispatchClient(config.url, config.token, fetch, AbortSignal.timeout(OPEN_ASKS_TIMEOUT_MS)).openAsks(requestedSessionID);
|
|
33784
|
+
availabilityWarningSessionIDs.delete(requestedSessionID);
|
|
33785
|
+
return snapshot;
|
|
33880
33786
|
};
|
|
33881
33787
|
const restoreLocalSessionState = (context) => {
|
|
33882
33788
|
sessionDirectory = context.cwd;
|
|
33883
33789
|
sessionID = context.sessionManager.getSessionId();
|
|
33884
33790
|
activeSessionContext = context;
|
|
33885
|
-
candidateBaseline = undefined;
|
|
33886
|
-
const branch = context.sessionManager.getBranch?.() ?? [];
|
|
33887
|
-
askAwareness = transcriptAskAwareness(branch, sessionID) ?? emptyAskAwareness(sessionID);
|
|
33888
|
-
const persistedLegionSession = transcriptMarksLegionSession(branch, sessionID);
|
|
33889
|
-
const managed = persistedLegionSession || legionRoleClaimBridge().managedSessions.has(sessionID);
|
|
33890
|
-
legionManagedSessionID = managed ? sessionID : "";
|
|
33891
33791
|
};
|
|
33892
33792
|
pi.on("resources_discover", async () => ({ skillPaths: [SKILLS_DIRECTORY] }));
|
|
33893
33793
|
registerEnvoyMessageRenderer(pi);
|
|
@@ -33928,13 +33828,6 @@ function envoyExtension(pi) {
|
|
|
33928
33828
|
const dedupeKey = rendered.envelope?.dedupe_key;
|
|
33929
33829
|
const duplicate = dedupeKey !== undefined && dedupeKeys.has(dedupeKey);
|
|
33930
33830
|
if (!duplicate && !rendered.skip) {
|
|
33931
|
-
if (rendered.dispatchActor?.kind === "user" && activeSessionContext !== undefined) {
|
|
33932
|
-
try {
|
|
33933
|
-
await armAskReminder(activeSessionContext, true);
|
|
33934
|
-
} catch (error) {
|
|
33935
|
-
warnAskAvailability(activeSessionContext, error);
|
|
33936
|
-
}
|
|
33937
|
-
}
|
|
33938
33831
|
const envelope = rendered.envelope;
|
|
33939
33832
|
if (envelope !== undefined) {
|
|
33940
33833
|
inbox.unshift({
|
|
@@ -34254,17 +34147,6 @@ function envoyExtension(pi) {
|
|
|
34254
34147
|
return run;
|
|
34255
34148
|
};
|
|
34256
34149
|
const bridge = legionRoleClaimBridge();
|
|
34257
|
-
bridge.markManagedSession = (targetSessionID, context) => {
|
|
34258
|
-
bridge.managedSessions.add(targetSessionID);
|
|
34259
|
-
if (targetSessionID !== sessionID)
|
|
34260
|
-
return;
|
|
34261
|
-
if (legionManagedSessionID === targetSessionID)
|
|
34262
|
-
return;
|
|
34263
|
-
legionManagedSessionID = targetSessionID;
|
|
34264
|
-
if (context !== undefined || activeSessionContext !== undefined) {
|
|
34265
|
-
pi.appendEntry(LEGION_MANAGED_ENTRY, { session_id: targetSessionID });
|
|
34266
|
-
}
|
|
34267
|
-
};
|
|
34268
34150
|
const claim = async (targetSessionID, role, callerContext) => {
|
|
34269
34151
|
const context = callerContext ?? activeSessionContext;
|
|
34270
34152
|
if (context === undefined || context.sessionManager.getSessionId() !== targetSessionID) {
|
|
@@ -34278,7 +34160,6 @@ function envoyExtension(pi) {
|
|
|
34278
34160
|
bridge.claim = claim;
|
|
34279
34161
|
bridge.ready.resolve(claim);
|
|
34280
34162
|
pi.on("session_start", async (_event, context) => {
|
|
34281
|
-
installTerminalInputCancellation(context);
|
|
34282
34163
|
const previousSessionID = sessionID;
|
|
34283
34164
|
restoreLocalSessionState(context);
|
|
34284
34165
|
if (dispatchConfig.error !== null) {
|
|
@@ -34314,7 +34195,6 @@ function envoyExtension(pi) {
|
|
|
34314
34195
|
}
|
|
34315
34196
|
});
|
|
34316
34197
|
const rebind = async (reason, context) => {
|
|
34317
|
-
installTerminalInputCancellation(context);
|
|
34318
34198
|
const previousID = sessionID;
|
|
34319
34199
|
restoreLocalSessionState(context);
|
|
34320
34200
|
if (defaults.natsUrls.length === 0)
|
|
@@ -34364,8 +34244,6 @@ function envoyExtension(pi) {
|
|
|
34364
34244
|
]);
|
|
34365
34245
|
} finally {
|
|
34366
34246
|
clearTimeout(timer);
|
|
34367
|
-
terminalInputUnsubscribe?.();
|
|
34368
|
-
terminalInputUnsubscribe = undefined;
|
|
34369
34247
|
connection = undefined;
|
|
34370
34248
|
activeSessionContext = undefined;
|
|
34371
34249
|
subscriptions.clear();
|
|
@@ -34419,7 +34297,6 @@ function envoyExtension(pi) {
|
|
|
34419
34297
|
const snapshot = await queryOpenAsks(id);
|
|
34420
34298
|
if (snapshot === null)
|
|
34421
34299
|
return;
|
|
34422
|
-
setReminderBaseline(id, askAwareness.session_id === id ? askAwareness.period : 0, snapshot.as_of);
|
|
34423
34300
|
return {
|
|
34424
34301
|
message: {
|
|
34425
34302
|
customType: "dispatch-open-asks",
|
|
@@ -34441,60 +34318,7 @@ ${formatOpenAsksSummary(snapshot, currentDispatchConfig().url)}`,
|
|
|
34441
34318
|
};
|
|
34442
34319
|
}
|
|
34443
34320
|
});
|
|
34444
|
-
pi.on("agent_start", async () => {
|
|
34445
|
-
agentRunning = true;
|
|
34446
|
-
agentWillContinue = false;
|
|
34447
|
-
});
|
|
34448
|
-
pi.on("agent_end", async (event) => {
|
|
34449
|
-
agentRunning = false;
|
|
34450
|
-
agentWillContinue = event.willContinue === true;
|
|
34451
|
-
});
|
|
34452
|
-
pi.on("input", async () => {
|
|
34453
|
-
cancelPendingAskReminder();
|
|
34454
|
-
});
|
|
34455
|
-
pi.on("message_start", async (event, context) => {
|
|
34456
|
-
if (!isHumanTurnInitiator(event.message))
|
|
34457
|
-
return;
|
|
34458
|
-
try {
|
|
34459
|
-
await armAskReminder(context);
|
|
34460
|
-
} catch (error) {
|
|
34461
|
-
warnAskAvailability(context, error);
|
|
34462
|
-
}
|
|
34463
|
-
});
|
|
34464
|
-
pi.on("session_stop", async (event, context) => {
|
|
34465
|
-
if (event.stop_hook_active || event.signal.aborted || shuttingDown || agentWillContinue || event.last_assistant_message?.role !== "assistant" || event.session_id !== sessionID || legionManagedSessionID === event.session_id || askAwareness.session_id !== event.session_id || askAwareness.period === 0 || askAwareness.checked || askAwareness.fired) {
|
|
34466
|
-
return;
|
|
34467
|
-
}
|
|
34468
|
-
const baseline = askAwareness.baseline_as_of;
|
|
34469
|
-
if (baseline === null)
|
|
34470
|
-
return;
|
|
34471
|
-
if (context.ui.getEditorText().trim() !== "")
|
|
34472
|
-
return;
|
|
34473
|
-
const claimed = { ...askAwareness, checked: true };
|
|
34474
|
-
const generation = awarenessGeneration;
|
|
34475
|
-
persistAskAwareness(claimed);
|
|
34476
|
-
let snapshot;
|
|
34477
|
-
try {
|
|
34478
|
-
snapshot = await queryOpenAsks(event.session_id, baseline, event.signal);
|
|
34479
|
-
} catch (error) {
|
|
34480
|
-
if (!event.signal.aborted && generation === awarenessGeneration) {
|
|
34481
|
-
warnAskAvailability(context, error);
|
|
34482
|
-
}
|
|
34483
|
-
return;
|
|
34484
|
-
}
|
|
34485
|
-
if (snapshot === null || event.signal.aborted || generation !== awarenessGeneration || shuttingDown || agentWillContinue || context.sessionManager.getSessionId() !== event.session_id || sessionID !== event.session_id || legionManagedSessionID === event.session_id || context.ui.getEditorText().trim() !== "" || askAwareness.session_id !== claimed.session_id || askAwareness.period !== claimed.period || !askAwareness.checked) {
|
|
34486
|
-
return;
|
|
34487
|
-
}
|
|
34488
|
-
if (snapshot.count > 0 || snapshot.opened_since || askAwareness.saw_ask) {
|
|
34489
|
-
return;
|
|
34490
|
-
}
|
|
34491
|
-
persistAskAwareness({ ...askAwareness, fired: true });
|
|
34492
|
-
return { continue: true, additionalContext: OPEN_ASKS_REMINDER };
|
|
34493
|
-
});
|
|
34494
34321
|
pi.on("tool_result", async (event) => {
|
|
34495
|
-
if (!event.isError && event.toolName === "dispatch_ask" && typeof event.details === "object" && event.details !== null && "ask" in event.details && typeof event.details.ask === "string" && askAwareness.session_id === sessionID && askAwareness.period > 0 && !askAwareness.saw_ask) {
|
|
34496
|
-
persistAskAwareness({ ...askAwareness, saw_ask: true });
|
|
34497
|
-
}
|
|
34498
34322
|
if (event.isError)
|
|
34499
34323
|
return;
|
|
34500
34324
|
const topic = dispatchSubscriptionTopic(event.details);
|
|
@@ -45,7 +45,7 @@ before step 3. The design gate is not a substitute for review and retro.
|
|
|
45
45
|
|
|
46
46
|
1. Re-read the issue, its acceptance criteria, the PR, test evidence, and review evidence.
|
|
47
47
|
Confirm the PR carries both proofs: the implementer's own `E2E (implementer)` line and the tester's `E2E (tester)` line,
|
|
48
|
-
each naming a production-like surface (
|
|
48
|
+
each naming a production-like surface (the daemon's test harness (`packages/daemon/src/daemon/__tests__/`) and real-process fixtures; a live check at the operator's next daemon restart, recorded on the PR; a sandbox repository, a devN
|
|
49
49
|
stack, staging, or a local stack with real migrations), a command or run id, an observation, a head
|
|
50
50
|
SHA, and a negative control. If either is missing, or links only a unit suite, the retro's first
|
|
51
51
|
durable learning is that gap and the issue goes back — to the implementer for its own proof, to the
|
|
@@ -251,7 +251,7 @@ PR opens, and every later phase keeps it current rather than replacing it:
|
|
|
251
251
|
```
|
|
252
252
|
## Verification
|
|
253
253
|
|
|
254
|
-
**CI:** `Tests` run <run-id> — jobs lint,
|
|
254
|
+
**CI:** `Tests` run <run-id> — jobs lint, typecheck, test all success at <head-sha>; `PR Title` run <run-id> — job pr-title success at <head-sha>.
|
|
255
255
|
|
|
256
256
|
**Threads:** <n> resolved, 0 unresolved. Each disposed individually, never in bulk:
|
|
257
257
|
- Thread <id>: fixed in <commit-sha> — <one line>.
|
|
@@ -276,6 +276,7 @@ Verified the implementer's proof by <re-running its command | driving the same s
|
|
|
276
276
|
**Fast-follow:** <one named cleanup item and where it will land>, or "none".
|
|
277
277
|
|
|
278
278
|
**Chain:** stacked on <base bookmark> frozen at <sha> / not stacked.
|
|
279
|
+
**Retarget:** Retargeting a pull request to a new base does not re-run Tests; after a retarget, rebase onto the new base and push — the new head runs Tests against the new merge result — and cite that run in the PR body.
|
|
279
280
|
```
|
|
280
281
|
|
|
281
282
|
- **Threads are dispositioned individually, never resolved in bulk.** Every open review
|
|
@@ -301,11 +302,11 @@ Verified the implementer's proof by <re-running its command | driving the same s
|
|
|
301
302
|
changes behavior, hides an error, or breaks a gate is fixed here — never deferred.
|
|
302
303
|
Findings about naming, duplication, or wording are batched into the single `Fast-follow`
|
|
303
304
|
line instead of iterating per push.
|
|
304
|
-
- **Rebase only on a real conflict.** Sami, 2026-09-11, verbatim:
|
|
305
|
+
- **Rebase only on a real conflict, except after a base retarget.** Sami, 2026-09-11, verbatim:
|
|
305
306
|
"Please don't do unnecessary rebases (i.e. unless there are merge conflicts). The CI queue is too long and slow."
|
|
306
|
-
The implementer rebases the issue branch only when GitHub reports it `CONFLICTING
|
|
307
|
-
|
|
308
|
-
failed CI job is re-run on its own with `legion gh -- run rerun <run-id> --failed`, never by
|
|
307
|
+
The implementer rebases the issue branch only when GitHub reports it `CONFLICTING`, the controller asks
|
|
308
|
+
because of a conflict, or after the pull request is retargeted to a new base. Otherwise, never rebase to
|
|
309
|
+
pick up `main` or refresh CI. A single failed CI job is re-run on its own with `legion gh -- run rerun <run-id> --failed`, never by
|
|
309
310
|
pushing a new commit. A conflict-forced rebase that leaves the branch's diff unchanged is a
|
|
310
311
|
confirmation, not a new round (see *The unchanged-diff check* below). Before rebasing, record
|
|
311
312
|
the fingerprint at the current tip; after pushing the rebased branch, record it at the new
|
|
@@ -317,9 +318,10 @@ Verified the implementer's proof by <re-running its command | driving the same s
|
|
|
317
318
|
field names naming, duplication, or wording cleanup only; anything that changes behaviour,
|
|
318
319
|
hides an error, or breaks a gate lands in this PR.
|
|
319
320
|
- **The implementer proves the change before its phase completes, and writes the `E2E (implementer)` line when the pull request opens.**
|
|
320
|
-
The proof is the changed behaviour exercised on the surface a user reaches it through —
|
|
321
|
-
|
|
322
|
-
|
|
321
|
+
The proof is the changed behaviour exercised on the surface a user reaches it through — the daemon's
|
|
322
|
+
test harness (`packages/daemon/src/daemon/__tests__/`) and real-process fixtures; a live check at the
|
|
323
|
+
operator's next daemon restart, recorded on the PR; a sandbox repository, a real browser, a devN stack,
|
|
324
|
+
or a local stack with real migrations — with the exact command or run id, what was observed, the head SHA,
|
|
323
325
|
one negative control. The same proof goes into `.legion/implement.json` as its required `proof`
|
|
324
326
|
array (`legion handoff write --phase implement` refuses a payload without one and names the
|
|
325
327
|
field), and into the PR body, because the reviewer and the merger verify facts on GitHub and
|