@sjawhar/pi-envoy 10.6.2 → 10.6.3
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/README.md +2 -1
- package/dist/envoy.js +501 -147
- package/dist/skills/envoy/SKILL.md +3 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,7 +12,8 @@ described in `packages/pi-shared/AGENTS.md`.
|
|
|
12
12
|
Normal topic subscriptions are direct NATS subscriptions owned by this extension. A role claim is
|
|
13
13
|
different: the listener arbitrates the core-NATS role lane for the current live holder, then sends
|
|
14
14
|
a receipt-backed request with the original role topic to the holder's direct agent subject.
|
|
15
|
-
`envoy_unsubscribe` manages only normal topic subscriptions; it does not relinquish a held role
|
|
15
|
+
`envoy_unsubscribe` manages only normal topic subscriptions; it does not relinquish a held role,
|
|
16
|
+
and it keeps open, listing them as kept, the subscriptions another plugin holds for that role.
|
|
16
17
|
The agent pump replies after accepting the envelope; without a receipt within two seconds the
|
|
17
18
|
listener emits a `delivery_failed` exception. Role messages are live only; they are not retained
|
|
18
19
|
for a later claimant.
|
package/dist/envoy.js
CHANGED
|
@@ -34054,7 +34054,7 @@ var envoyToolSpecs = [
|
|
|
34054
34054
|
},
|
|
34055
34055
|
{
|
|
34056
34056
|
name: "envoy_unsubscribe",
|
|
34057
|
-
description: "Unsubscribe this session from Envoy topics, or remove all current subscriptions if topics are omitted.",
|
|
34057
|
+
description: "Unsubscribe this session from Envoy topics, or remove all current subscriptions if topics are omitted. A subscription another plugin holds for this session's role stays, and the result lists it as kept.",
|
|
34058
34058
|
arguments: (schema) => ({ topics: schema.array(schema.string()).optional() }),
|
|
34059
34059
|
operation: EnvoyToolOperation.unsubscribe
|
|
34060
34060
|
},
|
|
@@ -34087,7 +34087,7 @@ var envoyToolSpecs = [
|
|
|
34087
34087
|
},
|
|
34088
34088
|
{
|
|
34089
34089
|
name: "envoy_role_set",
|
|
34090
|
-
description: "Set the current session as the holder of a named role. Messages published to notifications.role.<role> route to this session.",
|
|
34090
|
+
description: "Set the current session as the holder of a named role. Messages published to notifications.role.<role> route to this session. Moving to a different role is refused while another plugin holds subscriptions that last only as long as this session keeps its current role.",
|
|
34091
34091
|
arguments: (schema) => ({ role: schema.string() }),
|
|
34092
34092
|
operation: EnvoyToolOperation.setRole
|
|
34093
34093
|
},
|
|
@@ -35266,6 +35266,19 @@ import("@oh-my-pi/pi-coding-agent/extensibility/extensions/send-user-input-handl
|
|
|
35266
35266
|
var codec2 = import_nats2.StringCodec();
|
|
35267
35267
|
var NATS_RETRY_INTERVAL_MS = 15000;
|
|
35268
35268
|
var DELIVERY_PICTURES_TIMEOUT_MS = 20000;
|
|
35269
|
+
function serialQueue() {
|
|
35270
|
+
let tail = Promise.resolve();
|
|
35271
|
+
return {
|
|
35272
|
+
run(task) {
|
|
35273
|
+
const next = tail.then(task);
|
|
35274
|
+
tail = next.catch(() => {
|
|
35275
|
+
return;
|
|
35276
|
+
});
|
|
35277
|
+
return next;
|
|
35278
|
+
},
|
|
35279
|
+
settled: () => tail
|
|
35280
|
+
};
|
|
35281
|
+
}
|
|
35269
35282
|
function timedDispatchClient(timeoutMs) {
|
|
35270
35283
|
const config2 = activeDispatchSettings(sessionDispatchSettings(CONFIG_FILE, process.env, { cwd: process.cwd() }).dispatch);
|
|
35271
35284
|
return config2 === null ? undefined : new DispatchClient(config2.url, config2.token, fetch, AbortSignal.timeout(timeoutMs));
|
|
@@ -35332,6 +35345,20 @@ function isLegionManagedEntry(entry) {
|
|
|
35332
35345
|
return false;
|
|
35333
35346
|
return "session_id" in entry.data && typeof entry.data.session_id === "string";
|
|
35334
35347
|
}
|
|
35348
|
+
|
|
35349
|
+
class SessionLeftError extends Error {
|
|
35350
|
+
}
|
|
35351
|
+
|
|
35352
|
+
class RegistrationError extends Error {
|
|
35353
|
+
}
|
|
35354
|
+
|
|
35355
|
+
class InboundError extends Error {
|
|
35356
|
+
decided;
|
|
35357
|
+
constructor(message, decided, options) {
|
|
35358
|
+
super(message, options);
|
|
35359
|
+
this.decided = decided;
|
|
35360
|
+
}
|
|
35361
|
+
}
|
|
35335
35362
|
function isRoleClaimEntry(entry) {
|
|
35336
35363
|
if (typeof entry !== "object" || entry === null)
|
|
35337
35364
|
return false;
|
|
@@ -35401,10 +35428,31 @@ function envoyExtension(pi) {
|
|
|
35401
35428
|
let sessionID = "";
|
|
35402
35429
|
let recordedSessionKey;
|
|
35403
35430
|
let heartbeatRegistered = false;
|
|
35431
|
+
let announced;
|
|
35432
|
+
let runUnderAnnouncement;
|
|
35433
|
+
const continues = new Map;
|
|
35434
|
+
const continuedBy = (id) => {
|
|
35435
|
+
const lineage = [id];
|
|
35436
|
+
for (let at = continues.get(id);at !== undefined && !lineage.includes(at); ) {
|
|
35437
|
+
lineage.push(at);
|
|
35438
|
+
at = continues.get(at);
|
|
35439
|
+
}
|
|
35440
|
+
return lineage;
|
|
35441
|
+
};
|
|
35442
|
+
let establishedSession = "";
|
|
35443
|
+
const notifiedSessions = new Set;
|
|
35404
35444
|
let claimedRoleTopic;
|
|
35405
35445
|
let claimedRoleSessionID;
|
|
35446
|
+
let holdUnconfirmed = false;
|
|
35447
|
+
const pendingHandBacks = new Map;
|
|
35448
|
+
const abandonedHolds = new Map;
|
|
35449
|
+
const refusedRoles = new Map;
|
|
35450
|
+
let roleNoticesFor;
|
|
35406
35451
|
const roleNoticeSubjects = new Set;
|
|
35407
35452
|
let roleEnds = 0;
|
|
35453
|
+
const roleMoves = serialQueue();
|
|
35454
|
+
const isRoleBound = (subject2) => roleNoticeSubjects.has(subject2);
|
|
35455
|
+
const heldRole = () => claimedRoleTopic?.slice(ROLE_TOPIC_PREFIX.length);
|
|
35408
35456
|
let activeSessionContext;
|
|
35409
35457
|
const inbox = [];
|
|
35410
35458
|
let askAwareness = {
|
|
@@ -35741,9 +35789,25 @@ function envoyExtension(pi) {
|
|
|
35741
35789
|
subscriptions.delete(topic);
|
|
35742
35790
|
return true;
|
|
35743
35791
|
};
|
|
35792
|
+
const lineageOf = (id) => {
|
|
35793
|
+
const lineage = continuedBy(id);
|
|
35794
|
+
return announced !== undefined && announced.from !== id && carriesSession(announced.reason) && !lineage.includes(announced.from) ? [...lineage, announced.from] : lineage;
|
|
35795
|
+
};
|
|
35796
|
+
const replacedSessionTopic = (topic) => {
|
|
35797
|
+
if (!topic.startsWith(agentSubject("")))
|
|
35798
|
+
return false;
|
|
35799
|
+
const host3 = hostSessionID();
|
|
35800
|
+
return host3 !== "" && !lineageOf(host3).some((id) => topic === agentSubject(id));
|
|
35801
|
+
};
|
|
35744
35802
|
const pump = async (topic, subscription) => {
|
|
35745
35803
|
try {
|
|
35746
35804
|
for await (const message of subscription) {
|
|
35805
|
+
if (replacedSessionTopic(topic)) {
|
|
35806
|
+
logger3.warn("envoy: frame for a session a switch replaced left undelivered", {
|
|
35807
|
+
subject: message.subject
|
|
35808
|
+
});
|
|
35809
|
+
continue;
|
|
35810
|
+
}
|
|
35747
35811
|
try {
|
|
35748
35812
|
await deliver(message.subject, codec2.decode(message.data), message.reply ?? "");
|
|
35749
35813
|
} catch {}
|
|
@@ -35913,12 +35977,27 @@ function envoyExtension(pi) {
|
|
|
35913
35977
|
closeIntentionally(subject2);
|
|
35914
35978
|
roleNoticeSubjects.clear();
|
|
35915
35979
|
};
|
|
35916
|
-
const
|
|
35917
|
-
|
|
35918
|
-
|
|
35980
|
+
const handBackRole = async () => {
|
|
35981
|
+
const role = heldRole();
|
|
35982
|
+
const holdID = claimedRoleSessionID;
|
|
35983
|
+
const topic = claimedRoleTopic;
|
|
35984
|
+
if (role === undefined || topic === undefined)
|
|
35985
|
+
return;
|
|
35986
|
+
const pluginHeld = roleNoticeSubjects.size > 0;
|
|
35987
|
+
endRole();
|
|
35988
|
+
if (holdID !== undefined && holdID !== "") {
|
|
35989
|
+
await requireEnvoy().client.unsubscribe({ sessionID: holdID, topics: [topic] }).catch((error48) => logger3.warn("envoy: a role this process gave up was not handed back", {
|
|
35990
|
+
role,
|
|
35991
|
+
sessionID: holdID,
|
|
35992
|
+
error: messageFor(error48)
|
|
35993
|
+
}));
|
|
35994
|
+
}
|
|
35995
|
+
return { role, pluginHeld };
|
|
35919
35996
|
};
|
|
35997
|
+
const hostSessionID = () => activeSessionContext?.sessionManager.getSessionId() ?? sessionID;
|
|
35998
|
+
const sessionOpen = (id) => sessionID === id && hostSessionID() === id;
|
|
35920
35999
|
const roleSnapshot = (topic) => ({
|
|
35921
|
-
roleBound: expandSubscriptionTopics([topic]).some(
|
|
36000
|
+
roleBound: expandSubscriptionTopics([topic]).some(isRoleBound),
|
|
35922
36001
|
endsBefore: roleEnds
|
|
35923
36002
|
});
|
|
35924
36003
|
const subscribeUnlessRoleEnds = async (topic, snapshot) => {
|
|
@@ -35932,54 +36011,58 @@ function envoyExtension(pi) {
|
|
|
35932
36011
|
closeIntentionally(subject2);
|
|
35933
36012
|
return false;
|
|
35934
36013
|
};
|
|
35935
|
-
const registerSession = () =>
|
|
35936
|
-
|
|
35937
|
-
|
|
35938
|
-
|
|
35939
|
-
|
|
35940
|
-
|
|
35941
|
-
|
|
35942
|
-
|
|
35943
|
-
|
|
35944
|
-
|
|
35945
|
-
|
|
35946
|
-
|
|
35947
|
-
|
|
35948
|
-
|
|
35949
|
-
|
|
35950
|
-
|
|
35951
|
-
|
|
35952
|
-
|
|
35953
|
-
|
|
35954
|
-
|
|
35955
|
-
|
|
36014
|
+
const registerSession = () => {
|
|
36015
|
+
for (const [topic, hold] of abandonedHolds) {
|
|
36016
|
+
if (hold.id !== sessionID)
|
|
36017
|
+
continue;
|
|
36018
|
+
abandonedHolds.delete(topic);
|
|
36019
|
+
pendingHandBacks.set(`${topic}@${sessionID}`, { topic, id: sessionID });
|
|
36020
|
+
}
|
|
36021
|
+
return requireEnvoy().client.subscribe({
|
|
36022
|
+
sessionID,
|
|
36023
|
+
directory: sessionDirectory2,
|
|
36024
|
+
topics: [
|
|
36025
|
+
...new Set([
|
|
36026
|
+
agentSubject(sessionID),
|
|
36027
|
+
...[...subscriptions.keys()].filter((subject2) => !isRoleBound(subject2))
|
|
36028
|
+
])
|
|
36029
|
+
],
|
|
36030
|
+
port: 0,
|
|
36031
|
+
title: activeSessionContext?.sessionManager.getSessionName?.() ?? "",
|
|
36032
|
+
capabilities: sideTurn(activeSessionContext) === undefined ? CAPABILITIES_WITHOUT_BTW : DELIVERY_CAPABILITIES,
|
|
36033
|
+
driving: false,
|
|
36034
|
+
selfSubscribed: true
|
|
36035
|
+
});
|
|
36036
|
+
};
|
|
36037
|
+
const reassertRoleNow = async (id, role, afterOutage, context) => {
|
|
36038
|
+
const topic = ROLE_TOPIC_PREFIX + role;
|
|
35956
36039
|
const { client } = requireEnvoy();
|
|
35957
36040
|
const holder = await client.getRole(role).then((info) => info.holder, (error48) => {
|
|
35958
36041
|
if (error48 instanceof EnvoyApiError && error48.details.status === 404)
|
|
35959
36042
|
return;
|
|
35960
36043
|
throw error48;
|
|
35961
36044
|
});
|
|
36045
|
+
if (claimedRoleTopic !== topic || !sessionOpen(id))
|
|
36046
|
+
return false;
|
|
35962
36047
|
let reason;
|
|
35963
36048
|
if (holder === id) {
|
|
35964
36049
|
if (!afterOutage)
|
|
35965
|
-
return;
|
|
36050
|
+
return true;
|
|
35966
36051
|
reason = "reregistered";
|
|
35967
36052
|
} else {
|
|
35968
36053
|
const result = await client.setRole({ sessionID: id, role, soft: true });
|
|
35969
|
-
if (claimedRoleTopic !== topic ||
|
|
35970
|
-
|
|
35971
|
-
await client.unsubscribe({ sessionID: id, topics: [topic] });
|
|
35972
|
-
return;
|
|
35973
|
-
}
|
|
36054
|
+
if (claimedRoleTopic !== topic || !sessionOpen(id))
|
|
36055
|
+
return false;
|
|
35974
36056
|
if (!result.claimed) {
|
|
35975
36057
|
endRole();
|
|
36058
|
+
refusedRoles.set(id, role);
|
|
35976
36059
|
logger3.warn("envoy: role re-assertion refused; held by another live session", {
|
|
35977
36060
|
role,
|
|
35978
36061
|
sessionID: id,
|
|
35979
36062
|
holder: result.holder
|
|
35980
36063
|
});
|
|
35981
36064
|
context.ui.notify(`envoy: role ${role} is now held by session ${result.holder}; this session no longer holds it`, "warning");
|
|
35982
|
-
return;
|
|
36065
|
+
return true;
|
|
35983
36066
|
}
|
|
35984
36067
|
logger3.warn("envoy: role re-asserted after the listener lost the claim", {
|
|
35985
36068
|
role,
|
|
@@ -35990,10 +36073,11 @@ function envoyExtension(pi) {
|
|
|
35990
36073
|
}
|
|
35991
36074
|
const regained = legionRoleClaimBridge().regained;
|
|
35992
36075
|
if (regained === undefined)
|
|
35993
|
-
return;
|
|
36076
|
+
return true;
|
|
35994
36077
|
Promise.resolve().then(() => regained(role, reason)).catch((error48) => {
|
|
35995
36078
|
logger3.warn("envoy: role regain hook failed", { role, reason, error: messageFor(error48) });
|
|
35996
36079
|
});
|
|
36080
|
+
return true;
|
|
35997
36081
|
};
|
|
35998
36082
|
const ensureHeartbeat = (context) => {
|
|
35999
36083
|
if (heartbeatRegistered)
|
|
@@ -36002,24 +36086,23 @@ function envoyExtension(pi) {
|
|
|
36002
36086
|
let registrationFailed = false;
|
|
36003
36087
|
let healing = false;
|
|
36004
36088
|
context.setInterval(() => {
|
|
36005
|
-
|
|
36006
|
-
if (liveSessionID === "")
|
|
36089
|
+
if (context.sessionManager.getSessionId() === "")
|
|
36007
36090
|
return;
|
|
36008
|
-
const drifted = liveSessionID !== sessionID;
|
|
36009
36091
|
if (healing)
|
|
36010
36092
|
return;
|
|
36011
36093
|
healing = true;
|
|
36012
|
-
if (drifted) {
|
|
36013
|
-
bindDispatchSession(liveSessionID);
|
|
36014
|
-
procmgr.refreshShellConfigCache();
|
|
36015
|
-
}
|
|
36016
36094
|
const afterOutage = registrationFailed;
|
|
36017
|
-
|
|
36018
|
-
|
|
36019
|
-
|
|
36020
|
-
}).then(() => {
|
|
36021
|
-
registrationFailed = false;
|
|
36095
|
+
const settled = (decided) => {
|
|
36096
|
+
if (decided)
|
|
36097
|
+
registrationFailed = false;
|
|
36022
36098
|
heartbeatOutageNotified = false;
|
|
36099
|
+
};
|
|
36100
|
+
reconcile(context, { heartbeat: true, afterOutage }).then(settled, (error48) => {
|
|
36101
|
+
if (error48 instanceof InboundError)
|
|
36102
|
+
return settled(error48.decided);
|
|
36103
|
+
if (error48 instanceof RegistrationError)
|
|
36104
|
+
registrationFailed = true;
|
|
36105
|
+
throw error48 instanceof RegistrationError ? error48.cause : error48;
|
|
36023
36106
|
}).catch((error48) => {
|
|
36024
36107
|
if (heartbeatOutageNotified)
|
|
36025
36108
|
return;
|
|
@@ -36056,34 +36139,75 @@ function envoyExtension(pi) {
|
|
|
36056
36139
|
await subscribe(topic);
|
|
36057
36140
|
}
|
|
36058
36141
|
};
|
|
36059
|
-
const
|
|
36142
|
+
const observeAbandonedHold = async (topic, role, holder) => {
|
|
36143
|
+
const abandoned = abandonedHolds.get(topic);
|
|
36144
|
+
if (abandoned === undefined || abandoned.id !== holder)
|
|
36145
|
+
return false;
|
|
36146
|
+
const seen = await requireEnvoy().client.getRole(role).then((info) => info.holder === holder ? info.last_seen : undefined, () => {
|
|
36147
|
+
return;
|
|
36148
|
+
});
|
|
36149
|
+
if (seen === undefined)
|
|
36150
|
+
return true;
|
|
36151
|
+
if (abandoned.lastSeen !== undefined && seen !== abandoned.lastSeen) {
|
|
36152
|
+
abandonedHolds.delete(topic);
|
|
36153
|
+
return false;
|
|
36154
|
+
}
|
|
36155
|
+
abandonedHolds.set(topic, { id: abandoned.id, lastSeen: seen });
|
|
36156
|
+
return true;
|
|
36157
|
+
};
|
|
36158
|
+
const setEnvoyRole = async (role, forSession, options = {}) => {
|
|
36159
|
+
if (!sessionOpen(forSession)) {
|
|
36160
|
+
throw new SessionLeftError(`the session moved from ${forSession} to ${hostSessionID()} before its claim of role ${role} ran; nothing was claimed`);
|
|
36161
|
+
}
|
|
36060
36162
|
const topic = ROLE_TOPIC_PREFIX + role;
|
|
36061
36163
|
const previousTopic = claimedRoleTopic;
|
|
36164
|
+
const previousHoldID = claimedRoleSessionID;
|
|
36165
|
+
const id = forSession;
|
|
36062
36166
|
const { client } = requireEnvoy();
|
|
36063
36167
|
const result = await client.setRole({
|
|
36064
|
-
sessionID,
|
|
36168
|
+
sessionID: id,
|
|
36065
36169
|
role,
|
|
36066
36170
|
soft: options.soft,
|
|
36067
36171
|
previousSessionID: options.previousSessionID
|
|
36068
36172
|
});
|
|
36173
|
+
if (result.claimed) {
|
|
36174
|
+
refusedRoles.delete(id);
|
|
36175
|
+
holdUnconfirmed = false;
|
|
36176
|
+
abandonedHolds.delete(topic);
|
|
36177
|
+
for (const [key, hold] of pendingHandBacks) {
|
|
36178
|
+
if (hold.topic === topic)
|
|
36179
|
+
pendingHandBacks.delete(key);
|
|
36180
|
+
}
|
|
36181
|
+
}
|
|
36182
|
+
if (!sessionOpen(id)) {
|
|
36183
|
+
if (result.claimed) {
|
|
36184
|
+
if (previousTopic !== undefined && previousTopic !== topic)
|
|
36185
|
+
endRole();
|
|
36186
|
+
claimedRoleTopic = topic;
|
|
36187
|
+
claimedRoleSessionID = id;
|
|
36188
|
+
if (previousTopic !== undefined && previousTopic !== topic && previousHoldID) {
|
|
36189
|
+
await client.unsubscribe({ sessionID: previousHoldID, topics: [previousTopic] });
|
|
36190
|
+
}
|
|
36191
|
+
}
|
|
36192
|
+
throw new SessionLeftError(`the session moved from ${id} to ${hostSessionID()} while its claim of role ${role} was in flight; ${result.claimed ? "the next pass keeps the hold for the session the host is on, or hands it back" : "nothing was claimed"}`);
|
|
36193
|
+
}
|
|
36069
36194
|
if (!result.claimed) {
|
|
36070
|
-
logger3.warn("envoy: role held by another live session; not reclaimed", {
|
|
36071
|
-
role,
|
|
36072
|
-
sessionID,
|
|
36073
|
-
holder: result.holder
|
|
36074
|
-
});
|
|
36075
36195
|
endRole();
|
|
36196
|
+
const waitingOnReap = await observeAbandonedHold(topic, role, result.holder);
|
|
36197
|
+
if (!waitingOnReap)
|
|
36198
|
+
refusedRoles.set(id, role);
|
|
36199
|
+
logger3.warn(waitingOnReap ? "envoy: role still held by a session this process left; claiming again once the listener reaps it" : "envoy: role held by another live session; not reclaimed", { role, sessionID: id, holder: result.holder });
|
|
36076
36200
|
return false;
|
|
36077
36201
|
}
|
|
36078
36202
|
if (previousTopic !== undefined && previousTopic !== topic)
|
|
36079
36203
|
endRole();
|
|
36080
36204
|
claimedRoleTopic = topic;
|
|
36081
|
-
claimedRoleSessionID =
|
|
36205
|
+
claimedRoleSessionID = id;
|
|
36082
36206
|
pi.appendEntry(ROLE_CLAIM_ENTRY, { role });
|
|
36083
36207
|
if (activeSessionContext !== undefined)
|
|
36084
36208
|
ensureHeartbeat(activeSessionContext);
|
|
36085
|
-
if (previousTopic !== undefined && previousTopic !== topic) {
|
|
36086
|
-
await client.unsubscribe({ sessionID, topics: [previousTopic] });
|
|
36209
|
+
if (previousTopic !== undefined && previousTopic !== topic && previousHoldID) {
|
|
36210
|
+
await client.unsubscribe({ sessionID: previousHoldID, topics: [previousTopic] });
|
|
36087
36211
|
}
|
|
36088
36212
|
return true;
|
|
36089
36213
|
};
|
|
@@ -36096,73 +36220,271 @@ function envoyExtension(pi) {
|
|
|
36096
36220
|
}
|
|
36097
36221
|
return role;
|
|
36098
36222
|
};
|
|
36099
|
-
const
|
|
36100
|
-
|
|
36101
|
-
|
|
36102
|
-
|
|
36103
|
-
const remembered = transcriptClaimedRole(branch);
|
|
36104
|
-
if (remembered === null) {
|
|
36105
|
-
endRole();
|
|
36223
|
+
const desiredRole = async (id, branch, readRows) => {
|
|
36224
|
+
const want = (role) => refusedRoles.get(id) === role ? undefined : role;
|
|
36225
|
+
const recorded = transcriptClaimedRole(branch);
|
|
36226
|
+
if (recorded === null)
|
|
36106
36227
|
return;
|
|
36228
|
+
if (recorded !== undefined)
|
|
36229
|
+
return want(recorded);
|
|
36230
|
+
const held = heldRole();
|
|
36231
|
+
const holdID = claimedRoleSessionID;
|
|
36232
|
+
const lineage = continuedBy(id);
|
|
36233
|
+
if (held !== undefined && holdID !== undefined && lineage.includes(holdID))
|
|
36234
|
+
return want(held);
|
|
36235
|
+
if (!readRows)
|
|
36236
|
+
return;
|
|
36237
|
+
const { client } = requireEnvoy();
|
|
36238
|
+
for (const source of lineage) {
|
|
36239
|
+
const registry2 = await client.getInterest(source).catch(() => {
|
|
36240
|
+
return;
|
|
36241
|
+
});
|
|
36242
|
+
const topic = registry2?.topics.find((candidate) => candidate.startsWith(ROLE_TOPIC_PREFIX));
|
|
36243
|
+
if (topic !== undefined)
|
|
36244
|
+
return want(topic.slice(ROLE_TOPIC_PREFIX.length));
|
|
36107
36245
|
}
|
|
36108
|
-
|
|
36109
|
-
|
|
36110
|
-
|
|
36111
|
-
|
|
36112
|
-
|
|
36113
|
-
|
|
36114
|
-
|
|
36115
|
-
|
|
36116
|
-
|
|
36117
|
-
|
|
36118
|
-
|
|
36246
|
+
return;
|
|
36247
|
+
};
|
|
36248
|
+
const endUnwantedRoleNow = (context) => {
|
|
36249
|
+
const host3 = context.sessionManager.getSessionId();
|
|
36250
|
+
const held = heldRole();
|
|
36251
|
+
const topic = claimedRoleTopic;
|
|
36252
|
+
const holdID = claimedRoleSessionID;
|
|
36253
|
+
if (held === undefined || topic === undefined || holdID === undefined)
|
|
36254
|
+
return;
|
|
36255
|
+
const recorded = transcriptClaimedRole(context.sessionManager.getBranch?.() ?? []);
|
|
36256
|
+
const wanted = recorded === undefined ? continuedBy(host3).includes(holdID) : recorded === held;
|
|
36257
|
+
if (wanted)
|
|
36258
|
+
return;
|
|
36259
|
+
endRole();
|
|
36260
|
+
if (holdID !== "")
|
|
36261
|
+
pendingHandBacks.set(`${topic}@${holdID}`, { topic, id: holdID });
|
|
36262
|
+
};
|
|
36263
|
+
const tellIdChanged = (current) => {
|
|
36264
|
+
const previous = continues.get(current);
|
|
36265
|
+
if (previous === undefined || previous === current || notifiedSessions.has(current))
|
|
36266
|
+
return;
|
|
36267
|
+
notifiedSessions.add(current);
|
|
36268
|
+
pi.sendMessage({
|
|
36269
|
+
customType: "envoy-message",
|
|
36270
|
+
content: encode3({
|
|
36271
|
+
envoy: {
|
|
36272
|
+
notice: "session id changed",
|
|
36273
|
+
previous_session_id: previous,
|
|
36274
|
+
session_id: current,
|
|
36275
|
+
detail: "This conversation now lives under a new session id. Any identity you shared earlier (envoy_whoami output, session ids named in messages) is stale, and peers using the old id cannot reach you. Re-run envoy_whoami before identifying yourself and re-announce the new id to peers you already introduced yourself to."
|
|
36119
36276
|
}
|
|
36277
|
+
}),
|
|
36278
|
+
display: true,
|
|
36279
|
+
details: LOCAL_ENVOY_NOTICE
|
|
36280
|
+
}, { deliverAs: "steer", triggerTurn: false });
|
|
36281
|
+
};
|
|
36282
|
+
const noteMove = (host3, reason) => {
|
|
36283
|
+
if (host3 === "")
|
|
36284
|
+
return;
|
|
36285
|
+
if (announced !== undefined && host3 === announced.from) {
|
|
36286
|
+
if (reason !== undefined)
|
|
36287
|
+
announced = undefined;
|
|
36288
|
+
return;
|
|
36289
|
+
}
|
|
36290
|
+
const from = announced?.from ?? sessionID;
|
|
36291
|
+
const told = announced !== undefined || reason !== undefined;
|
|
36292
|
+
const kind = announced !== undefined ? announced.reason : reason?.kind;
|
|
36293
|
+
announced = undefined;
|
|
36294
|
+
if (from !== "" && from !== host3) {
|
|
36295
|
+
if (told) {
|
|
36296
|
+
if (carriesSession(kind))
|
|
36297
|
+
continues.set(host3, from);
|
|
36298
|
+
else
|
|
36299
|
+
continues.delete(host3);
|
|
36300
|
+
} else if (!knownSessions.has(host3)) {
|
|
36301
|
+
continues.set(host3, from);
|
|
36302
|
+
}
|
|
36303
|
+
knownSessions.add(from);
|
|
36304
|
+
}
|
|
36305
|
+
knownSessions.add(host3);
|
|
36306
|
+
};
|
|
36307
|
+
const knownSessions = new Set;
|
|
36308
|
+
const passes = serialQueue();
|
|
36309
|
+
const reconcile = (context, cause = {}) => passes.run(() => reconcileNow(context, cause));
|
|
36310
|
+
const agentSessions = new Set;
|
|
36311
|
+
let inboundOpenFor = "";
|
|
36312
|
+
const reconcileNow = async (context, cause) => {
|
|
36313
|
+
const host3 = context.sessionManager.getSessionId();
|
|
36314
|
+
noteMove(host3);
|
|
36315
|
+
if (sessionID !== host3)
|
|
36316
|
+
restoreLocalSessionState(context);
|
|
36317
|
+
if (establishedSession !== host3) {
|
|
36318
|
+
if ((process.env.DISPATCH_SESSION_ID ?? "") !== host3) {
|
|
36319
|
+
bindDispatchSession(host3);
|
|
36320
|
+
procmgr.refreshShellConfigCache();
|
|
36120
36321
|
}
|
|
36322
|
+
tellIdChanged(host3);
|
|
36323
|
+
if (host3 !== lastHeard)
|
|
36324
|
+
lastHeard = "";
|
|
36325
|
+
}
|
|
36326
|
+
for (const other of agentSessions) {
|
|
36327
|
+
if (other === host3)
|
|
36328
|
+
continue;
|
|
36329
|
+
closeIntentionally(agentSubject(other));
|
|
36330
|
+
agentSessions.delete(other);
|
|
36331
|
+
if (inboundOpenFor === other)
|
|
36332
|
+
inboundOpenFor = "";
|
|
36121
36333
|
}
|
|
36122
|
-
if (
|
|
36334
|
+
if (host3 === "") {
|
|
36335
|
+
establishedSession = host3;
|
|
36336
|
+
ensureHeartbeat(context);
|
|
36337
|
+
return true;
|
|
36338
|
+
}
|
|
36339
|
+
let inboundFailure;
|
|
36340
|
+
if (inbound && inboundOpenFor !== host3) {
|
|
36341
|
+
try {
|
|
36342
|
+
agentSessions.add(host3);
|
|
36343
|
+
await ensureConnection();
|
|
36344
|
+
await subscribe(agentSubject(host3));
|
|
36345
|
+
await watchAgentStream(host3);
|
|
36346
|
+
if ((context.sessionManager.getBranch?.() ?? []).length > 0) {
|
|
36347
|
+
await recoverRegisteredInterests();
|
|
36348
|
+
}
|
|
36349
|
+
inboundOpenFor = host3;
|
|
36350
|
+
} catch (error48) {
|
|
36351
|
+
inboundFailure = error48;
|
|
36352
|
+
}
|
|
36353
|
+
}
|
|
36354
|
+
if (hostSessionID() !== host3)
|
|
36355
|
+
return false;
|
|
36356
|
+
const registration = await registerSession().then(() => {
|
|
36123
36357
|
return;
|
|
36124
|
-
|
|
36125
|
-
|
|
36126
|
-
|
|
36127
|
-
|
|
36358
|
+
}, (error48) => ({ error: error48 }));
|
|
36359
|
+
if (registration === undefined) {
|
|
36360
|
+
establishedSession = host3;
|
|
36361
|
+
ensureHeartbeat(context);
|
|
36362
|
+
}
|
|
36363
|
+
const decided = await roleMoves.run(() => settleRole(context, host3, cause, registration === undefined));
|
|
36364
|
+
if (registration !== undefined) {
|
|
36365
|
+
throw new RegistrationError(messageFor(registration.error), { cause: registration.error });
|
|
36366
|
+
}
|
|
36367
|
+
if (inboundFailure !== undefined) {
|
|
36368
|
+
throw new InboundError(messageFor(inboundFailure), decided, { cause: inboundFailure });
|
|
36369
|
+
}
|
|
36370
|
+
return decided;
|
|
36371
|
+
};
|
|
36372
|
+
let roleSettledFor = "";
|
|
36373
|
+
let lastHeard = "";
|
|
36374
|
+
const settleEndedHolds = async (host3, wanted) => {
|
|
36375
|
+
let restored = false;
|
|
36376
|
+
const failed = [];
|
|
36377
|
+
for (const [key, hold] of pendingHandBacks) {
|
|
36378
|
+
pendingHandBacks.delete(key);
|
|
36379
|
+
if (wanted !== undefined && hold.topic === ROLE_TOPIC_PREFIX + wanted) {
|
|
36380
|
+
claimedRoleTopic = hold.topic;
|
|
36381
|
+
claimedRoleSessionID = hold.id;
|
|
36382
|
+
holdUnconfirmed = true;
|
|
36383
|
+
restored = true;
|
|
36384
|
+
continue;
|
|
36385
|
+
}
|
|
36386
|
+
await requireEnvoy().client.unsubscribe({ sessionID: hold.id, topics: [hold.topic] }).catch((error48) => {
|
|
36387
|
+
failed.push({ key, ...hold });
|
|
36388
|
+
logger3.warn("envoy: a role this process gave up was not handed back", {
|
|
36389
|
+
topic: hold.topic,
|
|
36390
|
+
sessionID: hold.id,
|
|
36391
|
+
error: messageFor(error48)
|
|
36392
|
+
});
|
|
36393
|
+
});
|
|
36394
|
+
}
|
|
36395
|
+
for (const { key, topic, id } of failed) {
|
|
36396
|
+
if (id === host3)
|
|
36397
|
+
pendingHandBacks.set(key, { topic, id });
|
|
36398
|
+
else
|
|
36399
|
+
abandonedHolds.set(topic, { id });
|
|
36400
|
+
}
|
|
36401
|
+
return restored;
|
|
36128
36402
|
};
|
|
36129
|
-
|
|
36130
|
-
|
|
36131
|
-
|
|
36132
|
-
if (options.carryPreviousSessionRole === false)
|
|
36133
|
-
endOutgoingRole();
|
|
36134
|
-
restoreLocalSessionState(context);
|
|
36403
|
+
const settleRole = async (context, host3, cause, registered) => {
|
|
36404
|
+
if (!sessionOpen(host3))
|
|
36405
|
+
return false;
|
|
36135
36406
|
const branch = context.sessionManager.getBranch?.() ?? [];
|
|
36136
|
-
const
|
|
36137
|
-
|
|
36138
|
-
|
|
36139
|
-
|
|
36140
|
-
|
|
36141
|
-
|
|
36407
|
+
const readRows = roleSettledFor !== host3 && (branch.length > 0 || roleSettledFor !== "");
|
|
36408
|
+
const desired = await desiredRole(host3, branch, readRows);
|
|
36409
|
+
if (!sessionOpen(host3))
|
|
36410
|
+
return false;
|
|
36411
|
+
const unheard = lastHeard !== host3;
|
|
36412
|
+
const held = heldRole();
|
|
36413
|
+
if (held !== undefined && held !== desired) {
|
|
36414
|
+
const ended = await handBackRole();
|
|
36415
|
+
if (ended !== undefined && unheard)
|
|
36416
|
+
warnRoleEnded(context, host3, ended);
|
|
36417
|
+
if (!sessionOpen(host3))
|
|
36418
|
+
return false;
|
|
36142
36419
|
}
|
|
36143
|
-
|
|
36144
|
-
|
|
36145
|
-
closeIntentionally(previousTopic);
|
|
36420
|
+
if (await settleEndedHolds(host3, desired) && unheard && desired !== undefined) {
|
|
36421
|
+
warnNoticesClosed(context, desired);
|
|
36146
36422
|
}
|
|
36147
|
-
|
|
36148
|
-
|
|
36149
|
-
|
|
36150
|
-
|
|
36151
|
-
|
|
36423
|
+
if (!sessionOpen(host3))
|
|
36424
|
+
return false;
|
|
36425
|
+
if (!registered)
|
|
36426
|
+
return false;
|
|
36427
|
+
if (desired === undefined) {
|
|
36428
|
+
roleSettledFor = host3;
|
|
36429
|
+
return true;
|
|
36152
36430
|
}
|
|
36153
|
-
|
|
36154
|
-
|
|
36155
|
-
|
|
36431
|
+
if (!holdUnconfirmed && heldRole() === desired && claimedRoleSessionID === host3) {
|
|
36432
|
+
roleSettledFor = host3;
|
|
36433
|
+
return cause.heartbeat === true ? reassertRoleNow(host3, desired, cause.afterOutage === true, context) : true;
|
|
36156
36434
|
}
|
|
36157
|
-
|
|
36435
|
+
if (!inbound) {
|
|
36436
|
+
roleSettledFor = host3;
|
|
36437
|
+
return true;
|
|
36438
|
+
}
|
|
36439
|
+
let supersedes = heldRole() === desired && claimedRoleSessionID !== undefined && claimedRoleSessionID !== "" ? claimedRoleSessionID : undefined;
|
|
36440
|
+
const lineage = continuedBy(host3);
|
|
36441
|
+
if (supersedes === undefined && lineage.length > 1) {
|
|
36442
|
+
const holder = await requireEnvoy().client.getRole(desired).then((info) => info.holder, () => {
|
|
36443
|
+
return;
|
|
36444
|
+
});
|
|
36445
|
+
if (holder !== undefined && holder !== host3 && lineage.includes(holder))
|
|
36446
|
+
supersedes = holder;
|
|
36447
|
+
if (!sessionOpen(host3))
|
|
36448
|
+
return false;
|
|
36449
|
+
}
|
|
36450
|
+
try {
|
|
36451
|
+
const claimed = await setEnvoyRole(desired, host3, {
|
|
36452
|
+
soft: true,
|
|
36453
|
+
previousSessionID: supersedes
|
|
36454
|
+
});
|
|
36455
|
+
if (claimed && unheard)
|
|
36456
|
+
warnNoticesClosed(context, desired);
|
|
36457
|
+
} catch (error48) {
|
|
36458
|
+
if (error48 instanceof SessionLeftError)
|
|
36459
|
+
return false;
|
|
36460
|
+
logger3.warn("envoy: role reclaim failed", {
|
|
36461
|
+
role: desired,
|
|
36462
|
+
sessionID: host3,
|
|
36463
|
+
error: messageFor(error48)
|
|
36464
|
+
});
|
|
36465
|
+
}
|
|
36466
|
+
roleSettledFor = host3;
|
|
36467
|
+
return true;
|
|
36158
36468
|
};
|
|
36159
|
-
const
|
|
36160
|
-
|
|
36161
|
-
|
|
36162
|
-
|
|
36469
|
+
const warnRoleEnded = (context, host3, ended) => {
|
|
36470
|
+
logger3.warn("envoy: role ended after a move with no session event", {
|
|
36471
|
+
role: ended.role,
|
|
36472
|
+
sessionID: host3
|
|
36163
36473
|
});
|
|
36164
|
-
|
|
36474
|
+
context.ui.notify(ended.pluginHeld ? `envoy: role ${ended.role} ended: the host moved to session ${host3} with no session event, and that session's records do not name it; the plugin that opened its notices claims it again on its next session switch or restart` : `envoy: role ${ended.role} ended: the host moved to session ${host3} with no session event, and that session's records do not name it; if this session should hold it, run envoy_role_set`, "warning");
|
|
36165
36475
|
};
|
|
36476
|
+
const warnNoticesClosed = (context, role) => {
|
|
36477
|
+
if (roleNoticesFor !== role || roleNoticeSubjects.size > 0)
|
|
36478
|
+
return;
|
|
36479
|
+
roleNoticesFor = undefined;
|
|
36480
|
+
context.ui.notify(`envoy: role ${role} is held again, but the notices a plugin opened for it stay closed until that plugin claims it again on its next session switch or restart`, "warning");
|
|
36481
|
+
};
|
|
36482
|
+
const establishForBridge = (context) => reconcile(context).then(() => {
|
|
36483
|
+
return;
|
|
36484
|
+
}, (error48) => {
|
|
36485
|
+
if (!(error48 instanceof InboundError))
|
|
36486
|
+
throw error48;
|
|
36487
|
+
});
|
|
36166
36488
|
const claim = async (targetSessionID, role, callerContext) => {
|
|
36167
36489
|
const context = callerContext ?? activeSessionContext;
|
|
36168
36490
|
if (context === undefined || context.sessionManager.getSessionId() !== targetSessionID) {
|
|
@@ -36170,33 +36492,49 @@ function envoyExtension(pi) {
|
|
|
36170
36492
|
}
|
|
36171
36493
|
markLegionManagedSession(targetSessionID);
|
|
36172
36494
|
requireInbound();
|
|
36173
|
-
if (
|
|
36174
|
-
await
|
|
36495
|
+
if (establishedSession !== targetSessionID)
|
|
36496
|
+
await establishForBridge(context);
|
|
36175
36497
|
await registerSession();
|
|
36176
|
-
await setEnvoyRole(role);
|
|
36498
|
+
await roleMoves.run(() => setEnvoyRole(role, targetSessionID));
|
|
36177
36499
|
};
|
|
36178
36500
|
const subscribeNotice = async (targetSessionID, topic, callerContext, whileHolding) => {
|
|
36179
36501
|
const context = callerContext ?? activeSessionContext;
|
|
36180
36502
|
if (context === undefined || context.sessionManager.getSessionId() !== targetSessionID) {
|
|
36181
36503
|
throw new Error(`Envoy has no active session for Legion notice subscription: ${targetSessionID}`);
|
|
36182
36504
|
}
|
|
36183
|
-
if (
|
|
36184
|
-
await
|
|
36505
|
+
if (establishedSession !== targetSessionID)
|
|
36506
|
+
await establishForBridge(context);
|
|
36507
|
+
const marked = [];
|
|
36185
36508
|
if (whileHolding !== undefined) {
|
|
36509
|
+
await roleMoves.settled();
|
|
36186
36510
|
if (claimedRoleTopic !== ROLE_TOPIC_PREFIX + whileHolding) {
|
|
36187
36511
|
throw new Error(`Envoy session ${targetSessionID} does not hold role ${whileHolding} for a notice subscription`);
|
|
36188
36512
|
}
|
|
36189
|
-
for (const subject2 of expandSubscriptionTopics([topic]))
|
|
36513
|
+
for (const subject2 of expandSubscriptionTopics([topic])) {
|
|
36514
|
+
if (isRoleBound(subject2))
|
|
36515
|
+
continue;
|
|
36190
36516
|
roleNoticeSubjects.add(subject2);
|
|
36517
|
+
marked.push(subject2);
|
|
36518
|
+
}
|
|
36519
|
+
roleNoticesFor = whileHolding;
|
|
36520
|
+
}
|
|
36521
|
+
const snapshot = roleSnapshot(topic);
|
|
36522
|
+
try {
|
|
36523
|
+
if (!await subscribeUnlessRoleEnds(topic, snapshot))
|
|
36524
|
+
return;
|
|
36525
|
+
} catch (error48) {
|
|
36526
|
+
if (roleEnds === snapshot.endsBefore) {
|
|
36527
|
+
for (const subject2 of marked)
|
|
36528
|
+
roleNoticeSubjects.delete(subject2);
|
|
36529
|
+
}
|
|
36530
|
+
throw error48;
|
|
36191
36531
|
}
|
|
36192
|
-
if (!await subscribeUnlessRoleEnds(topic, roleSnapshot(topic)))
|
|
36193
|
-
return;
|
|
36194
36532
|
await registerSession();
|
|
36195
36533
|
};
|
|
36196
36534
|
const claimInstance = {
|
|
36197
36535
|
claim,
|
|
36198
36536
|
subscribe: subscribeNotice,
|
|
36199
|
-
sessionID:
|
|
36537
|
+
sessionID: hostSessionID
|
|
36200
36538
|
};
|
|
36201
36539
|
if (publishedInterface !== undefined)
|
|
36202
36540
|
publishedInterface.roleClaim.instances.push(claimInstance);
|
|
@@ -36212,15 +36550,15 @@ function envoyExtension(pi) {
|
|
|
36212
36550
|
if (await isSubagent(context))
|
|
36213
36551
|
return;
|
|
36214
36552
|
bindDispatchSession(context.sessionManager.getSessionId());
|
|
36215
|
-
const previousSessionID = sessionID;
|
|
36216
36553
|
restoreLocalSessionState(context);
|
|
36554
|
+
lastHeard = context.sessionManager.getSessionId();
|
|
36217
36555
|
if (startNotice2 !== "")
|
|
36218
36556
|
context.ui.notify(`envoy: ${startNotice2}`, "warning");
|
|
36219
36557
|
if (!inbound)
|
|
36220
36558
|
return;
|
|
36221
36559
|
let established = false;
|
|
36222
36560
|
const establish = async () => {
|
|
36223
|
-
await
|
|
36561
|
+
await reconcile(context);
|
|
36224
36562
|
established = true;
|
|
36225
36563
|
};
|
|
36226
36564
|
try {
|
|
@@ -36240,36 +36578,24 @@ function envoyExtension(pi) {
|
|
|
36240
36578
|
}, NATS_RETRY_INTERVAL_MS);
|
|
36241
36579
|
}
|
|
36242
36580
|
});
|
|
36581
|
+
const carriesSession = (reason) => reason !== "new" && reason !== "resume";
|
|
36243
36582
|
const rebind = async (reason, context) => {
|
|
36244
36583
|
if (await isSubagent(context))
|
|
36245
36584
|
return;
|
|
36246
|
-
const
|
|
36247
|
-
|
|
36585
|
+
const host3 = context.sessionManager.getSessionId();
|
|
36586
|
+
noteMove(host3, { kind: reason });
|
|
36587
|
+
lastHeard = host3;
|
|
36588
|
+
refusedRoles.delete(host3);
|
|
36589
|
+
if (sessionID !== host3)
|
|
36590
|
+
restoreLocalSessionState(context);
|
|
36591
|
+
endUnwantedRoleNow(context);
|
|
36248
36592
|
if (!inbound)
|
|
36249
36593
|
return;
|
|
36250
36594
|
try {
|
|
36251
|
-
|
|
36252
|
-
await establishSession(context, { carryPreviousSessionRole }, previousID);
|
|
36595
|
+
await reconcile(context);
|
|
36253
36596
|
} catch (error48) {
|
|
36254
36597
|
context.ui.notify(`envoy: rebind failed (${messageFor(error48)}); will recover on reconnect`, "warning");
|
|
36255
36598
|
}
|
|
36256
|
-
if (previousID === "" || previousID === sessionID)
|
|
36257
|
-
return;
|
|
36258
|
-
if (reason === "new" || reason === "resume")
|
|
36259
|
-
return;
|
|
36260
|
-
pi.sendMessage({
|
|
36261
|
-
customType: "envoy-message",
|
|
36262
|
-
content: encode3({
|
|
36263
|
-
envoy: {
|
|
36264
|
-
notice: "session id changed",
|
|
36265
|
-
previous_session_id: previousID,
|
|
36266
|
-
session_id: sessionID,
|
|
36267
|
-
detail: "This conversation now lives under a new session id. Any identity you shared earlier (envoy_whoami output, session ids named in messages) is stale, and peers using the old id cannot reach you. Re-run envoy_whoami before identifying yourself and re-announce the new id to peers you already introduced yourself to."
|
|
36268
|
-
}
|
|
36269
|
-
}),
|
|
36270
|
-
display: true,
|
|
36271
|
-
details: LOCAL_ENVOY_NOTICE
|
|
36272
|
-
}, { deliverAs: "steer", triggerTurn: false });
|
|
36273
36599
|
};
|
|
36274
36600
|
const rebindDispatch = async (reason, context) => {
|
|
36275
36601
|
if (!await isSubagent(context))
|
|
@@ -36279,6 +36605,15 @@ function envoyExtension(pi) {
|
|
|
36279
36605
|
pi.on("session_switch", (event, context) => rebindDispatch(event.reason, context));
|
|
36280
36606
|
pi.on("session_branch", (_event, context) => rebindDispatch(undefined, context));
|
|
36281
36607
|
pi.on("session_tree", (_event, context) => rebind(undefined, context));
|
|
36608
|
+
const announce = (reason, context) => {
|
|
36609
|
+
const from = context.sessionManager.getSessionId();
|
|
36610
|
+
noteMove(from);
|
|
36611
|
+
if (from === "" || sessionID === "")
|
|
36612
|
+
return;
|
|
36613
|
+
announced = { from, reason };
|
|
36614
|
+
};
|
|
36615
|
+
pi.on("session_before_switch", async (event, context) => announce(event.reason, context));
|
|
36616
|
+
pi.on("session_before_branch", async (_event, context) => announce(undefined, context));
|
|
36282
36617
|
pi.on("session_shutdown", async (_event, context) => {
|
|
36283
36618
|
shuttingDown = true;
|
|
36284
36619
|
if (publishedInterface !== undefined) {
|
|
@@ -36358,8 +36693,9 @@ function envoyExtension(pi) {
|
|
|
36358
36693
|
}
|
|
36359
36694
|
return;
|
|
36360
36695
|
});
|
|
36361
|
-
pi.on("agent_start", async () => {
|
|
36696
|
+
pi.on("agent_start", async (_event, context) => {
|
|
36362
36697
|
runSeq += 1;
|
|
36698
|
+
runUnderAnnouncement = announced?.from === context.sessionManager.getSessionId() ? announced : undefined;
|
|
36363
36699
|
});
|
|
36364
36700
|
function armAskAwareness(id, asOf) {
|
|
36365
36701
|
askAwareness = {
|
|
@@ -36374,6 +36710,12 @@ function envoyExtension(pi) {
|
|
|
36374
36710
|
const id = context.sessionManager.getSessionId();
|
|
36375
36711
|
endInjectedUserTurns(sessionID);
|
|
36376
36712
|
const lastReply = event.messages?.findLast((message) => message.role === "assistant");
|
|
36713
|
+
if (runUnderAnnouncement !== undefined && event.willContinue !== true) {
|
|
36714
|
+
if (announced === runUnderAnnouncement && announced.from === id && lastReply !== undefined && lastReply.stopReason !== "aborted") {
|
|
36715
|
+
announced = undefined;
|
|
36716
|
+
}
|
|
36717
|
+
runUnderAnnouncement = undefined;
|
|
36718
|
+
}
|
|
36377
36719
|
const ask = sideTurn(context);
|
|
36378
36720
|
if (event.willContinue === true || lastReply?.stopReason !== "stop" || !context.hasUI || id === "" || ask === undefined || !checkOwed(id)) {
|
|
36379
36721
|
return;
|
|
@@ -36526,14 +36868,18 @@ Warnings: ${warnings.join("; ")}`, {
|
|
|
36526
36868
|
}
|
|
36527
36869
|
case EnvoyToolOperation.unsubscribe: {
|
|
36528
36870
|
const inbox2 = agentSubject(sessionID);
|
|
36529
|
-
const
|
|
36530
|
-
const
|
|
36871
|
+
const candidates = topicsFor(parameters, [...subscriptions.keys()]).filter((subject2) => subject2 !== inbox2);
|
|
36872
|
+
const kept = candidates.filter(isRoleBound);
|
|
36873
|
+
const removed = candidates.filter((subject2) => !isRoleBound(subject2)).filter(closeIntentionally);
|
|
36531
36874
|
const registryRemoved = removed.filter((topic) => !topic.startsWith(ROLE_TOPIC_PREFIX));
|
|
36532
36875
|
const registrationError = registryRemoved.length === 0 ? undefined : await client.unsubscribe({ sessionID, topics: registryRemoved }).then(registerSession).then(() => {
|
|
36533
36876
|
return;
|
|
36534
36877
|
}, messageFor);
|
|
36535
|
-
|
|
36878
|
+
const unsubscribed = `Unsubscribed: ${removed.join(", ") || "(none)"}`;
|
|
36879
|
+
return toolSuccess(kept.length === 0 ? unsubscribed : `${unsubscribed}
|
|
36880
|
+
Kept (they last while this session holds role ${heldRole()}): ${kept.join(", ")}`, {
|
|
36536
36881
|
removed,
|
|
36882
|
+
...kept.length === 0 ? {} : { kept },
|
|
36537
36883
|
...registrationError === undefined ? {} : { registrationError }
|
|
36538
36884
|
});
|
|
36539
36885
|
}
|
|
@@ -36582,8 +36928,16 @@ Not delivered: that holder is the session this subagent sends as, and the listen
|
|
|
36582
36928
|
}
|
|
36583
36929
|
case EnvoyToolOperation.setRole: {
|
|
36584
36930
|
const role = stringFor(parameters, "role");
|
|
36585
|
-
await
|
|
36586
|
-
|
|
36931
|
+
const released = await roleMoves.run(async () => {
|
|
36932
|
+
const held = heldRole();
|
|
36933
|
+
const moving = held !== undefined && held !== role;
|
|
36934
|
+
if (moving && roleNoticeSubjects.size > 0) {
|
|
36935
|
+
throw new Error(`This session holds role ${held}, and ${[...roleNoticeSubjects].join(", ")} lasts only while it does; envoy_role_set does not move it to ${role}`);
|
|
36936
|
+
}
|
|
36937
|
+
await setEnvoyRole(role, sessionID);
|
|
36938
|
+
return moving ? held : undefined;
|
|
36939
|
+
});
|
|
36940
|
+
return toolSuccess(released === undefined ? `Now holding role: ${role}` : `Now holding role: ${role} (released role ${released})`, { role, ...released === undefined ? {} : { released } });
|
|
36587
36941
|
}
|
|
36588
36942
|
case EnvoyToolOperation.getRole: {
|
|
36589
36943
|
const role = await client.getRole(stringFor(parameters, "role"));
|
|
@@ -200,6 +200,9 @@ fork's parent). If a different live session holds it — the parent of a `/fork`
|
|
|
200
200
|
the child, or a second process on the same transcript — the listener refuses atomically and the
|
|
201
201
|
resumed session logs a warning and holds nothing. Only an explicit `envoy_role_set` is
|
|
202
202
|
last-claim-wins. Re-running `envoy_role_set` for a role this session already holds is harmless.
|
|
203
|
+
Moving to a different role is refused while another plugin holds subscriptions that last only as
|
|
204
|
+
long as this session keeps its current role (the refusal names them), and `envoy_unsubscribe`
|
|
205
|
+
keeps those subscriptions open, listing them as kept.
|
|
203
206
|
|
|
204
207
|
## Legion role claims
|
|
205
208
|
|