@sjawhar/pi-legion-envoy 0.16.0 → 0.17.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 +193 -164
- package/dist/legion.js +193 -164
- package/dist/skills/envoy/SKILL.md +15 -0
- package/package.json +1 -1
package/dist/envoy.js
CHANGED
|
@@ -30785,147 +30785,37 @@ var ChildStatusPayloadSchema = object({
|
|
|
30785
30785
|
from: string2().optional(),
|
|
30786
30786
|
to: string2().optional()
|
|
30787
30787
|
});
|
|
30788
|
-
|
|
30789
|
-
|
|
30790
|
-
|
|
30791
|
-
|
|
30792
|
-
|
|
30793
|
-
|
|
30794
|
-
|
|
30795
|
-
|
|
30796
|
-
|
|
30797
|
-
|
|
30798
|
-
|
|
30799
|
-
|
|
30800
|
-
|
|
30801
|
-
|
|
30802
|
-
|
|
30803
|
-
|
|
30804
|
-
|
|
30805
|
-
|
|
30806
|
-
|
|
30807
|
-
|
|
30808
|
-
|
|
30809
|
-
|
|
30810
|
-
|
|
30811
|
-
|
|
30812
|
-
lines.push(`Artifact: ${name}`);
|
|
30813
|
-
break;
|
|
30814
|
-
}
|
|
30815
|
-
case "artifact.version": {
|
|
30816
|
-
const parsed = ArtifactVersionPayloadSchema.safeParse(event.payload);
|
|
30817
|
-
if (!parsed.success)
|
|
30818
|
-
break;
|
|
30819
|
-
const version = parsed.data.version;
|
|
30820
|
-
if (parsed.data.name)
|
|
30821
|
-
lines.push(`Artifact: ${parsed.data.name}`);
|
|
30822
|
-
if (version?.number !== undefined)
|
|
30823
|
-
lines.push(`Version: ${version.number}`);
|
|
30824
|
-
if (version?.summary)
|
|
30825
|
-
lines.push(`Summary: ${version.summary}`);
|
|
30826
|
-
if (parsed.data.diff)
|
|
30827
|
-
lines.push("Diff:", parsed.data.diff);
|
|
30828
|
-
break;
|
|
30829
|
-
}
|
|
30830
|
-
case "ask.opened":
|
|
30831
|
-
case "ask.answered": {
|
|
30832
|
-
const parsed = AskPayloadSchema.safeParse(event.payload);
|
|
30833
|
-
if (!parsed.success)
|
|
30834
|
-
break;
|
|
30835
|
-
if (parsed.data.question)
|
|
30836
|
-
lines.push(`Question: ${parsed.data.question}`);
|
|
30837
|
-
if (event.type === "ask.opened") {
|
|
30838
|
-
const options = (parsed.data.options ?? []).map((option) => option.label).filter((label) => label !== undefined);
|
|
30839
|
-
if (options.length > 0)
|
|
30840
|
-
lines.push(`Options: ${options.join(", ")}`);
|
|
30841
|
-
break;
|
|
30842
|
-
}
|
|
30843
|
-
const answer = parsed.data.answer;
|
|
30844
|
-
lines.push(`Selected: ${answer?.selected?.join(", ") || "none"}`);
|
|
30845
|
-
if (answer?.text)
|
|
30846
|
-
lines.push(`Text: ${answer.text}`);
|
|
30847
|
-
break;
|
|
30848
|
-
}
|
|
30849
|
-
case "comment.created":
|
|
30850
|
-
case "comment.resolved": {
|
|
30851
|
-
const parsed = CommentPayloadSchema.safeParse(event.payload);
|
|
30852
|
-
if (!parsed.success)
|
|
30853
|
-
break;
|
|
30854
|
-
const quote = parsed.data.anchor?.quote;
|
|
30855
|
-
if (parsed.data.artifact_name)
|
|
30856
|
-
lines.push(`Artifact: ${parsed.data.artifact_name}`);
|
|
30857
|
-
if (quote)
|
|
30858
|
-
lines.push(`> ${quote}`);
|
|
30859
|
-
if (parsed.data.reply_to)
|
|
30860
|
-
lines.push(`Reply chain: ${parsed.data.reply_to}`);
|
|
30861
|
-
if (parsed.data.body)
|
|
30862
|
-
lines.push(`Body: ${parsed.data.body}`);
|
|
30863
|
-
break;
|
|
30864
|
-
}
|
|
30865
|
-
case "suggestion.accepted":
|
|
30866
|
-
case "suggestion.rejected": {
|
|
30867
|
-
const parsed = CommentPayloadSchema.safeParse(event.payload);
|
|
30868
|
-
if (!parsed.success)
|
|
30869
|
-
break;
|
|
30870
|
-
const quote = parsed.data.anchor?.quote;
|
|
30871
|
-
const replacement = parsed.data.suggestion?.replace_with;
|
|
30872
|
-
if (parsed.data.artifact_name)
|
|
30873
|
-
lines.push(`Artifact: ${parsed.data.artifact_name}`);
|
|
30874
|
-
if (quote)
|
|
30875
|
-
lines.push(`> ${quote}`);
|
|
30876
|
-
if (quote && replacement)
|
|
30877
|
-
lines.push(`${quote} \u2192 ${replacement}`);
|
|
30878
|
-
break;
|
|
30879
|
-
}
|
|
30880
|
-
case "message.created": {
|
|
30881
|
-
const parsed = MessagePayloadSchema.safeParse(event.payload);
|
|
30882
|
-
const body = parsed.success ? parsed.data.body : undefined;
|
|
30883
|
-
if (body)
|
|
30884
|
-
lines.push(`Body: ${body}`);
|
|
30885
|
-
break;
|
|
30886
|
-
}
|
|
30887
|
-
case "child.status": {
|
|
30888
|
-
const parsed = ChildStatusPayloadSchema.safeParse(event.payload);
|
|
30889
|
-
if (!parsed.success)
|
|
30890
|
-
break;
|
|
30891
|
-
const { child_key: child, from, to } = parsed.data;
|
|
30892
|
-
if (child && from && to)
|
|
30893
|
-
lines.push(`Child: ${child} ${from} \u2192 ${to}`);
|
|
30894
|
-
break;
|
|
30895
|
-
}
|
|
30896
|
-
}
|
|
30897
|
-
return lines;
|
|
30898
|
-
}
|
|
30899
|
-
function renderDispatch(envelope, sessionID) {
|
|
30900
|
-
const invalid = () => ({
|
|
30901
|
-
skip: false,
|
|
30902
|
-
content: "dispatch: invalid event",
|
|
30903
|
-
envelope
|
|
30904
|
-
});
|
|
30905
|
-
if (envelope.payload === undefined)
|
|
30906
|
-
return invalid();
|
|
30907
|
-
let rawEvent;
|
|
30788
|
+
var DISPATCH_PAYLOAD_SCHEMAS = {
|
|
30789
|
+
"issue.created": IssuePayloadSchema,
|
|
30790
|
+
"issue.updated": IssuePayloadSchema,
|
|
30791
|
+
"issue.closed": IssuePayloadSchema,
|
|
30792
|
+
"artifact.created": ArtifactCreatedPayloadSchema,
|
|
30793
|
+
"artifact.version": ArtifactVersionPayloadSchema,
|
|
30794
|
+
"ask.opened": AskPayloadSchema,
|
|
30795
|
+
"ask.answered": AskPayloadSchema,
|
|
30796
|
+
"comment.created": CommentPayloadSchema,
|
|
30797
|
+
"comment.resolved": CommentPayloadSchema,
|
|
30798
|
+
"suggestion.accepted": CommentPayloadSchema,
|
|
30799
|
+
"suggestion.rejected": CommentPayloadSchema,
|
|
30800
|
+
"message.created": MessagePayloadSchema,
|
|
30801
|
+
"child.status": ChildStatusPayloadSchema
|
|
30802
|
+
};
|
|
30803
|
+
function dispatchPayload(event) {
|
|
30804
|
+
const schema = DISPATCH_PAYLOAD_SCHEMAS[event.type];
|
|
30805
|
+
if (schema === undefined)
|
|
30806
|
+
return event.payload;
|
|
30807
|
+
const parsed = schema.safeParse(event.payload);
|
|
30808
|
+
return parsed.success ? parsed.data : event.payload;
|
|
30809
|
+
}
|
|
30810
|
+
function parseDispatchFrame(rawPayload) {
|
|
30811
|
+
let value;
|
|
30908
30812
|
try {
|
|
30909
|
-
|
|
30813
|
+
value = JSON.parse(rawPayload);
|
|
30910
30814
|
} catch {
|
|
30911
|
-
return
|
|
30912
|
-
}
|
|
30913
|
-
const parsed = DispatchEventSchema.safeParse(rawEvent);
|
|
30914
|
-
if (!parsed.success)
|
|
30915
|
-
return invalid();
|
|
30916
|
-
const event = parsed.data;
|
|
30917
|
-
if (event.actor.kind === "session" && event.actor.id === sessionID) {
|
|
30918
|
-
return { skip: true, content: "", envelope };
|
|
30815
|
+
return { raw: rawPayload };
|
|
30919
30816
|
}
|
|
30920
|
-
|
|
30921
|
-
|
|
30922
|
-
content: [
|
|
30923
|
-
`dispatch ${event.issue_key} \xB7 ${event.type} \xB7 by ${dispatchActorLabel(event.actor)}`,
|
|
30924
|
-
...dispatchBody(event)
|
|
30925
|
-
].join(`
|
|
30926
|
-
`),
|
|
30927
|
-
envelope
|
|
30928
|
-
};
|
|
30817
|
+
const parsed = DispatchEventSchema.safeParse(value);
|
|
30818
|
+
return parsed.success ? { event: parsed.data } : { raw: value };
|
|
30929
30819
|
}
|
|
30930
30820
|
function inboundTimestamp(milliseconds) {
|
|
30931
30821
|
if (milliseconds === undefined)
|
|
@@ -30961,13 +30851,31 @@ function renderInbound(raw, sessionID, subject) {
|
|
|
30961
30851
|
const data = tolerant.data;
|
|
30962
30852
|
envelope = { ...data, source: data.source ?? "unknown" };
|
|
30963
30853
|
}
|
|
30854
|
+
let dispatchEvent;
|
|
30855
|
+
let dispatchIssue;
|
|
30856
|
+
const dispatchRendered = envelope.source === "dispatch" && envelope.payload !== undefined;
|
|
30964
30857
|
if (envelope.source === "dispatch") {
|
|
30965
|
-
|
|
30966
|
-
|
|
30967
|
-
|
|
30858
|
+
if (envelope.payload === undefined) {
|
|
30859
|
+
dispatchIssue = "payload";
|
|
30860
|
+
} else {
|
|
30861
|
+
const frame = parseDispatchFrame(envelope.payload);
|
|
30862
|
+
if ("event" in frame) {
|
|
30863
|
+
if (frame.event.actor.kind === "session" && frame.event.actor.id === sessionID) {
|
|
30864
|
+
return { skip: true, content: "", envelope };
|
|
30865
|
+
}
|
|
30866
|
+
dispatchEvent = {
|
|
30867
|
+
issue_key: frame.event.issue_key,
|
|
30868
|
+
type: frame.event.type,
|
|
30869
|
+
actor: frame.event.actor,
|
|
30870
|
+
payload: dispatchPayload(frame.event)
|
|
30871
|
+
};
|
|
30872
|
+
} else {
|
|
30873
|
+
dispatchEvent = frame.raw;
|
|
30874
|
+
}
|
|
30875
|
+
}
|
|
30968
30876
|
}
|
|
30969
30877
|
let parsedPayload;
|
|
30970
|
-
if (envelope.payload !== undefined && (envelope.source === "github" || envelope.payload !== envelope.payload_summary)) {
|
|
30878
|
+
if (!dispatchRendered && envelope.payload !== undefined && (envelope.source === "github" || envelope.payload !== envelope.payload_summary)) {
|
|
30971
30879
|
try {
|
|
30972
30880
|
parsedPayload = JSON.parse(envelope.payload);
|
|
30973
30881
|
} catch {
|
|
@@ -30975,7 +30883,7 @@ function renderInbound(raw, sessionID, subject) {
|
|
|
30975
30883
|
}
|
|
30976
30884
|
}
|
|
30977
30885
|
const payloadSummary = envelope.payload_summary ?? "unknown";
|
|
30978
|
-
const message = envelope.payload !== undefined && envelope.payload !== envelope.payload_summary ? parsedPayload : undefined;
|
|
30886
|
+
const message = !dispatchRendered && envelope.payload !== undefined && envelope.payload !== envelope.payload_summary ? parsedPayload : undefined;
|
|
30979
30887
|
const summaryIsHead = typeof message === "string" && envelope.payload_summary !== undefined && message.startsWith(envelope.payload_summary.endsWith("\u2026") ? envelope.payload_summary.slice(0, -1) : envelope.payload_summary);
|
|
30980
30888
|
const body = `${envelope.payload_summary ?? ""}
|
|
30981
30889
|
${envelope.payload ?? ""}`;
|
|
@@ -30989,7 +30897,11 @@ ${envelope.payload ?? ""}`;
|
|
|
30989
30897
|
const role = envelope.sender?.roles?.[0];
|
|
30990
30898
|
const reply = replyWith(envelope);
|
|
30991
30899
|
const sourceIssue = parseIssues.includes("source") || KNOWN_SOURCES[envelope.source] !== undefined ? undefined : `source=${envelope.source}`;
|
|
30992
|
-
const unrecognised = [
|
|
30900
|
+
const unrecognised = [
|
|
30901
|
+
...parseIssues,
|
|
30902
|
+
...sourceIssue === undefined ? [] : [sourceIssue],
|
|
30903
|
+
...dispatchIssue === undefined ? [] : [dispatchIssue]
|
|
30904
|
+
].join(", ") || undefined;
|
|
30993
30905
|
const rendered = {
|
|
30994
30906
|
...envelope.topic === agentSubject(sessionID) ? { to: `you (${sessionID.slice(0, 4)}\u2026)` } : {},
|
|
30995
30907
|
from: senderLabel(envelope),
|
|
@@ -31004,8 +30916,10 @@ ${envelope.payload ?? ""}`;
|
|
|
31004
30916
|
...role === undefined ? {} : {
|
|
31005
30917
|
reply_role: `envoy_publish(topic="notifications.role.${role}", message="...")`
|
|
31006
30918
|
},
|
|
31007
|
-
...
|
|
31008
|
-
|
|
30919
|
+
...dispatchRendered ? { dispatch: dispatchEvent } : {
|
|
30920
|
+
...summaryIsHead ? {} : { summary: payloadSummary },
|
|
30921
|
+
...message === undefined ? {} : { message }
|
|
30922
|
+
},
|
|
31009
30923
|
...foreignSession === undefined ? {} : {
|
|
31010
30924
|
note: `body names session ${foreignSession}; the sender is ${envelope.source_session ?? "unknown"}`
|
|
31011
30925
|
},
|
|
@@ -31935,6 +31849,11 @@ var RoleWireSchema = object({
|
|
|
31935
31849
|
holder: string2(),
|
|
31936
31850
|
last_seen: number2().int()
|
|
31937
31851
|
});
|
|
31852
|
+
var RoleHeldWireSchema = object({
|
|
31853
|
+
error: string2(),
|
|
31854
|
+
role: string2(),
|
|
31855
|
+
holder: string2()
|
|
31856
|
+
});
|
|
31938
31857
|
var ErrorWireSchema = object({
|
|
31939
31858
|
error: string2(),
|
|
31940
31859
|
expected: array(string2()).optional()
|
|
@@ -32067,7 +31986,28 @@ function createEnvoyClient(config) {
|
|
|
32067
31986
|
unregisterSession: async (sessionID) => {
|
|
32068
31987
|
await request(`/v1/sessions/${encodeURIComponent(sessionID)}`, { method: "DELETE" });
|
|
32069
31988
|
},
|
|
32070
|
-
setRole: async (input) =>
|
|
31989
|
+
setRole: async (input) => {
|
|
31990
|
+
const soft = input.soft === true;
|
|
31991
|
+
const previous = soft ? input.previousSessionID ?? "" : "";
|
|
31992
|
+
const body = {
|
|
31993
|
+
session_id: input.sessionID,
|
|
31994
|
+
role: input.role,
|
|
31995
|
+
...soft ? { soft: true } : {},
|
|
31996
|
+
...previous === "" ? {} : { previous_session_id: previous }
|
|
31997
|
+
};
|
|
31998
|
+
let raw;
|
|
31999
|
+
try {
|
|
32000
|
+
raw = await post("/v1/roles/set", body);
|
|
32001
|
+
} catch (error) {
|
|
32002
|
+
if (error instanceof EnvoyApiError && error.details.status === 409) {
|
|
32003
|
+
const held = RoleHeldWireSchema.safeParse(JSON.parse(error.details.responseBody));
|
|
32004
|
+
if (held.success)
|
|
32005
|
+
return { claimed: false, holder: held.data.holder };
|
|
32006
|
+
}
|
|
32007
|
+
throw error;
|
|
32008
|
+
}
|
|
32009
|
+
return { claimed: true, interest: InterestWireSchema.parse(JSON.parse(raw)) };
|
|
32010
|
+
},
|
|
32071
32011
|
getRole: async (role) => RoleWireSchema.parse(JSON.parse(await request(`/v1/roles/${encodeURIComponent(role)}`, { method: "GET" }))),
|
|
32072
32012
|
listSessions: async (input = {}) => {
|
|
32073
32013
|
const search = new URLSearchParams;
|
|
@@ -32163,6 +32103,20 @@ async function copySessionID(copyToClipboard, sessionID) {
|
|
|
32163
32103
|
// extensions/envoy.ts
|
|
32164
32104
|
var codec2 = import_nats.StringCodec();
|
|
32165
32105
|
var NATS_RETRY_INTERVAL_MS = 15000;
|
|
32106
|
+
var ROLE_CLAIM_ENTRY = "envoy-role-claim";
|
|
32107
|
+
function isRoleClaimEntry(entry) {
|
|
32108
|
+
if (typeof entry !== "object" || entry === null)
|
|
32109
|
+
return false;
|
|
32110
|
+
if (!("type" in entry) || entry.type !== "custom")
|
|
32111
|
+
return false;
|
|
32112
|
+
if (!("customType" in entry) || entry.customType !== ROLE_CLAIM_ENTRY)
|
|
32113
|
+
return false;
|
|
32114
|
+
if (!("data" in entry) || typeof entry.data !== "object" || entry.data === null)
|
|
32115
|
+
return false;
|
|
32116
|
+
if (!("role" in entry.data))
|
|
32117
|
+
return false;
|
|
32118
|
+
return entry.data.role === null || typeof entry.data.role === "string";
|
|
32119
|
+
}
|
|
32166
32120
|
var LEGION_ROLE_CLAIM_BRIDGE = Symbol.for("legion.pi-envoy.role-claim-bridge");
|
|
32167
32121
|
function legionRoleClaimBridge() {
|
|
32168
32122
|
const store = globalThis;
|
|
@@ -32360,8 +32314,81 @@ function envoyExtension(pi) {
|
|
|
32360
32314
|
await subscribe(topic);
|
|
32361
32315
|
}
|
|
32362
32316
|
};
|
|
32363
|
-
const
|
|
32317
|
+
const setEnvoyRole = async (role, options = {}) => {
|
|
32318
|
+
const topic = ROLE_TOPIC_PREFIX + role;
|
|
32319
|
+
const previousTopic = claimedRoleTopic;
|
|
32320
|
+
const result = await client.setRole({
|
|
32321
|
+
sessionID,
|
|
32322
|
+
role,
|
|
32323
|
+
soft: options.soft,
|
|
32324
|
+
previousSessionID: options.previousSessionID
|
|
32325
|
+
});
|
|
32326
|
+
if (!result.claimed) {
|
|
32327
|
+
logger.warn("envoy: role held by another live session; not reclaimed", {
|
|
32328
|
+
role,
|
|
32329
|
+
sessionID,
|
|
32330
|
+
holder: result.holder
|
|
32331
|
+
});
|
|
32332
|
+
claimedRoleTopic = undefined;
|
|
32333
|
+
return false;
|
|
32334
|
+
}
|
|
32335
|
+
claimedRoleTopic = topic;
|
|
32336
|
+
pi.appendEntry(ROLE_CLAIM_ENTRY, { role });
|
|
32337
|
+
if (activeSessionContext !== undefined)
|
|
32338
|
+
ensureHeartbeat(activeSessionContext);
|
|
32339
|
+
if (previousTopic !== undefined && previousTopic !== topic) {
|
|
32340
|
+
await client.unsubscribe({ sessionID, topics: [previousTopic] });
|
|
32341
|
+
}
|
|
32342
|
+
return true;
|
|
32343
|
+
};
|
|
32344
|
+
const releaseClaimedRole = () => {
|
|
32345
|
+
claimedRoleTopic = undefined;
|
|
32346
|
+
pi.appendEntry(ROLE_CLAIM_ENTRY, { role: null });
|
|
32347
|
+
};
|
|
32348
|
+
const transcriptClaimedRole = (branch) => {
|
|
32349
|
+
let role;
|
|
32350
|
+
for (const entry of branch) {
|
|
32351
|
+
if (!isRoleClaimEntry(entry))
|
|
32352
|
+
continue;
|
|
32353
|
+
role = entry.data.role;
|
|
32354
|
+
}
|
|
32355
|
+
return role;
|
|
32356
|
+
};
|
|
32357
|
+
const roleSources = (previousSessionID, carryPreviousSessionRole) => previousSessionID === "" || previousSessionID === sessionID || !carryPreviousSessionRole ? [sessionID] : [sessionID, previousSessionID];
|
|
32358
|
+
const reclaimHeldRoles = async (previousSessionID, branch, carryPreviousSessionRole) => {
|
|
32359
|
+
if (!carryPreviousSessionRole)
|
|
32360
|
+
claimedRoleTopic = undefined;
|
|
32361
|
+
const remembered = transcriptClaimedRole(branch);
|
|
32362
|
+
if (remembered === null) {
|
|
32363
|
+
claimedRoleTopic = undefined;
|
|
32364
|
+
return;
|
|
32365
|
+
}
|
|
32366
|
+
let role = remembered ?? claimedRoleTopic?.slice(ROLE_TOPIC_PREFIX.length);
|
|
32367
|
+
if (role === undefined) {
|
|
32368
|
+
for (const source of roleSources(previousSessionID, carryPreviousSessionRole)) {
|
|
32369
|
+
const registry = await client.getInterest(source).catch(() => {
|
|
32370
|
+
return;
|
|
32371
|
+
});
|
|
32372
|
+
const topic = registry?.topics.find((candidate) => candidate.startsWith(ROLE_TOPIC_PREFIX));
|
|
32373
|
+
if (topic !== undefined) {
|
|
32374
|
+
role = topic.slice(ROLE_TOPIC_PREFIX.length);
|
|
32375
|
+
break;
|
|
32376
|
+
}
|
|
32377
|
+
}
|
|
32378
|
+
}
|
|
32379
|
+
if (role === undefined)
|
|
32380
|
+
return;
|
|
32381
|
+
const continued = carryPreviousSessionRole && previousSessionID !== "" && previousSessionID !== sessionID ? previousSessionID : undefined;
|
|
32382
|
+
await setEnvoyRole(role, { soft: true, previousSessionID: continued }).catch((error) => {
|
|
32383
|
+
logger.warn("envoy: role reclaim failed", { role, sessionID, error: messageFor(error) });
|
|
32384
|
+
});
|
|
32385
|
+
};
|
|
32386
|
+
let sessionEstablishment = Promise.resolve();
|
|
32387
|
+
const establishSessionNow = async (context, options = {}) => {
|
|
32388
|
+
const previousSessionID = sessionID;
|
|
32364
32389
|
const previousTopic = sessionID === "" ? undefined : agentSubject(sessionID);
|
|
32390
|
+
if (options.carryPreviousSessionRole === false)
|
|
32391
|
+
claimedRoleTopic = undefined;
|
|
32365
32392
|
sessionDirectory = context.cwd;
|
|
32366
32393
|
sessionID = context.sessionManager.getSessionId();
|
|
32367
32394
|
activeSessionContext = context;
|
|
@@ -32377,22 +32404,23 @@ function envoyExtension(pi) {
|
|
|
32377
32404
|
}
|
|
32378
32405
|
await ensureConnection();
|
|
32379
32406
|
await subscribe(currentTopic);
|
|
32380
|
-
|
|
32407
|
+
const branch = context.sessionManager.getBranch?.() ?? [];
|
|
32408
|
+
const resumed = branch.length > 0;
|
|
32409
|
+
if (resumed) {
|
|
32381
32410
|
await recoverRegisteredInterests();
|
|
32382
32411
|
}
|
|
32383
32412
|
await registerSession();
|
|
32413
|
+
if (resumed || previousSessionID !== "") {
|
|
32414
|
+
await reclaimHeldRoles(previousSessionID, branch, options.carryPreviousSessionRole ?? true);
|
|
32415
|
+
}
|
|
32384
32416
|
ensureHeartbeat(context);
|
|
32385
32417
|
};
|
|
32386
|
-
const
|
|
32387
|
-
const
|
|
32388
|
-
|
|
32389
|
-
|
|
32390
|
-
|
|
32391
|
-
|
|
32392
|
-
ensureHeartbeat(activeSessionContext);
|
|
32393
|
-
if (previousTopic !== undefined && previousTopic !== topic) {
|
|
32394
|
-
await client.unsubscribe({ sessionID, topics: [previousTopic] });
|
|
32395
|
-
}
|
|
32418
|
+
const establishSession = (context, options = {}) => {
|
|
32419
|
+
const run = sessionEstablishment.then(() => establishSessionNow(context, options), () => establishSessionNow(context, options));
|
|
32420
|
+
sessionEstablishment = run.catch(() => {
|
|
32421
|
+
return;
|
|
32422
|
+
});
|
|
32423
|
+
return run;
|
|
32396
32424
|
};
|
|
32397
32425
|
const bridge = legionRoleClaimBridge();
|
|
32398
32426
|
const claim = async (targetSessionID, role, callerContext) => {
|
|
@@ -32402,8 +32430,8 @@ function envoyExtension(pi) {
|
|
|
32402
32430
|
}
|
|
32403
32431
|
if (sessionID !== targetSessionID)
|
|
32404
32432
|
await establishSession(context);
|
|
32405
|
-
await setEnvoyRole(role);
|
|
32406
32433
|
await registerSession();
|
|
32434
|
+
await setEnvoyRole(role);
|
|
32407
32435
|
};
|
|
32408
32436
|
bridge.claim = claim;
|
|
32409
32437
|
bridge.ready.resolve(claim);
|
|
@@ -32445,7 +32473,8 @@ function envoyExtension(pi) {
|
|
|
32445
32473
|
return;
|
|
32446
32474
|
const previousID = sessionID;
|
|
32447
32475
|
try {
|
|
32448
|
-
|
|
32476
|
+
const carryPreviousSessionRole = reason !== "new" && reason !== "resume";
|
|
32477
|
+
await establishSession(context, { carryPreviousSessionRole });
|
|
32449
32478
|
} catch (error) {
|
|
32450
32479
|
context.ui.notify(`envoy: rebind failed (${messageFor(error)}); will recover on reconnect`, "warning");
|
|
32451
32480
|
}
|
|
@@ -32579,12 +32608,12 @@ Warnings: ${warnings.join("; ")}`, {
|
|
|
32579
32608
|
...claimedRoleTopic === undefined ? [] : [claimedRoleTopic]
|
|
32580
32609
|
]).filter((topic) => topic !== inbox);
|
|
32581
32610
|
const removed = targets.filter((topic) => closeIntentionally(topic) || topic === claimedRoleTopic);
|
|
32582
|
-
|
|
32583
|
-
claimedRoleTopic = undefined;
|
|
32584
|
-
}
|
|
32611
|
+
const releasingRole = claimedRoleTopic !== undefined && removed.includes(claimedRoleTopic);
|
|
32585
32612
|
const registrationError = removed.length === 0 ? undefined : await client.unsubscribe({ sessionID, topics: removed }).then(registerSession).then(() => {
|
|
32586
32613
|
return;
|
|
32587
32614
|
}, messageFor);
|
|
32615
|
+
if (releasingRole && registrationError === undefined)
|
|
32616
|
+
releaseClaimedRole();
|
|
32588
32617
|
return toolSuccess(`Unsubscribed: ${removed.join(", ") || "(none)"}`, {
|
|
32589
32618
|
removed,
|
|
32590
32619
|
...registrationError === undefined ? {} : { registrationError }
|
package/dist/legion.js
CHANGED
|
@@ -31282,147 +31282,37 @@ var ChildStatusPayloadSchema = object({
|
|
|
31282
31282
|
from: string2().optional(),
|
|
31283
31283
|
to: string2().optional()
|
|
31284
31284
|
});
|
|
31285
|
-
|
|
31286
|
-
|
|
31287
|
-
|
|
31288
|
-
|
|
31289
|
-
|
|
31290
|
-
|
|
31291
|
-
|
|
31292
|
-
|
|
31293
|
-
|
|
31294
|
-
|
|
31295
|
-
|
|
31296
|
-
|
|
31297
|
-
|
|
31298
|
-
|
|
31299
|
-
|
|
31300
|
-
|
|
31301
|
-
|
|
31302
|
-
|
|
31303
|
-
|
|
31304
|
-
|
|
31305
|
-
|
|
31306
|
-
|
|
31307
|
-
|
|
31308
|
-
|
|
31309
|
-
lines.push(`Artifact: ${name}`);
|
|
31310
|
-
break;
|
|
31311
|
-
}
|
|
31312
|
-
case "artifact.version": {
|
|
31313
|
-
const parsed = ArtifactVersionPayloadSchema.safeParse(event.payload);
|
|
31314
|
-
if (!parsed.success)
|
|
31315
|
-
break;
|
|
31316
|
-
const version = parsed.data.version;
|
|
31317
|
-
if (parsed.data.name)
|
|
31318
|
-
lines.push(`Artifact: ${parsed.data.name}`);
|
|
31319
|
-
if (version?.number !== undefined)
|
|
31320
|
-
lines.push(`Version: ${version.number}`);
|
|
31321
|
-
if (version?.summary)
|
|
31322
|
-
lines.push(`Summary: ${version.summary}`);
|
|
31323
|
-
if (parsed.data.diff)
|
|
31324
|
-
lines.push("Diff:", parsed.data.diff);
|
|
31325
|
-
break;
|
|
31326
|
-
}
|
|
31327
|
-
case "ask.opened":
|
|
31328
|
-
case "ask.answered": {
|
|
31329
|
-
const parsed = AskPayloadSchema.safeParse(event.payload);
|
|
31330
|
-
if (!parsed.success)
|
|
31331
|
-
break;
|
|
31332
|
-
if (parsed.data.question)
|
|
31333
|
-
lines.push(`Question: ${parsed.data.question}`);
|
|
31334
|
-
if (event.type === "ask.opened") {
|
|
31335
|
-
const options = (parsed.data.options ?? []).map((option) => option.label).filter((label) => label !== undefined);
|
|
31336
|
-
if (options.length > 0)
|
|
31337
|
-
lines.push(`Options: ${options.join(", ")}`);
|
|
31338
|
-
break;
|
|
31339
|
-
}
|
|
31340
|
-
const answer = parsed.data.answer;
|
|
31341
|
-
lines.push(`Selected: ${answer?.selected?.join(", ") || "none"}`);
|
|
31342
|
-
if (answer?.text)
|
|
31343
|
-
lines.push(`Text: ${answer.text}`);
|
|
31344
|
-
break;
|
|
31345
|
-
}
|
|
31346
|
-
case "comment.created":
|
|
31347
|
-
case "comment.resolved": {
|
|
31348
|
-
const parsed = CommentPayloadSchema.safeParse(event.payload);
|
|
31349
|
-
if (!parsed.success)
|
|
31350
|
-
break;
|
|
31351
|
-
const quote = parsed.data.anchor?.quote;
|
|
31352
|
-
if (parsed.data.artifact_name)
|
|
31353
|
-
lines.push(`Artifact: ${parsed.data.artifact_name}`);
|
|
31354
|
-
if (quote)
|
|
31355
|
-
lines.push(`> ${quote}`);
|
|
31356
|
-
if (parsed.data.reply_to)
|
|
31357
|
-
lines.push(`Reply chain: ${parsed.data.reply_to}`);
|
|
31358
|
-
if (parsed.data.body)
|
|
31359
|
-
lines.push(`Body: ${parsed.data.body}`);
|
|
31360
|
-
break;
|
|
31361
|
-
}
|
|
31362
|
-
case "suggestion.accepted":
|
|
31363
|
-
case "suggestion.rejected": {
|
|
31364
|
-
const parsed = CommentPayloadSchema.safeParse(event.payload);
|
|
31365
|
-
if (!parsed.success)
|
|
31366
|
-
break;
|
|
31367
|
-
const quote = parsed.data.anchor?.quote;
|
|
31368
|
-
const replacement = parsed.data.suggestion?.replace_with;
|
|
31369
|
-
if (parsed.data.artifact_name)
|
|
31370
|
-
lines.push(`Artifact: ${parsed.data.artifact_name}`);
|
|
31371
|
-
if (quote)
|
|
31372
|
-
lines.push(`> ${quote}`);
|
|
31373
|
-
if (quote && replacement)
|
|
31374
|
-
lines.push(`${quote} \u2192 ${replacement}`);
|
|
31375
|
-
break;
|
|
31376
|
-
}
|
|
31377
|
-
case "message.created": {
|
|
31378
|
-
const parsed = MessagePayloadSchema.safeParse(event.payload);
|
|
31379
|
-
const body = parsed.success ? parsed.data.body : undefined;
|
|
31380
|
-
if (body)
|
|
31381
|
-
lines.push(`Body: ${body}`);
|
|
31382
|
-
break;
|
|
31383
|
-
}
|
|
31384
|
-
case "child.status": {
|
|
31385
|
-
const parsed = ChildStatusPayloadSchema.safeParse(event.payload);
|
|
31386
|
-
if (!parsed.success)
|
|
31387
|
-
break;
|
|
31388
|
-
const { child_key: child, from, to } = parsed.data;
|
|
31389
|
-
if (child && from && to)
|
|
31390
|
-
lines.push(`Child: ${child} ${from} \u2192 ${to}`);
|
|
31391
|
-
break;
|
|
31392
|
-
}
|
|
31393
|
-
}
|
|
31394
|
-
return lines;
|
|
31395
|
-
}
|
|
31396
|
-
function renderDispatch(envelope, sessionID) {
|
|
31397
|
-
const invalid = () => ({
|
|
31398
|
-
skip: false,
|
|
31399
|
-
content: "dispatch: invalid event",
|
|
31400
|
-
envelope
|
|
31401
|
-
});
|
|
31402
|
-
if (envelope.payload === undefined)
|
|
31403
|
-
return invalid();
|
|
31404
|
-
let rawEvent;
|
|
31285
|
+
var DISPATCH_PAYLOAD_SCHEMAS = {
|
|
31286
|
+
"issue.created": IssuePayloadSchema,
|
|
31287
|
+
"issue.updated": IssuePayloadSchema,
|
|
31288
|
+
"issue.closed": IssuePayloadSchema,
|
|
31289
|
+
"artifact.created": ArtifactCreatedPayloadSchema,
|
|
31290
|
+
"artifact.version": ArtifactVersionPayloadSchema,
|
|
31291
|
+
"ask.opened": AskPayloadSchema,
|
|
31292
|
+
"ask.answered": AskPayloadSchema,
|
|
31293
|
+
"comment.created": CommentPayloadSchema,
|
|
31294
|
+
"comment.resolved": CommentPayloadSchema,
|
|
31295
|
+
"suggestion.accepted": CommentPayloadSchema,
|
|
31296
|
+
"suggestion.rejected": CommentPayloadSchema,
|
|
31297
|
+
"message.created": MessagePayloadSchema,
|
|
31298
|
+
"child.status": ChildStatusPayloadSchema
|
|
31299
|
+
};
|
|
31300
|
+
function dispatchPayload(event) {
|
|
31301
|
+
const schema = DISPATCH_PAYLOAD_SCHEMAS[event.type];
|
|
31302
|
+
if (schema === undefined)
|
|
31303
|
+
return event.payload;
|
|
31304
|
+
const parsed = schema.safeParse(event.payload);
|
|
31305
|
+
return parsed.success ? parsed.data : event.payload;
|
|
31306
|
+
}
|
|
31307
|
+
function parseDispatchFrame(rawPayload) {
|
|
31308
|
+
let value;
|
|
31405
31309
|
try {
|
|
31406
|
-
|
|
31310
|
+
value = JSON.parse(rawPayload);
|
|
31407
31311
|
} catch {
|
|
31408
|
-
return
|
|
31409
|
-
}
|
|
31410
|
-
const parsed = DispatchEventSchema.safeParse(rawEvent);
|
|
31411
|
-
if (!parsed.success)
|
|
31412
|
-
return invalid();
|
|
31413
|
-
const event = parsed.data;
|
|
31414
|
-
if (event.actor.kind === "session" && event.actor.id === sessionID) {
|
|
31415
|
-
return { skip: true, content: "", envelope };
|
|
31312
|
+
return { raw: rawPayload };
|
|
31416
31313
|
}
|
|
31417
|
-
|
|
31418
|
-
|
|
31419
|
-
content: [
|
|
31420
|
-
`dispatch ${event.issue_key} \xB7 ${event.type} \xB7 by ${dispatchActorLabel(event.actor)}`,
|
|
31421
|
-
...dispatchBody(event)
|
|
31422
|
-
].join(`
|
|
31423
|
-
`),
|
|
31424
|
-
envelope
|
|
31425
|
-
};
|
|
31314
|
+
const parsed = DispatchEventSchema.safeParse(value);
|
|
31315
|
+
return parsed.success ? { event: parsed.data } : { raw: value };
|
|
31426
31316
|
}
|
|
31427
31317
|
function inboundTimestamp(milliseconds) {
|
|
31428
31318
|
if (milliseconds === undefined)
|
|
@@ -31458,13 +31348,31 @@ function renderInbound(raw, sessionID, subject) {
|
|
|
31458
31348
|
const data = tolerant.data;
|
|
31459
31349
|
envelope = { ...data, source: data.source ?? "unknown" };
|
|
31460
31350
|
}
|
|
31351
|
+
let dispatchEvent;
|
|
31352
|
+
let dispatchIssue;
|
|
31353
|
+
const dispatchRendered = envelope.source === "dispatch" && envelope.payload !== undefined;
|
|
31461
31354
|
if (envelope.source === "dispatch") {
|
|
31462
|
-
|
|
31463
|
-
|
|
31464
|
-
|
|
31355
|
+
if (envelope.payload === undefined) {
|
|
31356
|
+
dispatchIssue = "payload";
|
|
31357
|
+
} else {
|
|
31358
|
+
const frame = parseDispatchFrame(envelope.payload);
|
|
31359
|
+
if ("event" in frame) {
|
|
31360
|
+
if (frame.event.actor.kind === "session" && frame.event.actor.id === sessionID) {
|
|
31361
|
+
return { skip: true, content: "", envelope };
|
|
31362
|
+
}
|
|
31363
|
+
dispatchEvent = {
|
|
31364
|
+
issue_key: frame.event.issue_key,
|
|
31365
|
+
type: frame.event.type,
|
|
31366
|
+
actor: frame.event.actor,
|
|
31367
|
+
payload: dispatchPayload(frame.event)
|
|
31368
|
+
};
|
|
31369
|
+
} else {
|
|
31370
|
+
dispatchEvent = frame.raw;
|
|
31371
|
+
}
|
|
31372
|
+
}
|
|
31465
31373
|
}
|
|
31466
31374
|
let parsedPayload;
|
|
31467
|
-
if (envelope.payload !== undefined && (envelope.source === "github" || envelope.payload !== envelope.payload_summary)) {
|
|
31375
|
+
if (!dispatchRendered && envelope.payload !== undefined && (envelope.source === "github" || envelope.payload !== envelope.payload_summary)) {
|
|
31468
31376
|
try {
|
|
31469
31377
|
parsedPayload = JSON.parse(envelope.payload);
|
|
31470
31378
|
} catch {
|
|
@@ -31472,7 +31380,7 @@ function renderInbound(raw, sessionID, subject) {
|
|
|
31472
31380
|
}
|
|
31473
31381
|
}
|
|
31474
31382
|
const payloadSummary = envelope.payload_summary ?? "unknown";
|
|
31475
|
-
const message = envelope.payload !== undefined && envelope.payload !== envelope.payload_summary ? parsedPayload : undefined;
|
|
31383
|
+
const message = !dispatchRendered && envelope.payload !== undefined && envelope.payload !== envelope.payload_summary ? parsedPayload : undefined;
|
|
31476
31384
|
const summaryIsHead = typeof message === "string" && envelope.payload_summary !== undefined && message.startsWith(envelope.payload_summary.endsWith("\u2026") ? envelope.payload_summary.slice(0, -1) : envelope.payload_summary);
|
|
31477
31385
|
const body = `${envelope.payload_summary ?? ""}
|
|
31478
31386
|
${envelope.payload ?? ""}`;
|
|
@@ -31486,7 +31394,11 @@ ${envelope.payload ?? ""}`;
|
|
|
31486
31394
|
const role = envelope.sender?.roles?.[0];
|
|
31487
31395
|
const reply = replyWith(envelope);
|
|
31488
31396
|
const sourceIssue = parseIssues.includes("source") || KNOWN_SOURCES[envelope.source] !== undefined ? undefined : `source=${envelope.source}`;
|
|
31489
|
-
const unrecognised = [
|
|
31397
|
+
const unrecognised = [
|
|
31398
|
+
...parseIssues,
|
|
31399
|
+
...sourceIssue === undefined ? [] : [sourceIssue],
|
|
31400
|
+
...dispatchIssue === undefined ? [] : [dispatchIssue]
|
|
31401
|
+
].join(", ") || undefined;
|
|
31490
31402
|
const rendered = {
|
|
31491
31403
|
...envelope.topic === agentSubject(sessionID) ? { to: `you (${sessionID.slice(0, 4)}\u2026)` } : {},
|
|
31492
31404
|
from: senderLabel(envelope),
|
|
@@ -31501,8 +31413,10 @@ ${envelope.payload ?? ""}`;
|
|
|
31501
31413
|
...role === undefined ? {} : {
|
|
31502
31414
|
reply_role: `envoy_publish(topic="notifications.role.${role}", message="...")`
|
|
31503
31415
|
},
|
|
31504
|
-
...
|
|
31505
|
-
|
|
31416
|
+
...dispatchRendered ? { dispatch: dispatchEvent } : {
|
|
31417
|
+
...summaryIsHead ? {} : { summary: payloadSummary },
|
|
31418
|
+
...message === undefined ? {} : { message }
|
|
31419
|
+
},
|
|
31506
31420
|
...foreignSession === undefined ? {} : {
|
|
31507
31421
|
note: `body names session ${foreignSession}; the sender is ${envelope.source_session ?? "unknown"}`
|
|
31508
31422
|
},
|
|
@@ -32425,6 +32339,11 @@ var RoleWireSchema = object({
|
|
|
32425
32339
|
holder: string2(),
|
|
32426
32340
|
last_seen: number2().int()
|
|
32427
32341
|
});
|
|
32342
|
+
var RoleHeldWireSchema = object({
|
|
32343
|
+
error: string2(),
|
|
32344
|
+
role: string2(),
|
|
32345
|
+
holder: string2()
|
|
32346
|
+
});
|
|
32428
32347
|
var ErrorWireSchema = object({
|
|
32429
32348
|
error: string2(),
|
|
32430
32349
|
expected: array(string2()).optional()
|
|
@@ -32557,7 +32476,28 @@ function createEnvoyClient(config) {
|
|
|
32557
32476
|
unregisterSession: async (sessionID) => {
|
|
32558
32477
|
await request(`/v1/sessions/${encodeURIComponent(sessionID)}`, { method: "DELETE" });
|
|
32559
32478
|
},
|
|
32560
|
-
setRole: async (input) =>
|
|
32479
|
+
setRole: async (input) => {
|
|
32480
|
+
const soft = input.soft === true;
|
|
32481
|
+
const previous = soft ? input.previousSessionID ?? "" : "";
|
|
32482
|
+
const body = {
|
|
32483
|
+
session_id: input.sessionID,
|
|
32484
|
+
role: input.role,
|
|
32485
|
+
...soft ? { soft: true } : {},
|
|
32486
|
+
...previous === "" ? {} : { previous_session_id: previous }
|
|
32487
|
+
};
|
|
32488
|
+
let raw;
|
|
32489
|
+
try {
|
|
32490
|
+
raw = await post("/v1/roles/set", body);
|
|
32491
|
+
} catch (error) {
|
|
32492
|
+
if (error instanceof EnvoyApiError && error.details.status === 409) {
|
|
32493
|
+
const held = RoleHeldWireSchema.safeParse(JSON.parse(error.details.responseBody));
|
|
32494
|
+
if (held.success)
|
|
32495
|
+
return { claimed: false, holder: held.data.holder };
|
|
32496
|
+
}
|
|
32497
|
+
throw error;
|
|
32498
|
+
}
|
|
32499
|
+
return { claimed: true, interest: InterestWireSchema.parse(JSON.parse(raw)) };
|
|
32500
|
+
},
|
|
32561
32501
|
getRole: async (role) => RoleWireSchema.parse(JSON.parse(await request(`/v1/roles/${encodeURIComponent(role)}`, { method: "GET" }))),
|
|
32562
32502
|
listSessions: async (input = {}) => {
|
|
32563
32503
|
const search = new URLSearchParams;
|
|
@@ -32645,6 +32585,20 @@ async function copySessionID(copyToClipboard, sessionID) {
|
|
|
32645
32585
|
// extensions/envoy.ts
|
|
32646
32586
|
var codec2 = import_nats.StringCodec();
|
|
32647
32587
|
var NATS_RETRY_INTERVAL_MS = 15000;
|
|
32588
|
+
var ROLE_CLAIM_ENTRY = "envoy-role-claim";
|
|
32589
|
+
function isRoleClaimEntry(entry) {
|
|
32590
|
+
if (typeof entry !== "object" || entry === null)
|
|
32591
|
+
return false;
|
|
32592
|
+
if (!("type" in entry) || entry.type !== "custom")
|
|
32593
|
+
return false;
|
|
32594
|
+
if (!("customType" in entry) || entry.customType !== ROLE_CLAIM_ENTRY)
|
|
32595
|
+
return false;
|
|
32596
|
+
if (!("data" in entry) || typeof entry.data !== "object" || entry.data === null)
|
|
32597
|
+
return false;
|
|
32598
|
+
if (!("role" in entry.data))
|
|
32599
|
+
return false;
|
|
32600
|
+
return entry.data.role === null || typeof entry.data.role === "string";
|
|
32601
|
+
}
|
|
32648
32602
|
var LEGION_ROLE_CLAIM_BRIDGE = Symbol.for("legion.pi-envoy.role-claim-bridge");
|
|
32649
32603
|
function legionRoleClaimBridge() {
|
|
32650
32604
|
const store = globalThis;
|
|
@@ -32842,8 +32796,81 @@ function envoyExtension(pi) {
|
|
|
32842
32796
|
await subscribe(topic);
|
|
32843
32797
|
}
|
|
32844
32798
|
};
|
|
32845
|
-
const
|
|
32799
|
+
const setEnvoyRole = async (role, options = {}) => {
|
|
32800
|
+
const topic = ROLE_TOPIC_PREFIX + role;
|
|
32801
|
+
const previousTopic = claimedRoleTopic;
|
|
32802
|
+
const result = await client.setRole({
|
|
32803
|
+
sessionID,
|
|
32804
|
+
role,
|
|
32805
|
+
soft: options.soft,
|
|
32806
|
+
previousSessionID: options.previousSessionID
|
|
32807
|
+
});
|
|
32808
|
+
if (!result.claimed) {
|
|
32809
|
+
logger.warn("envoy: role held by another live session; not reclaimed", {
|
|
32810
|
+
role,
|
|
32811
|
+
sessionID,
|
|
32812
|
+
holder: result.holder
|
|
32813
|
+
});
|
|
32814
|
+
claimedRoleTopic = undefined;
|
|
32815
|
+
return false;
|
|
32816
|
+
}
|
|
32817
|
+
claimedRoleTopic = topic;
|
|
32818
|
+
pi.appendEntry(ROLE_CLAIM_ENTRY, { role });
|
|
32819
|
+
if (activeSessionContext !== undefined)
|
|
32820
|
+
ensureHeartbeat(activeSessionContext);
|
|
32821
|
+
if (previousTopic !== undefined && previousTopic !== topic) {
|
|
32822
|
+
await client.unsubscribe({ sessionID, topics: [previousTopic] });
|
|
32823
|
+
}
|
|
32824
|
+
return true;
|
|
32825
|
+
};
|
|
32826
|
+
const releaseClaimedRole = () => {
|
|
32827
|
+
claimedRoleTopic = undefined;
|
|
32828
|
+
pi.appendEntry(ROLE_CLAIM_ENTRY, { role: null });
|
|
32829
|
+
};
|
|
32830
|
+
const transcriptClaimedRole = (branch) => {
|
|
32831
|
+
let role;
|
|
32832
|
+
for (const entry of branch) {
|
|
32833
|
+
if (!isRoleClaimEntry(entry))
|
|
32834
|
+
continue;
|
|
32835
|
+
role = entry.data.role;
|
|
32836
|
+
}
|
|
32837
|
+
return role;
|
|
32838
|
+
};
|
|
32839
|
+
const roleSources = (previousSessionID, carryPreviousSessionRole) => previousSessionID === "" || previousSessionID === sessionID || !carryPreviousSessionRole ? [sessionID] : [sessionID, previousSessionID];
|
|
32840
|
+
const reclaimHeldRoles = async (previousSessionID, branch, carryPreviousSessionRole) => {
|
|
32841
|
+
if (!carryPreviousSessionRole)
|
|
32842
|
+
claimedRoleTopic = undefined;
|
|
32843
|
+
const remembered = transcriptClaimedRole(branch);
|
|
32844
|
+
if (remembered === null) {
|
|
32845
|
+
claimedRoleTopic = undefined;
|
|
32846
|
+
return;
|
|
32847
|
+
}
|
|
32848
|
+
let role = remembered ?? claimedRoleTopic?.slice(ROLE_TOPIC_PREFIX.length);
|
|
32849
|
+
if (role === undefined) {
|
|
32850
|
+
for (const source of roleSources(previousSessionID, carryPreviousSessionRole)) {
|
|
32851
|
+
const registry = await client.getInterest(source).catch(() => {
|
|
32852
|
+
return;
|
|
32853
|
+
});
|
|
32854
|
+
const topic = registry?.topics.find((candidate) => candidate.startsWith(ROLE_TOPIC_PREFIX));
|
|
32855
|
+
if (topic !== undefined) {
|
|
32856
|
+
role = topic.slice(ROLE_TOPIC_PREFIX.length);
|
|
32857
|
+
break;
|
|
32858
|
+
}
|
|
32859
|
+
}
|
|
32860
|
+
}
|
|
32861
|
+
if (role === undefined)
|
|
32862
|
+
return;
|
|
32863
|
+
const continued = carryPreviousSessionRole && previousSessionID !== "" && previousSessionID !== sessionID ? previousSessionID : undefined;
|
|
32864
|
+
await setEnvoyRole(role, { soft: true, previousSessionID: continued }).catch((error) => {
|
|
32865
|
+
logger.warn("envoy: role reclaim failed", { role, sessionID, error: messageFor(error) });
|
|
32866
|
+
});
|
|
32867
|
+
};
|
|
32868
|
+
let sessionEstablishment = Promise.resolve();
|
|
32869
|
+
const establishSessionNow = async (context, options = {}) => {
|
|
32870
|
+
const previousSessionID = sessionID;
|
|
32846
32871
|
const previousTopic = sessionID === "" ? undefined : agentSubject(sessionID);
|
|
32872
|
+
if (options.carryPreviousSessionRole === false)
|
|
32873
|
+
claimedRoleTopic = undefined;
|
|
32847
32874
|
sessionDirectory = context.cwd;
|
|
32848
32875
|
sessionID = context.sessionManager.getSessionId();
|
|
32849
32876
|
activeSessionContext = context;
|
|
@@ -32859,22 +32886,23 @@ function envoyExtension(pi) {
|
|
|
32859
32886
|
}
|
|
32860
32887
|
await ensureConnection();
|
|
32861
32888
|
await subscribe(currentTopic);
|
|
32862
|
-
|
|
32889
|
+
const branch = context.sessionManager.getBranch?.() ?? [];
|
|
32890
|
+
const resumed = branch.length > 0;
|
|
32891
|
+
if (resumed) {
|
|
32863
32892
|
await recoverRegisteredInterests();
|
|
32864
32893
|
}
|
|
32865
32894
|
await registerSession();
|
|
32895
|
+
if (resumed || previousSessionID !== "") {
|
|
32896
|
+
await reclaimHeldRoles(previousSessionID, branch, options.carryPreviousSessionRole ?? true);
|
|
32897
|
+
}
|
|
32866
32898
|
ensureHeartbeat(context);
|
|
32867
32899
|
};
|
|
32868
|
-
const
|
|
32869
|
-
const
|
|
32870
|
-
|
|
32871
|
-
|
|
32872
|
-
|
|
32873
|
-
|
|
32874
|
-
ensureHeartbeat(activeSessionContext);
|
|
32875
|
-
if (previousTopic !== undefined && previousTopic !== topic) {
|
|
32876
|
-
await client.unsubscribe({ sessionID, topics: [previousTopic] });
|
|
32877
|
-
}
|
|
32900
|
+
const establishSession = (context, options = {}) => {
|
|
32901
|
+
const run = sessionEstablishment.then(() => establishSessionNow(context, options), () => establishSessionNow(context, options));
|
|
32902
|
+
sessionEstablishment = run.catch(() => {
|
|
32903
|
+
return;
|
|
32904
|
+
});
|
|
32905
|
+
return run;
|
|
32878
32906
|
};
|
|
32879
32907
|
const bridge = legionRoleClaimBridge();
|
|
32880
32908
|
const claim = async (targetSessionID, role, callerContext) => {
|
|
@@ -32884,8 +32912,8 @@ function envoyExtension(pi) {
|
|
|
32884
32912
|
}
|
|
32885
32913
|
if (sessionID !== targetSessionID)
|
|
32886
32914
|
await establishSession(context);
|
|
32887
|
-
await setEnvoyRole(role);
|
|
32888
32915
|
await registerSession();
|
|
32916
|
+
await setEnvoyRole(role);
|
|
32889
32917
|
};
|
|
32890
32918
|
bridge.claim = claim;
|
|
32891
32919
|
bridge.ready.resolve(claim);
|
|
@@ -32927,7 +32955,8 @@ function envoyExtension(pi) {
|
|
|
32927
32955
|
return;
|
|
32928
32956
|
const previousID = sessionID;
|
|
32929
32957
|
try {
|
|
32930
|
-
|
|
32958
|
+
const carryPreviousSessionRole = reason !== "new" && reason !== "resume";
|
|
32959
|
+
await establishSession(context, { carryPreviousSessionRole });
|
|
32931
32960
|
} catch (error) {
|
|
32932
32961
|
context.ui.notify(`envoy: rebind failed (${messageFor(error)}); will recover on reconnect`, "warning");
|
|
32933
32962
|
}
|
|
@@ -33061,12 +33090,12 @@ Warnings: ${warnings.join("; ")}`, {
|
|
|
33061
33090
|
...claimedRoleTopic === undefined ? [] : [claimedRoleTopic]
|
|
33062
33091
|
]).filter((topic) => topic !== inbox);
|
|
33063
33092
|
const removed = targets.filter((topic) => closeIntentionally(topic) || topic === claimedRoleTopic);
|
|
33064
|
-
|
|
33065
|
-
claimedRoleTopic = undefined;
|
|
33066
|
-
}
|
|
33093
|
+
const releasingRole = claimedRoleTopic !== undefined && removed.includes(claimedRoleTopic);
|
|
33067
33094
|
const registrationError = removed.length === 0 ? undefined : await client.unsubscribe({ sessionID, topics: removed }).then(registerSession).then(() => {
|
|
33068
33095
|
return;
|
|
33069
33096
|
}, messageFor);
|
|
33097
|
+
if (releasingRole && registrationError === undefined)
|
|
33098
|
+
releaseClaimedRole();
|
|
33070
33099
|
return toolSuccess(`Unsubscribed: ${removed.join(", ") || "(none)"}`, {
|
|
33071
33100
|
removed,
|
|
33072
33101
|
...registrationError === undefined ? {} : { registrationError }
|
|
@@ -123,6 +123,21 @@ Publish to a role; do not subscribe as its holder. A successful `envoy_publish`
|
|
|
123
123
|
its live `holder`; an unheld role returns an error. Use `envoy_role_get(role="reviewer")` to find
|
|
124
124
|
the live holder first when you need one.
|
|
125
125
|
|
|
126
|
+
A claim belongs to the session, not the process. `omp --resume` re-claims the role recorded in
|
|
127
|
+
the session transcript, including after the listener's stale-interest reaper has removed the old
|
|
128
|
+
row. A `/fork`, `/branch`, `/handoff`, or other transcript-carrying switch checks the
|
|
129
|
+
previous id and moves any held role to the new id while establishing the new session. A `/new`
|
|
130
|
+
or `/resume` switch installs an unrelated transcript and carries nothing. A process that dies and
|
|
131
|
+
is never resumed does not stay a live holder; once liveness expires, role publishes return
|
|
132
|
+
`no holder` until that transcript resumes or another session claims the role.
|
|
133
|
+
|
|
134
|
+
Automatic reclaim never steals. It is a *soft* claim, which the listener grants only when the role
|
|
135
|
+
is unheld, held by a session that is no longer live, or held by the id this session continues (a
|
|
136
|
+
fork's parent). If a different live session holds it — the parent of a `/fork` whose role moved to
|
|
137
|
+
the child, or a second process on the same transcript — the listener refuses atomically and the
|
|
138
|
+
resumed session logs a warning and holds nothing. Only an explicit `envoy_role_set` is
|
|
139
|
+
last-claim-wins. Re-running `envoy_role_set` for a role this session already holds is harmless.
|
|
140
|
+
|
|
126
141
|
## Legion role claims
|
|
127
142
|
|
|
128
143
|
Legion agents receive through a daemon-minted role token. Claim the assigned role with
|