@sjawhar/pi-legion-envoy 1.27.0 → 1.27.2
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 +41 -205
- package/dist/legion.js +41 -205
- package/dist/skills/dispatch/SKILL.md +21 -0
- package/package.json +1 -1
package/dist/envoy.js
CHANGED
|
@@ -29883,7 +29883,7 @@ var dispatchToolSpecs = [
|
|
|
29883
29883
|
issue: z.string().describe(ISSUE_REFERENCE).optional(),
|
|
29884
29884
|
project: z.string().describe("Project key owning the document.").optional(),
|
|
29885
29885
|
artifact: z.string().describe("Project document artifact id, slug, or filename.").optional(),
|
|
29886
|
-
ref: z.string().describe("Optional dispatch:// issue or
|
|
29886
|
+
ref: z.string().describe("Optional dispatch:// reference (issue, document, message, or ask); appended to the question and rendered as a link.").optional(),
|
|
29887
29887
|
question: z.string({ max: 800 }).describe("Decision question, at most 800 characters."),
|
|
29888
29888
|
kind: z.enum(["action"]).describe("Optional human to-do ask kind.").optional(),
|
|
29889
29889
|
options: z.array(z.object({
|
|
@@ -32015,6 +32015,19 @@ function askKind(args) {
|
|
|
32015
32015
|
return value;
|
|
32016
32016
|
throw new Error("kind must be action");
|
|
32017
32017
|
}
|
|
32018
|
+
function askQuestionWithRef(args) {
|
|
32019
|
+
const question = stringArg(args, "question");
|
|
32020
|
+
const ref = optionalString(args, "ref");
|
|
32021
|
+
if (ref === undefined || question.includes(ref))
|
|
32022
|
+
return question;
|
|
32023
|
+
const withRef = `${question}
|
|
32024
|
+
|
|
32025
|
+
Ref: ${ref}`;
|
|
32026
|
+
if (withRef.length > 800) {
|
|
32027
|
+
throw new Error("question plus ref must be at most 800 characters");
|
|
32028
|
+
}
|
|
32029
|
+
return withRef;
|
|
32030
|
+
}
|
|
32018
32031
|
function parseDispatchRef(ref) {
|
|
32019
32032
|
const projectDocument = ref.match(/^dispatch:\/\/([A-Z][A-Z0-9]{1,9})\/artifact\/([^/@]+)(?:@v(\d+))?(?:\/(ask|comment)\/([^/]+))?$/);
|
|
32020
32033
|
if (projectDocument) {
|
|
@@ -32097,6 +32110,9 @@ async function resolveOwnerArguments(tool, args, cwd, env, exec) {
|
|
|
32097
32110
|
if (issueFreeTools[tool] === true)
|
|
32098
32111
|
return { args, ref: null, owner: null };
|
|
32099
32112
|
const refArgument = args.ref;
|
|
32113
|
+
if (tool === "dispatch_ask" && typeof refArgument === "string" && !refArgument.startsWith("dispatch://")) {
|
|
32114
|
+
throw new Error("ref must be a dispatch:// reference");
|
|
32115
|
+
}
|
|
32100
32116
|
const ref = typeof refArgument === "string" ? parseDispatchRef(refArgument) ?? (() => {
|
|
32101
32117
|
throw new Error("ref must be a valid dispatch:// reference such as dispatch://KEY-1, " + "dispatch://KEY-1/ask/<uuid>, dispatch://KEY-1/comment/<uuid>, " + "dispatch://KEY-1/message/<uuid>, dispatch://KEY-1/artifact/<slug>, or " + "dispatch://PROJECT/artifact/<document-ref> (an artifact id, slug, or filename)");
|
|
32102
32118
|
})() : null;
|
|
@@ -32386,12 +32402,23 @@ async function executeDispatchTool(input) {
|
|
|
32386
32402
|
if (!input.config.enabled || !configUrl || !configToken) {
|
|
32387
32403
|
throw new Error("Dispatch is disabled; resolve both DISPATCH_URL and DISPATCH_TOKEN");
|
|
32388
32404
|
}
|
|
32405
|
+
const baseFetch = input.fetchImpl ?? fetch;
|
|
32406
|
+
const fetchImpl = Object.assign(async (...args) => {
|
|
32407
|
+
try {
|
|
32408
|
+
return await baseFetch(...args);
|
|
32409
|
+
} catch (error) {
|
|
32410
|
+
if (error instanceof TypeError) {
|
|
32411
|
+
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.`);
|
|
32412
|
+
}
|
|
32413
|
+
throw error;
|
|
32414
|
+
}
|
|
32415
|
+
}, { preconnect: baseFetch.preconnect });
|
|
32389
32416
|
if (input.tool === "dispatch_open_asks") {
|
|
32390
32417
|
const sessionId = input.sessionId?.trim();
|
|
32391
32418
|
if (!sessionId)
|
|
32392
32419
|
throw new Error("host session id is required for dispatch_open_asks");
|
|
32393
32420
|
toolSchema(input.tool).parse(input.args);
|
|
32394
|
-
const client = new DispatchClient(configUrl, configToken,
|
|
32421
|
+
const client = new DispatchClient(configUrl, configToken, fetchImpl, input.signal);
|
|
32395
32422
|
const response = await client.openAsks(sessionId);
|
|
32396
32423
|
return { text: formatOpenAsksSummary(response, configUrl), details: { ...response } };
|
|
32397
32424
|
}
|
|
@@ -32400,7 +32427,7 @@ async function executeDispatchTool(input) {
|
|
|
32400
32427
|
const ownerArguments = await resolveOwnerArguments(input.tool, input.args, input.cwd, env, exec);
|
|
32401
32428
|
const args = toolSchema(input.tool).parse(ownerArguments.args);
|
|
32402
32429
|
const actor = toolActor(await resolveOrigin(env, exec, input.cwd), input);
|
|
32403
|
-
const client = new DispatchClient(configUrl, configToken,
|
|
32430
|
+
const client = new DispatchClient(configUrl, configToken, fetchImpl, input.signal);
|
|
32404
32431
|
const owner = ownerArguments.owner?.kind === "issue" ? {
|
|
32405
32432
|
kind: "issue",
|
|
32406
32433
|
issue: await ensureIssue(client, ownerArguments.owner.issue, actor)
|
|
@@ -32506,7 +32533,7 @@ async function executeDispatchTool(input) {
|
|
|
32506
32533
|
const kind = askKind(args);
|
|
32507
32534
|
const anchored = anchorArgs && resolved ? anchor(resolved.artifact, anchorArgs) : undefined;
|
|
32508
32535
|
const askInput = {
|
|
32509
|
-
question:
|
|
32536
|
+
question: askQuestionWithRef(args),
|
|
32510
32537
|
...kind === undefined ? {} : { kind },
|
|
32511
32538
|
...Array.isArray(options) ? { options } : {},
|
|
32512
32539
|
...multiple === undefined ? {} : { multiple },
|
|
@@ -33239,59 +33266,7 @@ async function copySessionID(copyToClipboard, sessionID) {
|
|
|
33239
33266
|
var codec2 = import_nats.StringCodec();
|
|
33240
33267
|
var NATS_RETRY_INTERVAL_MS = 15000;
|
|
33241
33268
|
var ROLE_CLAIM_ENTRY = "envoy-role-claim";
|
|
33242
|
-
var ASK_AWARENESS_ENTRY = "dispatch-ask-awareness";
|
|
33243
|
-
var LEGION_MANAGED_ENTRY = "legion-managed-session";
|
|
33244
33269
|
var OPEN_ASKS_TIMEOUT_MS = 3000;
|
|
33245
|
-
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.";
|
|
33246
|
-
function isAskAwarenessEntry(entry) {
|
|
33247
|
-
if (typeof entry !== "object" || entry === null)
|
|
33248
|
-
return false;
|
|
33249
|
-
if (!("type" in entry) || entry.type !== "custom")
|
|
33250
|
-
return false;
|
|
33251
|
-
if (!("customType" in entry) || entry.customType !== ASK_AWARENESS_ENTRY)
|
|
33252
|
-
return false;
|
|
33253
|
-
if (!("data" in entry) || typeof entry.data !== "object" || entry.data === null)
|
|
33254
|
-
return false;
|
|
33255
|
-
const data = entry.data;
|
|
33256
|
-
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";
|
|
33257
|
-
}
|
|
33258
|
-
function isLegionManagedEntry(entry) {
|
|
33259
|
-
if (typeof entry !== "object" || entry === null)
|
|
33260
|
-
return false;
|
|
33261
|
-
if (!("type" in entry) || entry.type !== "custom")
|
|
33262
|
-
return false;
|
|
33263
|
-
if (!("customType" in entry) || entry.customType !== LEGION_MANAGED_ENTRY)
|
|
33264
|
-
return false;
|
|
33265
|
-
if (!("data" in entry) || typeof entry.data !== "object" || entry.data === null)
|
|
33266
|
-
return false;
|
|
33267
|
-
return "session_id" in entry.data && typeof entry.data.session_id === "string";
|
|
33268
|
-
}
|
|
33269
|
-
function emptyAskAwareness(sessionID) {
|
|
33270
|
-
return {
|
|
33271
|
-
session_id: sessionID,
|
|
33272
|
-
period: 0,
|
|
33273
|
-
baseline_as_of: null,
|
|
33274
|
-
checked: false,
|
|
33275
|
-
fired: false,
|
|
33276
|
-
saw_ask: false
|
|
33277
|
-
};
|
|
33278
|
-
}
|
|
33279
|
-
function transcriptAskAwareness(branch, sessionID) {
|
|
33280
|
-
let state;
|
|
33281
|
-
for (const entry of branch) {
|
|
33282
|
-
if (isAskAwarenessEntry(entry) && entry.data.session_id === sessionID)
|
|
33283
|
-
state = entry.data;
|
|
33284
|
-
}
|
|
33285
|
-
return state;
|
|
33286
|
-
}
|
|
33287
|
-
function transcriptMarksLegionSession(branch, sessionID) {
|
|
33288
|
-
return branch.some((entry) => isLegionManagedEntry(entry) && entry.data.session_id === sessionID);
|
|
33289
|
-
}
|
|
33290
|
-
function isHumanTurnInitiator(message) {
|
|
33291
|
-
if (message.attribution !== "user")
|
|
33292
|
-
return false;
|
|
33293
|
-
return message.role === "user" || message.role === "custom" && (message.customType === "skill-prompt" || message.customType === "collab-prompt");
|
|
33294
|
-
}
|
|
33295
33270
|
function isRoleClaimEntry(entry) {
|
|
33296
33271
|
if (typeof entry !== "object" || entry === null)
|
|
33297
33272
|
return false;
|
|
@@ -33311,15 +33286,13 @@ function legionRoleClaimBridge() {
|
|
|
33311
33286
|
let bridge = store[LEGION_ROLE_CLAIM_BRIDGE];
|
|
33312
33287
|
if (bridge === undefined) {
|
|
33313
33288
|
const ready = Promise.withResolvers();
|
|
33314
|
-
bridge = { ready
|
|
33289
|
+
bridge = { ready };
|
|
33315
33290
|
store[LEGION_ROLE_CLAIM_BRIDGE] = bridge;
|
|
33316
33291
|
}
|
|
33317
33292
|
return bridge;
|
|
33318
33293
|
}
|
|
33319
33294
|
async function claimEnvoyRole(sessionID, role, context) {
|
|
33320
33295
|
const bridge = legionRoleClaimBridge();
|
|
33321
|
-
bridge.managedSessions.add(sessionID);
|
|
33322
|
-
bridge.markManagedSession?.(sessionID, context);
|
|
33323
33296
|
const claim = bridge.claim ?? await bridge.ready.promise;
|
|
33324
33297
|
await claim(sessionID, role, context);
|
|
33325
33298
|
}
|
|
@@ -33368,87 +33341,26 @@ function envoyExtension(pi) {
|
|
|
33368
33341
|
let claimedRoleTopic;
|
|
33369
33342
|
let activeSessionContext;
|
|
33370
33343
|
const inbox = [];
|
|
33371
|
-
|
|
33372
|
-
let candidateBaseline;
|
|
33373
|
-
let awarenessGeneration = 0;
|
|
33374
|
-
let availabilityWarningPeriod;
|
|
33375
|
-
let agentRunning = false;
|
|
33376
|
-
let agentWillContinue = false;
|
|
33377
|
-
let legionManagedSessionID = "";
|
|
33378
|
-
let terminalInputUnsubscribe;
|
|
33379
|
-
const persistAskAwareness = (state) => {
|
|
33380
|
-
askAwareness = state;
|
|
33381
|
-
pi.appendEntry(ASK_AWARENESS_ENTRY, state);
|
|
33382
|
-
};
|
|
33344
|
+
const availabilityWarningSessionIDs = new Set;
|
|
33383
33345
|
const warnAskAvailability = (context, error) => {
|
|
33384
|
-
const
|
|
33385
|
-
if (
|
|
33346
|
+
const sessionID = context.sessionManager.getSessionId();
|
|
33347
|
+
if (availabilityWarningSessionIDs.has(sessionID))
|
|
33386
33348
|
return;
|
|
33387
|
-
|
|
33388
|
-
context.ui.notify(`envoy: Dispatch open-ask check unavailable (${messageFor(error)});
|
|
33349
|
+
availabilityWarningSessionIDs.add(sessionID);
|
|
33350
|
+
context.ui.notify(`envoy: Dispatch open-ask check unavailable (${messageFor(error)}); summary unavailable`, "warning");
|
|
33389
33351
|
};
|
|
33390
|
-
const queryOpenAsks = async (requestedSessionID
|
|
33352
|
+
const queryOpenAsks = async (requestedSessionID) => {
|
|
33391
33353
|
const config = activeDispatchConfig();
|
|
33392
33354
|
if (config === null)
|
|
33393
33355
|
return null;
|
|
33394
|
-
const
|
|
33395
|
-
|
|
33396
|
-
return
|
|
33397
|
-
};
|
|
33398
|
-
const setReminderBaseline = (requestedSessionID, period, asOf) => {
|
|
33399
|
-
candidateBaseline = { sessionID: requestedSessionID, asOf };
|
|
33400
|
-
if (askAwareness.session_id !== requestedSessionID || askAwareness.period !== period || askAwareness.checked || askAwareness.fired || askAwareness.baseline_as_of !== null) {
|
|
33401
|
-
return;
|
|
33402
|
-
}
|
|
33403
|
-
persistAskAwareness({ ...askAwareness, baseline_as_of: asOf });
|
|
33404
|
-
};
|
|
33405
|
-
const refreshReminderBaseline = async (requestedSessionID, period, context) => {
|
|
33406
|
-
try {
|
|
33407
|
-
const snapshot = await queryOpenAsks(requestedSessionID);
|
|
33408
|
-
if (snapshot === null)
|
|
33409
|
-
return;
|
|
33410
|
-
setReminderBaseline(requestedSessionID, period, snapshot.as_of);
|
|
33411
|
-
} catch (error) {
|
|
33412
|
-
warnAskAvailability(context, error);
|
|
33413
|
-
}
|
|
33414
|
-
};
|
|
33415
|
-
const armAskReminder = async (context, refreshBaseline = false) => {
|
|
33416
|
-
const id = context.sessionManager.getSessionId();
|
|
33417
|
-
if (id === "" || activeDispatchConfig() === null)
|
|
33418
|
-
return;
|
|
33419
|
-
awarenessGeneration++;
|
|
33420
|
-
availabilityWarningPeriod = undefined;
|
|
33421
|
-
const baseline = !agentRunning && !refreshBaseline && candidateBaseline !== undefined && candidateBaseline.sessionID === id ? candidateBaseline.asOf : null;
|
|
33422
|
-
const previousPeriod = askAwareness.session_id === id ? askAwareness.period : 0;
|
|
33423
|
-
persistAskAwareness({
|
|
33424
|
-
session_id: id,
|
|
33425
|
-
period: previousPeriod + 1,
|
|
33426
|
-
baseline_as_of: baseline,
|
|
33427
|
-
checked: false,
|
|
33428
|
-
fired: false,
|
|
33429
|
-
saw_ask: false
|
|
33430
|
-
});
|
|
33431
|
-
if (agentRunning || refreshBaseline) {
|
|
33432
|
-
await refreshReminderBaseline(id, askAwareness.period, context);
|
|
33433
|
-
}
|
|
33434
|
-
};
|
|
33435
|
-
const cancelPendingAskReminder = () => {
|
|
33436
|
-
awarenessGeneration++;
|
|
33437
|
-
};
|
|
33438
|
-
const installTerminalInputCancellation = (context) => {
|
|
33439
|
-
terminalInputUnsubscribe?.();
|
|
33440
|
-
terminalInputUnsubscribe = context.ui.onTerminalInput(cancelPendingAskReminder);
|
|
33356
|
+
const snapshot = await new DispatchClient(config.url, config.token, fetch, AbortSignal.timeout(OPEN_ASKS_TIMEOUT_MS)).openAsks(requestedSessionID);
|
|
33357
|
+
availabilityWarningSessionIDs.delete(requestedSessionID);
|
|
33358
|
+
return snapshot;
|
|
33441
33359
|
};
|
|
33442
33360
|
const restoreLocalSessionState = (context) => {
|
|
33443
33361
|
sessionDirectory = context.cwd;
|
|
33444
33362
|
sessionID = context.sessionManager.getSessionId();
|
|
33445
33363
|
activeSessionContext = context;
|
|
33446
|
-
candidateBaseline = undefined;
|
|
33447
|
-
const branch = context.sessionManager.getBranch?.() ?? [];
|
|
33448
|
-
askAwareness = transcriptAskAwareness(branch, sessionID) ?? emptyAskAwareness(sessionID);
|
|
33449
|
-
const persistedLegionSession = transcriptMarksLegionSession(branch, sessionID);
|
|
33450
|
-
const managed = persistedLegionSession || legionRoleClaimBridge().managedSessions.has(sessionID);
|
|
33451
|
-
legionManagedSessionID = managed ? sessionID : "";
|
|
33452
33364
|
};
|
|
33453
33365
|
pi.on("resources_discover", async () => ({ skillPaths: [SKILLS_DIRECTORY] }));
|
|
33454
33366
|
registerEnvoyMessageRenderer(pi);
|
|
@@ -33489,13 +33401,6 @@ function envoyExtension(pi) {
|
|
|
33489
33401
|
const dedupeKey = rendered.envelope?.dedupe_key;
|
|
33490
33402
|
const duplicate = dedupeKey !== undefined && dedupeKeys.has(dedupeKey);
|
|
33491
33403
|
if (!duplicate && !rendered.skip) {
|
|
33492
|
-
if (rendered.dispatchActor?.kind === "user" && activeSessionContext !== undefined) {
|
|
33493
|
-
try {
|
|
33494
|
-
await armAskReminder(activeSessionContext, true);
|
|
33495
|
-
} catch (error) {
|
|
33496
|
-
warnAskAvailability(activeSessionContext, error);
|
|
33497
|
-
}
|
|
33498
|
-
}
|
|
33499
33404
|
const envelope = rendered.envelope;
|
|
33500
33405
|
if (envelope !== undefined) {
|
|
33501
33406
|
inbox.unshift({
|
|
@@ -33815,17 +33720,6 @@ function envoyExtension(pi) {
|
|
|
33815
33720
|
return run;
|
|
33816
33721
|
};
|
|
33817
33722
|
const bridge = legionRoleClaimBridge();
|
|
33818
|
-
bridge.markManagedSession = (targetSessionID, context) => {
|
|
33819
|
-
bridge.managedSessions.add(targetSessionID);
|
|
33820
|
-
if (targetSessionID !== sessionID)
|
|
33821
|
-
return;
|
|
33822
|
-
if (legionManagedSessionID === targetSessionID)
|
|
33823
|
-
return;
|
|
33824
|
-
legionManagedSessionID = targetSessionID;
|
|
33825
|
-
if (context !== undefined || activeSessionContext !== undefined) {
|
|
33826
|
-
pi.appendEntry(LEGION_MANAGED_ENTRY, { session_id: targetSessionID });
|
|
33827
|
-
}
|
|
33828
|
-
};
|
|
33829
33723
|
const claim = async (targetSessionID, role, callerContext) => {
|
|
33830
33724
|
const context = callerContext ?? activeSessionContext;
|
|
33831
33725
|
if (context === undefined || context.sessionManager.getSessionId() !== targetSessionID) {
|
|
@@ -33839,7 +33733,6 @@ function envoyExtension(pi) {
|
|
|
33839
33733
|
bridge.claim = claim;
|
|
33840
33734
|
bridge.ready.resolve(claim);
|
|
33841
33735
|
pi.on("session_start", async (_event, context) => {
|
|
33842
|
-
installTerminalInputCancellation(context);
|
|
33843
33736
|
const previousSessionID = sessionID;
|
|
33844
33737
|
restoreLocalSessionState(context);
|
|
33845
33738
|
if (dispatchConfig.error !== null) {
|
|
@@ -33875,7 +33768,6 @@ function envoyExtension(pi) {
|
|
|
33875
33768
|
}
|
|
33876
33769
|
});
|
|
33877
33770
|
const rebind = async (reason, context) => {
|
|
33878
|
-
installTerminalInputCancellation(context);
|
|
33879
33771
|
const previousID = sessionID;
|
|
33880
33772
|
restoreLocalSessionState(context);
|
|
33881
33773
|
if (defaults.natsUrls.length === 0)
|
|
@@ -33925,8 +33817,6 @@ function envoyExtension(pi) {
|
|
|
33925
33817
|
]);
|
|
33926
33818
|
} finally {
|
|
33927
33819
|
clearTimeout(timer);
|
|
33928
|
-
terminalInputUnsubscribe?.();
|
|
33929
|
-
terminalInputUnsubscribe = undefined;
|
|
33930
33820
|
connection = undefined;
|
|
33931
33821
|
activeSessionContext = undefined;
|
|
33932
33822
|
subscriptions.clear();
|
|
@@ -33980,7 +33870,6 @@ function envoyExtension(pi) {
|
|
|
33980
33870
|
const snapshot = await queryOpenAsks(id);
|
|
33981
33871
|
if (snapshot === null)
|
|
33982
33872
|
return;
|
|
33983
|
-
setReminderBaseline(id, askAwareness.session_id === id ? askAwareness.period : 0, snapshot.as_of);
|
|
33984
33873
|
return {
|
|
33985
33874
|
message: {
|
|
33986
33875
|
customType: "dispatch-open-asks",
|
|
@@ -34002,60 +33891,7 @@ ${formatOpenAsksSummary(snapshot, currentDispatchConfig().url)}`,
|
|
|
34002
33891
|
};
|
|
34003
33892
|
}
|
|
34004
33893
|
});
|
|
34005
|
-
pi.on("agent_start", async () => {
|
|
34006
|
-
agentRunning = true;
|
|
34007
|
-
agentWillContinue = false;
|
|
34008
|
-
});
|
|
34009
|
-
pi.on("agent_end", async (event) => {
|
|
34010
|
-
agentRunning = false;
|
|
34011
|
-
agentWillContinue = event.willContinue === true;
|
|
34012
|
-
});
|
|
34013
|
-
pi.on("input", async () => {
|
|
34014
|
-
cancelPendingAskReminder();
|
|
34015
|
-
});
|
|
34016
|
-
pi.on("message_start", async (event, context) => {
|
|
34017
|
-
if (!isHumanTurnInitiator(event.message))
|
|
34018
|
-
return;
|
|
34019
|
-
try {
|
|
34020
|
-
await armAskReminder(context);
|
|
34021
|
-
} catch (error) {
|
|
34022
|
-
warnAskAvailability(context, error);
|
|
34023
|
-
}
|
|
34024
|
-
});
|
|
34025
|
-
pi.on("session_stop", async (event, context) => {
|
|
34026
|
-
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) {
|
|
34027
|
-
return;
|
|
34028
|
-
}
|
|
34029
|
-
const baseline = askAwareness.baseline_as_of;
|
|
34030
|
-
if (baseline === null)
|
|
34031
|
-
return;
|
|
34032
|
-
if (context.ui.getEditorText().trim() !== "")
|
|
34033
|
-
return;
|
|
34034
|
-
const claimed = { ...askAwareness, checked: true };
|
|
34035
|
-
const generation = awarenessGeneration;
|
|
34036
|
-
persistAskAwareness(claimed);
|
|
34037
|
-
let snapshot;
|
|
34038
|
-
try {
|
|
34039
|
-
snapshot = await queryOpenAsks(event.session_id, baseline, event.signal);
|
|
34040
|
-
} catch (error) {
|
|
34041
|
-
if (!event.signal.aborted && generation === awarenessGeneration) {
|
|
34042
|
-
warnAskAvailability(context, error);
|
|
34043
|
-
}
|
|
34044
|
-
return;
|
|
34045
|
-
}
|
|
34046
|
-
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) {
|
|
34047
|
-
return;
|
|
34048
|
-
}
|
|
34049
|
-
if (snapshot.count > 0 || snapshot.opened_since || askAwareness.saw_ask) {
|
|
34050
|
-
return;
|
|
34051
|
-
}
|
|
34052
|
-
persistAskAwareness({ ...askAwareness, fired: true });
|
|
34053
|
-
return { continue: true, additionalContext: OPEN_ASKS_REMINDER };
|
|
34054
|
-
});
|
|
34055
33894
|
pi.on("tool_result", async (event) => {
|
|
34056
|
-
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) {
|
|
34057
|
-
persistAskAwareness({ ...askAwareness, saw_ask: true });
|
|
34058
|
-
}
|
|
34059
33895
|
if (event.isError)
|
|
34060
33896
|
return;
|
|
34061
33897
|
const topic = dispatchSubscriptionTopic(event.details);
|
package/dist/legion.js
CHANGED
|
@@ -29883,7 +29883,7 @@ var dispatchToolSpecs = [
|
|
|
29883
29883
|
issue: z.string().describe(ISSUE_REFERENCE).optional(),
|
|
29884
29884
|
project: z.string().describe("Project key owning the document.").optional(),
|
|
29885
29885
|
artifact: z.string().describe("Project document artifact id, slug, or filename.").optional(),
|
|
29886
|
-
ref: z.string().describe("Optional dispatch:// issue or
|
|
29886
|
+
ref: z.string().describe("Optional dispatch:// reference (issue, document, message, or ask); appended to the question and rendered as a link.").optional(),
|
|
29887
29887
|
question: z.string({ max: 800 }).describe("Decision question, at most 800 characters."),
|
|
29888
29888
|
kind: z.enum(["action"]).describe("Optional human to-do ask kind.").optional(),
|
|
29889
29889
|
options: z.array(z.object({
|
|
@@ -32446,6 +32446,19 @@ function askKind(args) {
|
|
|
32446
32446
|
return value;
|
|
32447
32447
|
throw new Error("kind must be action");
|
|
32448
32448
|
}
|
|
32449
|
+
function askQuestionWithRef(args) {
|
|
32450
|
+
const question = stringArg(args, "question");
|
|
32451
|
+
const ref = optionalString(args, "ref");
|
|
32452
|
+
if (ref === undefined || question.includes(ref))
|
|
32453
|
+
return question;
|
|
32454
|
+
const withRef = `${question}
|
|
32455
|
+
|
|
32456
|
+
Ref: ${ref}`;
|
|
32457
|
+
if (withRef.length > 800) {
|
|
32458
|
+
throw new Error("question plus ref must be at most 800 characters");
|
|
32459
|
+
}
|
|
32460
|
+
return withRef;
|
|
32461
|
+
}
|
|
32449
32462
|
function parseDispatchRef(ref) {
|
|
32450
32463
|
const projectDocument = ref.match(/^dispatch:\/\/([A-Z][A-Z0-9]{1,9})\/artifact\/([^/@]+)(?:@v(\d+))?(?:\/(ask|comment)\/([^/]+))?$/);
|
|
32451
32464
|
if (projectDocument) {
|
|
@@ -32528,6 +32541,9 @@ async function resolveOwnerArguments(tool, args, cwd, env, exec) {
|
|
|
32528
32541
|
if (issueFreeTools[tool] === true)
|
|
32529
32542
|
return { args, ref: null, owner: null };
|
|
32530
32543
|
const refArgument = args.ref;
|
|
32544
|
+
if (tool === "dispatch_ask" && typeof refArgument === "string" && !refArgument.startsWith("dispatch://")) {
|
|
32545
|
+
throw new Error("ref must be a dispatch:// reference");
|
|
32546
|
+
}
|
|
32531
32547
|
const ref = typeof refArgument === "string" ? parseDispatchRef(refArgument) ?? (() => {
|
|
32532
32548
|
throw new Error("ref must be a valid dispatch:// reference such as dispatch://KEY-1, " + "dispatch://KEY-1/ask/<uuid>, dispatch://KEY-1/comment/<uuid>, " + "dispatch://KEY-1/message/<uuid>, dispatch://KEY-1/artifact/<slug>, or " + "dispatch://PROJECT/artifact/<document-ref> (an artifact id, slug, or filename)");
|
|
32533
32549
|
})() : null;
|
|
@@ -32817,12 +32833,23 @@ async function executeDispatchTool(input) {
|
|
|
32817
32833
|
if (!input.config.enabled || !configUrl || !configToken) {
|
|
32818
32834
|
throw new Error("Dispatch is disabled; resolve both DISPATCH_URL and DISPATCH_TOKEN");
|
|
32819
32835
|
}
|
|
32836
|
+
const baseFetch = input.fetchImpl ?? fetch;
|
|
32837
|
+
const fetchImpl = Object.assign(async (...args) => {
|
|
32838
|
+
try {
|
|
32839
|
+
return await baseFetch(...args);
|
|
32840
|
+
} catch (error) {
|
|
32841
|
+
if (error instanceof TypeError) {
|
|
32842
|
+
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.`);
|
|
32843
|
+
}
|
|
32844
|
+
throw error;
|
|
32845
|
+
}
|
|
32846
|
+
}, { preconnect: baseFetch.preconnect });
|
|
32820
32847
|
if (input.tool === "dispatch_open_asks") {
|
|
32821
32848
|
const sessionId = input.sessionId?.trim();
|
|
32822
32849
|
if (!sessionId)
|
|
32823
32850
|
throw new Error("host session id is required for dispatch_open_asks");
|
|
32824
32851
|
toolSchema(input.tool).parse(input.args);
|
|
32825
|
-
const client = new DispatchClient(configUrl, configToken,
|
|
32852
|
+
const client = new DispatchClient(configUrl, configToken, fetchImpl, input.signal);
|
|
32826
32853
|
const response = await client.openAsks(sessionId);
|
|
32827
32854
|
return { text: formatOpenAsksSummary(response, configUrl), details: { ...response } };
|
|
32828
32855
|
}
|
|
@@ -32831,7 +32858,7 @@ async function executeDispatchTool(input) {
|
|
|
32831
32858
|
const ownerArguments = await resolveOwnerArguments(input.tool, input.args, input.cwd, env, exec);
|
|
32832
32859
|
const args = toolSchema(input.tool).parse(ownerArguments.args);
|
|
32833
32860
|
const actor = toolActor(await resolveOrigin(env, exec, input.cwd), input);
|
|
32834
|
-
const client = new DispatchClient(configUrl, configToken,
|
|
32861
|
+
const client = new DispatchClient(configUrl, configToken, fetchImpl, input.signal);
|
|
32835
32862
|
const owner = ownerArguments.owner?.kind === "issue" ? {
|
|
32836
32863
|
kind: "issue",
|
|
32837
32864
|
issue: await ensureIssue(client, ownerArguments.owner.issue, actor)
|
|
@@ -32937,7 +32964,7 @@ async function executeDispatchTool(input) {
|
|
|
32937
32964
|
const kind = askKind(args);
|
|
32938
32965
|
const anchored = anchorArgs && resolved ? anchor(resolved.artifact, anchorArgs) : undefined;
|
|
32939
32966
|
const askInput = {
|
|
32940
|
-
question:
|
|
32967
|
+
question: askQuestionWithRef(args),
|
|
32941
32968
|
...kind === undefined ? {} : { kind },
|
|
32942
32969
|
...Array.isArray(options) ? { options } : {},
|
|
32943
32970
|
...multiple === undefined ? {} : { multiple },
|
|
@@ -33662,59 +33689,7 @@ async function copySessionID(copyToClipboard, sessionID) {
|
|
|
33662
33689
|
var codec2 = import_nats.StringCodec();
|
|
33663
33690
|
var NATS_RETRY_INTERVAL_MS = 15000;
|
|
33664
33691
|
var ROLE_CLAIM_ENTRY = "envoy-role-claim";
|
|
33665
|
-
var ASK_AWARENESS_ENTRY = "dispatch-ask-awareness";
|
|
33666
|
-
var LEGION_MANAGED_ENTRY = "legion-managed-session";
|
|
33667
33692
|
var OPEN_ASKS_TIMEOUT_MS = 3000;
|
|
33668
|
-
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.";
|
|
33669
|
-
function isAskAwarenessEntry(entry) {
|
|
33670
|
-
if (typeof entry !== "object" || entry === null)
|
|
33671
|
-
return false;
|
|
33672
|
-
if (!("type" in entry) || entry.type !== "custom")
|
|
33673
|
-
return false;
|
|
33674
|
-
if (!("customType" in entry) || entry.customType !== ASK_AWARENESS_ENTRY)
|
|
33675
|
-
return false;
|
|
33676
|
-
if (!("data" in entry) || typeof entry.data !== "object" || entry.data === null)
|
|
33677
|
-
return false;
|
|
33678
|
-
const data = entry.data;
|
|
33679
|
-
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";
|
|
33680
|
-
}
|
|
33681
|
-
function isLegionManagedEntry(entry) {
|
|
33682
|
-
if (typeof entry !== "object" || entry === null)
|
|
33683
|
-
return false;
|
|
33684
|
-
if (!("type" in entry) || entry.type !== "custom")
|
|
33685
|
-
return false;
|
|
33686
|
-
if (!("customType" in entry) || entry.customType !== LEGION_MANAGED_ENTRY)
|
|
33687
|
-
return false;
|
|
33688
|
-
if (!("data" in entry) || typeof entry.data !== "object" || entry.data === null)
|
|
33689
|
-
return false;
|
|
33690
|
-
return "session_id" in entry.data && typeof entry.data.session_id === "string";
|
|
33691
|
-
}
|
|
33692
|
-
function emptyAskAwareness(sessionID) {
|
|
33693
|
-
return {
|
|
33694
|
-
session_id: sessionID,
|
|
33695
|
-
period: 0,
|
|
33696
|
-
baseline_as_of: null,
|
|
33697
|
-
checked: false,
|
|
33698
|
-
fired: false,
|
|
33699
|
-
saw_ask: false
|
|
33700
|
-
};
|
|
33701
|
-
}
|
|
33702
|
-
function transcriptAskAwareness(branch, sessionID) {
|
|
33703
|
-
let state;
|
|
33704
|
-
for (const entry of branch) {
|
|
33705
|
-
if (isAskAwarenessEntry(entry) && entry.data.session_id === sessionID)
|
|
33706
|
-
state = entry.data;
|
|
33707
|
-
}
|
|
33708
|
-
return state;
|
|
33709
|
-
}
|
|
33710
|
-
function transcriptMarksLegionSession(branch, sessionID) {
|
|
33711
|
-
return branch.some((entry) => isLegionManagedEntry(entry) && entry.data.session_id === sessionID);
|
|
33712
|
-
}
|
|
33713
|
-
function isHumanTurnInitiator(message) {
|
|
33714
|
-
if (message.attribution !== "user")
|
|
33715
|
-
return false;
|
|
33716
|
-
return message.role === "user" || message.role === "custom" && (message.customType === "skill-prompt" || message.customType === "collab-prompt");
|
|
33717
|
-
}
|
|
33718
33693
|
function isRoleClaimEntry(entry) {
|
|
33719
33694
|
if (typeof entry !== "object" || entry === null)
|
|
33720
33695
|
return false;
|
|
@@ -33734,15 +33709,13 @@ function legionRoleClaimBridge() {
|
|
|
33734
33709
|
let bridge = store[LEGION_ROLE_CLAIM_BRIDGE];
|
|
33735
33710
|
if (bridge === undefined) {
|
|
33736
33711
|
const ready = Promise.withResolvers();
|
|
33737
|
-
bridge = { ready
|
|
33712
|
+
bridge = { ready };
|
|
33738
33713
|
store[LEGION_ROLE_CLAIM_BRIDGE] = bridge;
|
|
33739
33714
|
}
|
|
33740
33715
|
return bridge;
|
|
33741
33716
|
}
|
|
33742
33717
|
async function claimEnvoyRole(sessionID, role, context) {
|
|
33743
33718
|
const bridge = legionRoleClaimBridge();
|
|
33744
|
-
bridge.managedSessions.add(sessionID);
|
|
33745
|
-
bridge.markManagedSession?.(sessionID, context);
|
|
33746
33719
|
const claim = bridge.claim ?? await bridge.ready.promise;
|
|
33747
33720
|
await claim(sessionID, role, context);
|
|
33748
33721
|
}
|
|
@@ -33791,87 +33764,26 @@ function envoyExtension(pi) {
|
|
|
33791
33764
|
let claimedRoleTopic;
|
|
33792
33765
|
let activeSessionContext;
|
|
33793
33766
|
const inbox = [];
|
|
33794
|
-
|
|
33795
|
-
let candidateBaseline;
|
|
33796
|
-
let awarenessGeneration = 0;
|
|
33797
|
-
let availabilityWarningPeriod;
|
|
33798
|
-
let agentRunning = false;
|
|
33799
|
-
let agentWillContinue = false;
|
|
33800
|
-
let legionManagedSessionID = "";
|
|
33801
|
-
let terminalInputUnsubscribe;
|
|
33802
|
-
const persistAskAwareness = (state) => {
|
|
33803
|
-
askAwareness = state;
|
|
33804
|
-
pi.appendEntry(ASK_AWARENESS_ENTRY, state);
|
|
33805
|
-
};
|
|
33767
|
+
const availabilityWarningSessionIDs = new Set;
|
|
33806
33768
|
const warnAskAvailability = (context, error) => {
|
|
33807
|
-
const
|
|
33808
|
-
if (
|
|
33769
|
+
const sessionID = context.sessionManager.getSessionId();
|
|
33770
|
+
if (availabilityWarningSessionIDs.has(sessionID))
|
|
33809
33771
|
return;
|
|
33810
|
-
|
|
33811
|
-
context.ui.notify(`envoy: Dispatch open-ask check unavailable (${messageFor(error)});
|
|
33772
|
+
availabilityWarningSessionIDs.add(sessionID);
|
|
33773
|
+
context.ui.notify(`envoy: Dispatch open-ask check unavailable (${messageFor(error)}); summary unavailable`, "warning");
|
|
33812
33774
|
};
|
|
33813
|
-
const queryOpenAsks = async (requestedSessionID
|
|
33775
|
+
const queryOpenAsks = async (requestedSessionID) => {
|
|
33814
33776
|
const config = activeDispatchConfig();
|
|
33815
33777
|
if (config === null)
|
|
33816
33778
|
return null;
|
|
33817
|
-
const
|
|
33818
|
-
|
|
33819
|
-
return
|
|
33820
|
-
};
|
|
33821
|
-
const setReminderBaseline = (requestedSessionID, period, asOf) => {
|
|
33822
|
-
candidateBaseline = { sessionID: requestedSessionID, asOf };
|
|
33823
|
-
if (askAwareness.session_id !== requestedSessionID || askAwareness.period !== period || askAwareness.checked || askAwareness.fired || askAwareness.baseline_as_of !== null) {
|
|
33824
|
-
return;
|
|
33825
|
-
}
|
|
33826
|
-
persistAskAwareness({ ...askAwareness, baseline_as_of: asOf });
|
|
33827
|
-
};
|
|
33828
|
-
const refreshReminderBaseline = async (requestedSessionID, period, context) => {
|
|
33829
|
-
try {
|
|
33830
|
-
const snapshot = await queryOpenAsks(requestedSessionID);
|
|
33831
|
-
if (snapshot === null)
|
|
33832
|
-
return;
|
|
33833
|
-
setReminderBaseline(requestedSessionID, period, snapshot.as_of);
|
|
33834
|
-
} catch (error) {
|
|
33835
|
-
warnAskAvailability(context, error);
|
|
33836
|
-
}
|
|
33837
|
-
};
|
|
33838
|
-
const armAskReminder = async (context, refreshBaseline = false) => {
|
|
33839
|
-
const id = context.sessionManager.getSessionId();
|
|
33840
|
-
if (id === "" || activeDispatchConfig() === null)
|
|
33841
|
-
return;
|
|
33842
|
-
awarenessGeneration++;
|
|
33843
|
-
availabilityWarningPeriod = undefined;
|
|
33844
|
-
const baseline = !agentRunning && !refreshBaseline && candidateBaseline !== undefined && candidateBaseline.sessionID === id ? candidateBaseline.asOf : null;
|
|
33845
|
-
const previousPeriod = askAwareness.session_id === id ? askAwareness.period : 0;
|
|
33846
|
-
persistAskAwareness({
|
|
33847
|
-
session_id: id,
|
|
33848
|
-
period: previousPeriod + 1,
|
|
33849
|
-
baseline_as_of: baseline,
|
|
33850
|
-
checked: false,
|
|
33851
|
-
fired: false,
|
|
33852
|
-
saw_ask: false
|
|
33853
|
-
});
|
|
33854
|
-
if (agentRunning || refreshBaseline) {
|
|
33855
|
-
await refreshReminderBaseline(id, askAwareness.period, context);
|
|
33856
|
-
}
|
|
33857
|
-
};
|
|
33858
|
-
const cancelPendingAskReminder = () => {
|
|
33859
|
-
awarenessGeneration++;
|
|
33860
|
-
};
|
|
33861
|
-
const installTerminalInputCancellation = (context) => {
|
|
33862
|
-
terminalInputUnsubscribe?.();
|
|
33863
|
-
terminalInputUnsubscribe = context.ui.onTerminalInput(cancelPendingAskReminder);
|
|
33779
|
+
const snapshot = await new DispatchClient(config.url, config.token, fetch, AbortSignal.timeout(OPEN_ASKS_TIMEOUT_MS)).openAsks(requestedSessionID);
|
|
33780
|
+
availabilityWarningSessionIDs.delete(requestedSessionID);
|
|
33781
|
+
return snapshot;
|
|
33864
33782
|
};
|
|
33865
33783
|
const restoreLocalSessionState = (context) => {
|
|
33866
33784
|
sessionDirectory = context.cwd;
|
|
33867
33785
|
sessionID = context.sessionManager.getSessionId();
|
|
33868
33786
|
activeSessionContext = context;
|
|
33869
|
-
candidateBaseline = undefined;
|
|
33870
|
-
const branch = context.sessionManager.getBranch?.() ?? [];
|
|
33871
|
-
askAwareness = transcriptAskAwareness(branch, sessionID) ?? emptyAskAwareness(sessionID);
|
|
33872
|
-
const persistedLegionSession = transcriptMarksLegionSession(branch, sessionID);
|
|
33873
|
-
const managed = persistedLegionSession || legionRoleClaimBridge().managedSessions.has(sessionID);
|
|
33874
|
-
legionManagedSessionID = managed ? sessionID : "";
|
|
33875
33787
|
};
|
|
33876
33788
|
pi.on("resources_discover", async () => ({ skillPaths: [SKILLS_DIRECTORY] }));
|
|
33877
33789
|
registerEnvoyMessageRenderer(pi);
|
|
@@ -33912,13 +33824,6 @@ function envoyExtension(pi) {
|
|
|
33912
33824
|
const dedupeKey = rendered.envelope?.dedupe_key;
|
|
33913
33825
|
const duplicate = dedupeKey !== undefined && dedupeKeys.has(dedupeKey);
|
|
33914
33826
|
if (!duplicate && !rendered.skip) {
|
|
33915
|
-
if (rendered.dispatchActor?.kind === "user" && activeSessionContext !== undefined) {
|
|
33916
|
-
try {
|
|
33917
|
-
await armAskReminder(activeSessionContext, true);
|
|
33918
|
-
} catch (error) {
|
|
33919
|
-
warnAskAvailability(activeSessionContext, error);
|
|
33920
|
-
}
|
|
33921
|
-
}
|
|
33922
33827
|
const envelope = rendered.envelope;
|
|
33923
33828
|
if (envelope !== undefined) {
|
|
33924
33829
|
inbox.unshift({
|
|
@@ -34238,17 +34143,6 @@ function envoyExtension(pi) {
|
|
|
34238
34143
|
return run;
|
|
34239
34144
|
};
|
|
34240
34145
|
const bridge = legionRoleClaimBridge();
|
|
34241
|
-
bridge.markManagedSession = (targetSessionID, context) => {
|
|
34242
|
-
bridge.managedSessions.add(targetSessionID);
|
|
34243
|
-
if (targetSessionID !== sessionID)
|
|
34244
|
-
return;
|
|
34245
|
-
if (legionManagedSessionID === targetSessionID)
|
|
34246
|
-
return;
|
|
34247
|
-
legionManagedSessionID = targetSessionID;
|
|
34248
|
-
if (context !== undefined || activeSessionContext !== undefined) {
|
|
34249
|
-
pi.appendEntry(LEGION_MANAGED_ENTRY, { session_id: targetSessionID });
|
|
34250
|
-
}
|
|
34251
|
-
};
|
|
34252
34146
|
const claim = async (targetSessionID, role, callerContext) => {
|
|
34253
34147
|
const context = callerContext ?? activeSessionContext;
|
|
34254
34148
|
if (context === undefined || context.sessionManager.getSessionId() !== targetSessionID) {
|
|
@@ -34262,7 +34156,6 @@ function envoyExtension(pi) {
|
|
|
34262
34156
|
bridge.claim = claim;
|
|
34263
34157
|
bridge.ready.resolve(claim);
|
|
34264
34158
|
pi.on("session_start", async (_event, context) => {
|
|
34265
|
-
installTerminalInputCancellation(context);
|
|
34266
34159
|
const previousSessionID = sessionID;
|
|
34267
34160
|
restoreLocalSessionState(context);
|
|
34268
34161
|
if (dispatchConfig.error !== null) {
|
|
@@ -34298,7 +34191,6 @@ function envoyExtension(pi) {
|
|
|
34298
34191
|
}
|
|
34299
34192
|
});
|
|
34300
34193
|
const rebind = async (reason, context) => {
|
|
34301
|
-
installTerminalInputCancellation(context);
|
|
34302
34194
|
const previousID = sessionID;
|
|
34303
34195
|
restoreLocalSessionState(context);
|
|
34304
34196
|
if (defaults.natsUrls.length === 0)
|
|
@@ -34348,8 +34240,6 @@ function envoyExtension(pi) {
|
|
|
34348
34240
|
]);
|
|
34349
34241
|
} finally {
|
|
34350
34242
|
clearTimeout(timer);
|
|
34351
|
-
terminalInputUnsubscribe?.();
|
|
34352
|
-
terminalInputUnsubscribe = undefined;
|
|
34353
34243
|
connection = undefined;
|
|
34354
34244
|
activeSessionContext = undefined;
|
|
34355
34245
|
subscriptions.clear();
|
|
@@ -34403,7 +34293,6 @@ function envoyExtension(pi) {
|
|
|
34403
34293
|
const snapshot = await queryOpenAsks(id);
|
|
34404
34294
|
if (snapshot === null)
|
|
34405
34295
|
return;
|
|
34406
|
-
setReminderBaseline(id, askAwareness.session_id === id ? askAwareness.period : 0, snapshot.as_of);
|
|
34407
34296
|
return {
|
|
34408
34297
|
message: {
|
|
34409
34298
|
customType: "dispatch-open-asks",
|
|
@@ -34425,60 +34314,7 @@ ${formatOpenAsksSummary(snapshot, currentDispatchConfig().url)}`,
|
|
|
34425
34314
|
};
|
|
34426
34315
|
}
|
|
34427
34316
|
});
|
|
34428
|
-
pi.on("agent_start", async () => {
|
|
34429
|
-
agentRunning = true;
|
|
34430
|
-
agentWillContinue = false;
|
|
34431
|
-
});
|
|
34432
|
-
pi.on("agent_end", async (event) => {
|
|
34433
|
-
agentRunning = false;
|
|
34434
|
-
agentWillContinue = event.willContinue === true;
|
|
34435
|
-
});
|
|
34436
|
-
pi.on("input", async () => {
|
|
34437
|
-
cancelPendingAskReminder();
|
|
34438
|
-
});
|
|
34439
|
-
pi.on("message_start", async (event, context) => {
|
|
34440
|
-
if (!isHumanTurnInitiator(event.message))
|
|
34441
|
-
return;
|
|
34442
|
-
try {
|
|
34443
|
-
await armAskReminder(context);
|
|
34444
|
-
} catch (error) {
|
|
34445
|
-
warnAskAvailability(context, error);
|
|
34446
|
-
}
|
|
34447
|
-
});
|
|
34448
|
-
pi.on("session_stop", async (event, context) => {
|
|
34449
|
-
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) {
|
|
34450
|
-
return;
|
|
34451
|
-
}
|
|
34452
|
-
const baseline = askAwareness.baseline_as_of;
|
|
34453
|
-
if (baseline === null)
|
|
34454
|
-
return;
|
|
34455
|
-
if (context.ui.getEditorText().trim() !== "")
|
|
34456
|
-
return;
|
|
34457
|
-
const claimed = { ...askAwareness, checked: true };
|
|
34458
|
-
const generation = awarenessGeneration;
|
|
34459
|
-
persistAskAwareness(claimed);
|
|
34460
|
-
let snapshot;
|
|
34461
|
-
try {
|
|
34462
|
-
snapshot = await queryOpenAsks(event.session_id, baseline, event.signal);
|
|
34463
|
-
} catch (error) {
|
|
34464
|
-
if (!event.signal.aborted && generation === awarenessGeneration) {
|
|
34465
|
-
warnAskAvailability(context, error);
|
|
34466
|
-
}
|
|
34467
|
-
return;
|
|
34468
|
-
}
|
|
34469
|
-
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) {
|
|
34470
|
-
return;
|
|
34471
|
-
}
|
|
34472
|
-
if (snapshot.count > 0 || snapshot.opened_since || askAwareness.saw_ask) {
|
|
34473
|
-
return;
|
|
34474
|
-
}
|
|
34475
|
-
persistAskAwareness({ ...askAwareness, fired: true });
|
|
34476
|
-
return { continue: true, additionalContext: OPEN_ASKS_REMINDER };
|
|
34477
|
-
});
|
|
34478
34317
|
pi.on("tool_result", async (event) => {
|
|
34479
|
-
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) {
|
|
34480
|
-
persistAskAwareness({ ...askAwareness, saw_ask: true });
|
|
34481
|
-
}
|
|
34482
34318
|
if (event.isError)
|
|
34483
34319
|
return;
|
|
34484
34320
|
const topic = dispatchSubscriptionTopic(event.details);
|
|
@@ -111,6 +111,7 @@ dispatch_ask({
|
|
|
111
111
|
issue?,
|
|
112
112
|
project?,
|
|
113
113
|
artifact?,
|
|
114
|
+
ref?,
|
|
114
115
|
question,
|
|
115
116
|
options?: { label, description? }[],
|
|
116
117
|
multiple?,
|
|
@@ -120,6 +121,8 @@ dispatch_ask({
|
|
|
120
121
|
```
|
|
121
122
|
It returns `details` `{ issue, topic, ask }` for an issue or `{ project, artifact, document, topic, ask }` for a project document.
|
|
122
123
|
|
|
124
|
+
References belong in the question text; `ref` is sugar that appends its `dispatch://` value to the question as a rendered link.
|
|
125
|
+
|
|
123
126
|
An ask is read on a phone by someone who has not read the code. Open with one or two plain
|
|
124
127
|
sentences: what needs deciding and why it matters now. Each option is a button with a label and
|
|
125
128
|
one sentence saying what happens if it is chosen; never enumerate choices in prose. Put the
|
|
@@ -341,6 +344,24 @@ with your text. Never do that: the spec is edited in place with `dispatch_doc_ed
|
|
|
341
344
|
artifact by the slug shown in the upload result or by its filename, and a project document by its artifact id, slug, or filename; the
|
|
342
345
|
slug also arrives on `artifact.created` events.
|
|
343
346
|
|
|
347
|
+
## Structure over stream
|
|
348
|
+
|
|
349
|
+
Dispatch is a structured workspace, never a message stream (Sami, 2026-09-13, verbatim: "strange
|
|
350
|
+
to me that agents keep trying to use dispatch as a giant stream of messages instead of
|
|
351
|
+
high-signal, structured conversation"). The structure IS the product:
|
|
352
|
+
|
|
353
|
+
- **One issue per piece of work.** A new deliverable — an email to send, a document to review, a
|
|
354
|
+
decision with its own lifecycle — gets its own issue with the content as the issue's document
|
|
355
|
+
(spec artifact), not a message pile on an existing issue. If you are about to post a message
|
|
356
|
+
carrying a draft, a spec, or anything over a couple of paragraphs, stop: that is an issue with a
|
|
357
|
+
document, or an artifact on the issue it belongs to.
|
|
358
|
+
- **Content lives in documents; decisions live in asks; messages only announce.** A draft the
|
|
359
|
+
human must read goes in a document artifact the dashboard renders with versions and margins; the
|
|
360
|
+
ask that needs their word references it (`ref`, or the `dispatch://` link inline) instead of
|
|
361
|
+
restating it. A message never carries a body a human has to scroll.
|
|
362
|
+
- **Never split one deliverable across a message + an ask that points at it.** Ask the question
|
|
363
|
+
with the document reference in the question text; the reader lands on the content in one click.
|
|
364
|
+
|
|
344
365
|
## Messages
|
|
345
366
|
|
|
346
367
|
Dispatch is a high-signal record for humans, not a log of what you are doing. A message is a reply to a human's message, or a
|