@sjawhar/pi-legion-envoy 0.16.1 → 0.18.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 +135 -20
- package/dist/legion.js +135 -20
- package/dist/skills/envoy/SKILL.md +15 -0
- package/package.json +1 -1
package/dist/envoy.js
CHANGED
|
@@ -16117,7 +16117,7 @@ import { dirname, resolve } from "path";
|
|
|
16117
16117
|
import { fileURLToPath } from "url";
|
|
16118
16118
|
|
|
16119
16119
|
// ../contracts/src/dispatch-tools.ts
|
|
16120
|
-
var ISSUE_REFERENCE = "An issue is a native KEY or external owner/repo#n reference; an external reference creates its native issue on first use
|
|
16120
|
+
var ISSUE_REFERENCE = "An issue is a native KEY or external owner/repo#n reference; an external reference creates its native issue on first use in the repository's mapped project (DISPATCH_REPO_PROJECTS) or, failing that, the default project (DISPATCH_DEFAULT_PROJECT).";
|
|
16121
16121
|
var ASK_URGENCIES = ["low", "med", "high", "blocking"];
|
|
16122
16122
|
var DOC_EDIT_OPS = ["replace", "delete", "insert"];
|
|
16123
16123
|
var dispatchToolSpecs = [
|
|
@@ -31684,7 +31684,7 @@ async function ensureIssue(client, issueReference, actor) {
|
|
|
31684
31684
|
} catch (error) {
|
|
31685
31685
|
if (error instanceof DispatchServiceError && error.code === "PROJECT_UNMAPPED") {
|
|
31686
31686
|
const repository = issueReference.slice(0, issueReference.lastIndexOf("#"));
|
|
31687
|
-
throw new Error(`repository ${repository} is not mapped in DISPATCH_REPO_PROJECTS`);
|
|
31687
|
+
throw new Error(`repository ${repository} is not mapped in DISPATCH_REPO_PROJECTS and no DISPATCH_DEFAULT_PROJECT is configured`);
|
|
31688
31688
|
}
|
|
31689
31689
|
throw error;
|
|
31690
31690
|
}
|
|
@@ -31849,6 +31849,11 @@ var RoleWireSchema = object({
|
|
|
31849
31849
|
holder: string2(),
|
|
31850
31850
|
last_seen: number2().int()
|
|
31851
31851
|
});
|
|
31852
|
+
var RoleHeldWireSchema = object({
|
|
31853
|
+
error: string2(),
|
|
31854
|
+
role: string2(),
|
|
31855
|
+
holder: string2()
|
|
31856
|
+
});
|
|
31852
31857
|
var ErrorWireSchema = object({
|
|
31853
31858
|
error: string2(),
|
|
31854
31859
|
expected: array(string2()).optional()
|
|
@@ -31981,7 +31986,28 @@ function createEnvoyClient(config) {
|
|
|
31981
31986
|
unregisterSession: async (sessionID) => {
|
|
31982
31987
|
await request(`/v1/sessions/${encodeURIComponent(sessionID)}`, { method: "DELETE" });
|
|
31983
31988
|
},
|
|
31984
|
-
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
|
+
},
|
|
31985
32011
|
getRole: async (role) => RoleWireSchema.parse(JSON.parse(await request(`/v1/roles/${encodeURIComponent(role)}`, { method: "GET" }))),
|
|
31986
32012
|
listSessions: async (input = {}) => {
|
|
31987
32013
|
const search = new URLSearchParams;
|
|
@@ -32077,6 +32103,20 @@ async function copySessionID(copyToClipboard, sessionID) {
|
|
|
32077
32103
|
// extensions/envoy.ts
|
|
32078
32104
|
var codec2 = import_nats.StringCodec();
|
|
32079
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
|
+
}
|
|
32080
32120
|
var LEGION_ROLE_CLAIM_BRIDGE = Symbol.for("legion.pi-envoy.role-claim-bridge");
|
|
32081
32121
|
function legionRoleClaimBridge() {
|
|
32082
32122
|
const store = globalThis;
|
|
@@ -32274,8 +32314,81 @@ function envoyExtension(pi) {
|
|
|
32274
32314
|
await subscribe(topic);
|
|
32275
32315
|
}
|
|
32276
32316
|
};
|
|
32277
|
-
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;
|
|
32278
32389
|
const previousTopic = sessionID === "" ? undefined : agentSubject(sessionID);
|
|
32390
|
+
if (options.carryPreviousSessionRole === false)
|
|
32391
|
+
claimedRoleTopic = undefined;
|
|
32279
32392
|
sessionDirectory = context.cwd;
|
|
32280
32393
|
sessionID = context.sessionManager.getSessionId();
|
|
32281
32394
|
activeSessionContext = context;
|
|
@@ -32291,22 +32404,23 @@ function envoyExtension(pi) {
|
|
|
32291
32404
|
}
|
|
32292
32405
|
await ensureConnection();
|
|
32293
32406
|
await subscribe(currentTopic);
|
|
32294
|
-
|
|
32407
|
+
const branch = context.sessionManager.getBranch?.() ?? [];
|
|
32408
|
+
const resumed = branch.length > 0;
|
|
32409
|
+
if (resumed) {
|
|
32295
32410
|
await recoverRegisteredInterests();
|
|
32296
32411
|
}
|
|
32297
32412
|
await registerSession();
|
|
32413
|
+
if (resumed || previousSessionID !== "") {
|
|
32414
|
+
await reclaimHeldRoles(previousSessionID, branch, options.carryPreviousSessionRole ?? true);
|
|
32415
|
+
}
|
|
32298
32416
|
ensureHeartbeat(context);
|
|
32299
32417
|
};
|
|
32300
|
-
const
|
|
32301
|
-
const
|
|
32302
|
-
|
|
32303
|
-
|
|
32304
|
-
|
|
32305
|
-
|
|
32306
|
-
ensureHeartbeat(activeSessionContext);
|
|
32307
|
-
if (previousTopic !== undefined && previousTopic !== topic) {
|
|
32308
|
-
await client.unsubscribe({ sessionID, topics: [previousTopic] });
|
|
32309
|
-
}
|
|
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;
|
|
32310
32424
|
};
|
|
32311
32425
|
const bridge = legionRoleClaimBridge();
|
|
32312
32426
|
const claim = async (targetSessionID, role, callerContext) => {
|
|
@@ -32316,8 +32430,8 @@ function envoyExtension(pi) {
|
|
|
32316
32430
|
}
|
|
32317
32431
|
if (sessionID !== targetSessionID)
|
|
32318
32432
|
await establishSession(context);
|
|
32319
|
-
await setEnvoyRole(role);
|
|
32320
32433
|
await registerSession();
|
|
32434
|
+
await setEnvoyRole(role);
|
|
32321
32435
|
};
|
|
32322
32436
|
bridge.claim = claim;
|
|
32323
32437
|
bridge.ready.resolve(claim);
|
|
@@ -32359,7 +32473,8 @@ function envoyExtension(pi) {
|
|
|
32359
32473
|
return;
|
|
32360
32474
|
const previousID = sessionID;
|
|
32361
32475
|
try {
|
|
32362
|
-
|
|
32476
|
+
const carryPreviousSessionRole = reason !== "new" && reason !== "resume";
|
|
32477
|
+
await establishSession(context, { carryPreviousSessionRole });
|
|
32363
32478
|
} catch (error) {
|
|
32364
32479
|
context.ui.notify(`envoy: rebind failed (${messageFor(error)}); will recover on reconnect`, "warning");
|
|
32365
32480
|
}
|
|
@@ -32493,12 +32608,12 @@ Warnings: ${warnings.join("; ")}`, {
|
|
|
32493
32608
|
...claimedRoleTopic === undefined ? [] : [claimedRoleTopic]
|
|
32494
32609
|
]).filter((topic) => topic !== inbox);
|
|
32495
32610
|
const removed = targets.filter((topic) => closeIntentionally(topic) || topic === claimedRoleTopic);
|
|
32496
|
-
|
|
32497
|
-
claimedRoleTopic = undefined;
|
|
32498
|
-
}
|
|
32611
|
+
const releasingRole = claimedRoleTopic !== undefined && removed.includes(claimedRoleTopic);
|
|
32499
32612
|
const registrationError = removed.length === 0 ? undefined : await client.unsubscribe({ sessionID, topics: removed }).then(registerSession).then(() => {
|
|
32500
32613
|
return;
|
|
32501
32614
|
}, messageFor);
|
|
32615
|
+
if (releasingRole && registrationError === undefined)
|
|
32616
|
+
releaseClaimedRole();
|
|
32502
32617
|
return toolSuccess(`Unsubscribed: ${removed.join(", ") || "(none)"}`, {
|
|
32503
32618
|
removed,
|
|
32504
32619
|
...registrationError === undefined ? {} : { registrationError }
|
package/dist/legion.js
CHANGED
|
@@ -16115,7 +16115,7 @@ var require_mod4 = __commonJS(function(exports) {
|
|
|
16115
16115
|
import path4 from "path";
|
|
16116
16116
|
|
|
16117
16117
|
// ../contracts/src/dispatch-tools.ts
|
|
16118
|
-
var ISSUE_REFERENCE = "An issue is a native KEY or external owner/repo#n reference; an external reference creates its native issue on first use
|
|
16118
|
+
var ISSUE_REFERENCE = "An issue is a native KEY or external owner/repo#n reference; an external reference creates its native issue on first use in the repository's mapped project (DISPATCH_REPO_PROJECTS) or, failing that, the default project (DISPATCH_DEFAULT_PROJECT).";
|
|
16119
16119
|
var ASK_URGENCIES = ["low", "med", "high", "blocking"];
|
|
16120
16120
|
var DOC_EDIT_OPS = ["replace", "delete", "insert"];
|
|
16121
16121
|
var dispatchToolSpecs = [
|
|
@@ -32174,7 +32174,7 @@ async function ensureIssue(client, issueReference, actor) {
|
|
|
32174
32174
|
} catch (error) {
|
|
32175
32175
|
if (error instanceof DispatchServiceError && error.code === "PROJECT_UNMAPPED") {
|
|
32176
32176
|
const repository = issueReference.slice(0, issueReference.lastIndexOf("#"));
|
|
32177
|
-
throw new Error(`repository ${repository} is not mapped in DISPATCH_REPO_PROJECTS`);
|
|
32177
|
+
throw new Error(`repository ${repository} is not mapped in DISPATCH_REPO_PROJECTS and no DISPATCH_DEFAULT_PROJECT is configured`);
|
|
32178
32178
|
}
|
|
32179
32179
|
throw error;
|
|
32180
32180
|
}
|
|
@@ -32339,6 +32339,11 @@ var RoleWireSchema = object({
|
|
|
32339
32339
|
holder: string2(),
|
|
32340
32340
|
last_seen: number2().int()
|
|
32341
32341
|
});
|
|
32342
|
+
var RoleHeldWireSchema = object({
|
|
32343
|
+
error: string2(),
|
|
32344
|
+
role: string2(),
|
|
32345
|
+
holder: string2()
|
|
32346
|
+
});
|
|
32342
32347
|
var ErrorWireSchema = object({
|
|
32343
32348
|
error: string2(),
|
|
32344
32349
|
expected: array(string2()).optional()
|
|
@@ -32471,7 +32476,28 @@ function createEnvoyClient(config) {
|
|
|
32471
32476
|
unregisterSession: async (sessionID) => {
|
|
32472
32477
|
await request(`/v1/sessions/${encodeURIComponent(sessionID)}`, { method: "DELETE" });
|
|
32473
32478
|
},
|
|
32474
|
-
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
|
+
},
|
|
32475
32501
|
getRole: async (role) => RoleWireSchema.parse(JSON.parse(await request(`/v1/roles/${encodeURIComponent(role)}`, { method: "GET" }))),
|
|
32476
32502
|
listSessions: async (input = {}) => {
|
|
32477
32503
|
const search = new URLSearchParams;
|
|
@@ -32559,6 +32585,20 @@ async function copySessionID(copyToClipboard, sessionID) {
|
|
|
32559
32585
|
// extensions/envoy.ts
|
|
32560
32586
|
var codec2 = import_nats.StringCodec();
|
|
32561
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
|
+
}
|
|
32562
32602
|
var LEGION_ROLE_CLAIM_BRIDGE = Symbol.for("legion.pi-envoy.role-claim-bridge");
|
|
32563
32603
|
function legionRoleClaimBridge() {
|
|
32564
32604
|
const store = globalThis;
|
|
@@ -32756,8 +32796,81 @@ function envoyExtension(pi) {
|
|
|
32756
32796
|
await subscribe(topic);
|
|
32757
32797
|
}
|
|
32758
32798
|
};
|
|
32759
|
-
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;
|
|
32760
32871
|
const previousTopic = sessionID === "" ? undefined : agentSubject(sessionID);
|
|
32872
|
+
if (options.carryPreviousSessionRole === false)
|
|
32873
|
+
claimedRoleTopic = undefined;
|
|
32761
32874
|
sessionDirectory = context.cwd;
|
|
32762
32875
|
sessionID = context.sessionManager.getSessionId();
|
|
32763
32876
|
activeSessionContext = context;
|
|
@@ -32773,22 +32886,23 @@ function envoyExtension(pi) {
|
|
|
32773
32886
|
}
|
|
32774
32887
|
await ensureConnection();
|
|
32775
32888
|
await subscribe(currentTopic);
|
|
32776
|
-
|
|
32889
|
+
const branch = context.sessionManager.getBranch?.() ?? [];
|
|
32890
|
+
const resumed = branch.length > 0;
|
|
32891
|
+
if (resumed) {
|
|
32777
32892
|
await recoverRegisteredInterests();
|
|
32778
32893
|
}
|
|
32779
32894
|
await registerSession();
|
|
32895
|
+
if (resumed || previousSessionID !== "") {
|
|
32896
|
+
await reclaimHeldRoles(previousSessionID, branch, options.carryPreviousSessionRole ?? true);
|
|
32897
|
+
}
|
|
32780
32898
|
ensureHeartbeat(context);
|
|
32781
32899
|
};
|
|
32782
|
-
const
|
|
32783
|
-
const
|
|
32784
|
-
|
|
32785
|
-
|
|
32786
|
-
|
|
32787
|
-
|
|
32788
|
-
ensureHeartbeat(activeSessionContext);
|
|
32789
|
-
if (previousTopic !== undefined && previousTopic !== topic) {
|
|
32790
|
-
await client.unsubscribe({ sessionID, topics: [previousTopic] });
|
|
32791
|
-
}
|
|
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;
|
|
32792
32906
|
};
|
|
32793
32907
|
const bridge = legionRoleClaimBridge();
|
|
32794
32908
|
const claim = async (targetSessionID, role, callerContext) => {
|
|
@@ -32798,8 +32912,8 @@ function envoyExtension(pi) {
|
|
|
32798
32912
|
}
|
|
32799
32913
|
if (sessionID !== targetSessionID)
|
|
32800
32914
|
await establishSession(context);
|
|
32801
|
-
await setEnvoyRole(role);
|
|
32802
32915
|
await registerSession();
|
|
32916
|
+
await setEnvoyRole(role);
|
|
32803
32917
|
};
|
|
32804
32918
|
bridge.claim = claim;
|
|
32805
32919
|
bridge.ready.resolve(claim);
|
|
@@ -32841,7 +32955,8 @@ function envoyExtension(pi) {
|
|
|
32841
32955
|
return;
|
|
32842
32956
|
const previousID = sessionID;
|
|
32843
32957
|
try {
|
|
32844
|
-
|
|
32958
|
+
const carryPreviousSessionRole = reason !== "new" && reason !== "resume";
|
|
32959
|
+
await establishSession(context, { carryPreviousSessionRole });
|
|
32845
32960
|
} catch (error) {
|
|
32846
32961
|
context.ui.notify(`envoy: rebind failed (${messageFor(error)}); will recover on reconnect`, "warning");
|
|
32847
32962
|
}
|
|
@@ -32975,12 +33090,12 @@ Warnings: ${warnings.join("; ")}`, {
|
|
|
32975
33090
|
...claimedRoleTopic === undefined ? [] : [claimedRoleTopic]
|
|
32976
33091
|
]).filter((topic) => topic !== inbox);
|
|
32977
33092
|
const removed = targets.filter((topic) => closeIntentionally(topic) || topic === claimedRoleTopic);
|
|
32978
|
-
|
|
32979
|
-
claimedRoleTopic = undefined;
|
|
32980
|
-
}
|
|
33093
|
+
const releasingRole = claimedRoleTopic !== undefined && removed.includes(claimedRoleTopic);
|
|
32981
33094
|
const registrationError = removed.length === 0 ? undefined : await client.unsubscribe({ sessionID, topics: removed }).then(registerSession).then(() => {
|
|
32982
33095
|
return;
|
|
32983
33096
|
}, messageFor);
|
|
33097
|
+
if (releasingRole && registrationError === undefined)
|
|
33098
|
+
releaseClaimedRole();
|
|
32984
33099
|
return toolSuccess(`Unsubscribed: ${removed.join(", ") || "(none)"}`, {
|
|
32985
33100
|
removed,
|
|
32986
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
|