@sjawhar/pi-envoy 10.20.0 → 10.22.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/dispatch.js +14 -4
- package/dist/envoy.js +196 -85
- package/package.json +1 -1
package/dist/dispatch.js
CHANGED
|
@@ -19342,8 +19342,14 @@ function dispatchSettings(env, config2) {
|
|
|
19342
19342
|
|
|
19343
19343
|
// ../envoy-client/src/envoy-settings.ts
|
|
19344
19344
|
var DEFAULT_ENVOY_URL = "http://127.0.0.1:9020";
|
|
19345
|
-
var DEFAULT_HEARTBEAT_MS =
|
|
19346
|
-
var
|
|
19345
|
+
var DEFAULT_HEARTBEAT_MS = 90000;
|
|
19346
|
+
var MIN_PASS_DEADLINE_MS = 30000;
|
|
19347
|
+
var MIN_INTERVAL_MS = 25;
|
|
19348
|
+
var MAX_INTERVAL_MS = 2147483647;
|
|
19349
|
+
function namedInterval(raw) {
|
|
19350
|
+
const parsed = Number(raw);
|
|
19351
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
|
|
19352
|
+
}
|
|
19347
19353
|
function natsSeedFromEnvironment(env) {
|
|
19348
19354
|
const { NATS_NKEY_SEED_FILE: file2, NATS_NKEY_SEED: plain } = env;
|
|
19349
19355
|
if (file2 !== undefined) {
|
|
@@ -19430,13 +19436,17 @@ function envoySettings(env, config2, options = {}) {
|
|
|
19430
19436
|
inbound = { off: messageFor(error48) };
|
|
19431
19437
|
}
|
|
19432
19438
|
}
|
|
19433
|
-
const
|
|
19439
|
+
const namedHeartbeatMs = namedInterval(env.ENVOY_HEARTBEAT_MS);
|
|
19440
|
+
const heartbeatMs = namedHeartbeatMs === undefined ? DEFAULT_HEARTBEAT_MS : Math.min(Math.max(namedHeartbeatMs, MIN_INTERVAL_MS), MAX_INTERVAL_MS);
|
|
19441
|
+
const namedPassDeadlineMs = namedInterval(env.ENVOY_PASS_DEADLINE_MS);
|
|
19434
19442
|
return {
|
|
19435
19443
|
enabled: true,
|
|
19436
19444
|
url: normalizeEnvoyUrl(url2),
|
|
19437
19445
|
token,
|
|
19438
19446
|
inbound,
|
|
19439
|
-
heartbeatMs
|
|
19447
|
+
heartbeatMs,
|
|
19448
|
+
passDeadlineMs: Math.min(Math.max(namedPassDeadlineMs ?? MIN_PASS_DEADLINE_MS, heartbeatMs), MAX_INTERVAL_MS),
|
|
19449
|
+
passDeadlineRaisedFrom: namedPassDeadlineMs !== undefined && namedPassDeadlineMs < heartbeatMs ? namedPassDeadlineMs : undefined
|
|
19440
19450
|
};
|
|
19441
19451
|
}
|
|
19442
19452
|
function normalizeEnvoyUrl(value) {
|
package/dist/envoy.js
CHANGED
|
@@ -33567,29 +33567,16 @@ function writeSessionTitle(dir, title) {
|
|
|
33567
33567
|
writeAtomically(join3(dir, "title"), title);
|
|
33568
33568
|
}
|
|
33569
33569
|
|
|
33570
|
-
// ../envoy-client/src/nats-auth.ts
|
|
33571
|
-
var import_nats = __toESM(require_mod4(), 1);
|
|
33572
|
-
var seedKeys = import_nats.nkeys;
|
|
33573
|
-
function natsAuthOptions(seed) {
|
|
33574
|
-
if (seed === undefined)
|
|
33575
|
-
return {};
|
|
33576
|
-
const bytes = new TextEncoder().encode(seed.value);
|
|
33577
|
-
let publicKey;
|
|
33578
|
-
try {
|
|
33579
|
-
publicKey = seedKeys.fromSeed(bytes).getPublicKey();
|
|
33580
|
-
} catch (error48) {
|
|
33581
|
-
throw new Error(`${seed.source} does not hold a valid nkey seed: ${messageFor(error48)}`);
|
|
33582
|
-
}
|
|
33583
|
-
if (!publicKey.startsWith("U")) {
|
|
33584
|
-
throw new Error(`${seed.source} holds an nkey seed that is not a user's (public key ${publicKey})`);
|
|
33585
|
-
}
|
|
33586
|
-
return { authenticator: import_nats.nkeyAuthenticator(bytes) };
|
|
33587
|
-
}
|
|
33588
|
-
|
|
33589
33570
|
// ../envoy-client/src/envoy-settings.ts
|
|
33590
33571
|
var DEFAULT_ENVOY_URL = "http://127.0.0.1:9020";
|
|
33591
|
-
var DEFAULT_HEARTBEAT_MS =
|
|
33592
|
-
var
|
|
33572
|
+
var DEFAULT_HEARTBEAT_MS = 90000;
|
|
33573
|
+
var MIN_PASS_DEADLINE_MS = 30000;
|
|
33574
|
+
var MIN_INTERVAL_MS = 25;
|
|
33575
|
+
var MAX_INTERVAL_MS = 2147483647;
|
|
33576
|
+
function namedInterval(raw) {
|
|
33577
|
+
const parsed = Number(raw);
|
|
33578
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
|
|
33579
|
+
}
|
|
33593
33580
|
function natsSeedFromEnvironment(env) {
|
|
33594
33581
|
const { NATS_NKEY_SEED_FILE: file2, NATS_NKEY_SEED: plain } = env;
|
|
33595
33582
|
if (file2 !== undefined) {
|
|
@@ -33676,19 +33663,42 @@ function envoySettings(env, config2, options = {}) {
|
|
|
33676
33663
|
inbound = { off: messageFor(error48) };
|
|
33677
33664
|
}
|
|
33678
33665
|
}
|
|
33679
|
-
const
|
|
33666
|
+
const namedHeartbeatMs = namedInterval(env.ENVOY_HEARTBEAT_MS);
|
|
33667
|
+
const heartbeatMs = namedHeartbeatMs === undefined ? DEFAULT_HEARTBEAT_MS : Math.min(Math.max(namedHeartbeatMs, MIN_INTERVAL_MS), MAX_INTERVAL_MS);
|
|
33668
|
+
const namedPassDeadlineMs = namedInterval(env.ENVOY_PASS_DEADLINE_MS);
|
|
33680
33669
|
return {
|
|
33681
33670
|
enabled: true,
|
|
33682
33671
|
url: normalizeEnvoyUrl(url2),
|
|
33683
33672
|
token,
|
|
33684
33673
|
inbound,
|
|
33685
|
-
heartbeatMs
|
|
33674
|
+
heartbeatMs,
|
|
33675
|
+
passDeadlineMs: Math.min(Math.max(namedPassDeadlineMs ?? MIN_PASS_DEADLINE_MS, heartbeatMs), MAX_INTERVAL_MS),
|
|
33676
|
+
passDeadlineRaisedFrom: namedPassDeadlineMs !== undefined && namedPassDeadlineMs < heartbeatMs ? namedPassDeadlineMs : undefined
|
|
33686
33677
|
};
|
|
33687
33678
|
}
|
|
33688
33679
|
function normalizeEnvoyUrl(value) {
|
|
33689
33680
|
return value.replace(/\/+$/, "");
|
|
33690
33681
|
}
|
|
33691
33682
|
|
|
33683
|
+
// ../envoy-client/src/nats-auth.ts
|
|
33684
|
+
var import_nats = __toESM(require_mod4(), 1);
|
|
33685
|
+
var seedKeys = import_nats.nkeys;
|
|
33686
|
+
function natsAuthOptions(seed) {
|
|
33687
|
+
if (seed === undefined)
|
|
33688
|
+
return {};
|
|
33689
|
+
const bytes = new TextEncoder().encode(seed.value);
|
|
33690
|
+
let publicKey;
|
|
33691
|
+
try {
|
|
33692
|
+
publicKey = seedKeys.fromSeed(bytes).getPublicKey();
|
|
33693
|
+
} catch (error48) {
|
|
33694
|
+
throw new Error(`${seed.source} does not hold a valid nkey seed: ${messageFor(error48)}`);
|
|
33695
|
+
}
|
|
33696
|
+
if (!publicKey.startsWith("U")) {
|
|
33697
|
+
throw new Error(`${seed.source} holds an nkey seed that is not a user's (public key ${publicKey})`);
|
|
33698
|
+
}
|
|
33699
|
+
return { authenticator: import_nats.nkeyAuthenticator(bytes) };
|
|
33700
|
+
}
|
|
33701
|
+
|
|
33692
33702
|
// ../envoy-client/src/session-settings.ts
|
|
33693
33703
|
function sessionConfigFile(harness, env) {
|
|
33694
33704
|
try {
|
|
@@ -33727,6 +33737,9 @@ function startNotice(config2, dispatch, envoy, nats) {
|
|
|
33727
33737
|
if (envoy.enabled && nats && "off" in envoy.inbound) {
|
|
33728
33738
|
parts.push(`Envoy's inbound messages are off: ${envoy.inbound.off}; the Envoy tools still work`);
|
|
33729
33739
|
}
|
|
33740
|
+
if (envoy.enabled && envoy.passDeadlineRaisedFrom !== undefined) {
|
|
33741
|
+
parts.push(`Envoy's pass deadline is ${envoy.passDeadlineMs} ms, not the ${envoy.passDeadlineRaisedFrom} ms ENVOY_PASS_DEADLINE_MS names: a deadline inside one ${envoy.heartbeatMs} ms heartbeat interval would abandon every pass before the next tick, so the registration refresh the heartbeat posts for a stalled pass would never run`);
|
|
33742
|
+
}
|
|
33730
33743
|
return parts.length === 0 ? null : parts.join("; ");
|
|
33731
33744
|
}
|
|
33732
33745
|
function sessionSettings(file2, env, options) {
|
|
@@ -33737,7 +33750,11 @@ function sessionSettings(file2, env, options) {
|
|
|
33737
33750
|
const config2 = readEnvoyConfig(file2.path, env, options);
|
|
33738
33751
|
const dispatch = dispatchSettings(env, config2);
|
|
33739
33752
|
const envoy = envoySettings(env, config2, { nats: options.nats });
|
|
33740
|
-
return {
|
|
33753
|
+
return {
|
|
33754
|
+
dispatch,
|
|
33755
|
+
envoy,
|
|
33756
|
+
notice: startNotice(config2, dispatch, envoy, options.nats !== false)
|
|
33757
|
+
};
|
|
33741
33758
|
}
|
|
33742
33759
|
function sessionDispatchSettings(file2, env, options) {
|
|
33743
33760
|
if (file2.path === null)
|
|
@@ -35301,11 +35318,31 @@ import("@oh-my-pi/pi-coding-agent/extensibility/extensions/send-user-input-handl
|
|
|
35301
35318
|
var codec2 = import_nats2.StringCodec();
|
|
35302
35319
|
var NATS_RETRY_INTERVAL_MS = 15000;
|
|
35303
35320
|
var DELIVERY_PICTURES_TIMEOUT_MS = 20000;
|
|
35304
|
-
|
|
35321
|
+
var UNFENCED = () => true;
|
|
35322
|
+
|
|
35323
|
+
class AbandonedTaskError extends Error {
|
|
35324
|
+
}
|
|
35325
|
+
function serialQueue(deadlineMs) {
|
|
35305
35326
|
let tail = Promise.resolve();
|
|
35306
35327
|
return {
|
|
35307
35328
|
run(task) {
|
|
35308
|
-
const next = tail.then(
|
|
35329
|
+
const next = tail.then(() => new Promise((resolve3, reject) => {
|
|
35330
|
+
let abandoned = false;
|
|
35331
|
+
const taskDeadlineMs = deadlineMs();
|
|
35332
|
+
const timer = setTimeout(() => {
|
|
35333
|
+
abandoned = true;
|
|
35334
|
+
reject(new AbandonedTaskError(`a queued task did not settle within ${taskDeadlineMs}ms and was abandoned; the queue moved on`));
|
|
35335
|
+
}, taskDeadlineMs);
|
|
35336
|
+
(async () => task(() => !abandoned))().then((value) => {
|
|
35337
|
+
clearTimeout(timer);
|
|
35338
|
+
if (!abandoned)
|
|
35339
|
+
resolve3(value);
|
|
35340
|
+
}, (error48) => {
|
|
35341
|
+
clearTimeout(timer);
|
|
35342
|
+
if (!abandoned)
|
|
35343
|
+
reject(error48);
|
|
35344
|
+
});
|
|
35345
|
+
}));
|
|
35309
35346
|
tail = next.catch(() => {
|
|
35310
35347
|
return;
|
|
35311
35348
|
});
|
|
@@ -35456,6 +35493,7 @@ function envoyExtension(pi) {
|
|
|
35456
35493
|
}
|
|
35457
35494
|
return active;
|
|
35458
35495
|
};
|
|
35496
|
+
const passDeadlineMs = () => active?.settings.passDeadlineMs ?? MIN_PASS_DEADLINE_MS;
|
|
35459
35497
|
const subscriptions = new Map;
|
|
35460
35498
|
const delivered = createDeliveryDedupe();
|
|
35461
35499
|
let connection;
|
|
@@ -35485,7 +35523,7 @@ function envoyExtension(pi) {
|
|
|
35485
35523
|
let roleNoticesFor;
|
|
35486
35524
|
const roleNoticeSubjects = new Set;
|
|
35487
35525
|
let roleEnds = 0;
|
|
35488
|
-
const roleMoves = serialQueue();
|
|
35526
|
+
const roleMoves = serialQueue(passDeadlineMs);
|
|
35489
35527
|
const isRoleBound = (subject2) => roleNoticeSubjects.has(subject2);
|
|
35490
35528
|
const heldRole = () => claimedRoleTopic?.slice(ROLE_TOPIC_PREFIX.length);
|
|
35491
35529
|
let activeSessionContext;
|
|
@@ -35621,11 +35659,11 @@ function envoyExtension(pi) {
|
|
|
35621
35659
|
}
|
|
35622
35660
|
return inbound2;
|
|
35623
35661
|
};
|
|
35624
|
-
const ensureConnection = async () => {
|
|
35662
|
+
const ensureConnection = async (live = UNFENCED) => {
|
|
35625
35663
|
if (connection?.isClosed() === false)
|
|
35626
35664
|
return connection;
|
|
35627
35665
|
const { natsUrls, natsSeed } = requireInbound();
|
|
35628
|
-
|
|
35666
|
+
const dialled = await import_nats2.connect({
|
|
35629
35667
|
servers: [...natsUrls],
|
|
35630
35668
|
name: `omp-${sessionID || "unknown"}`,
|
|
35631
35669
|
...natsAuthOptions(natsSeed),
|
|
@@ -35633,6 +35671,13 @@ function envoyExtension(pi) {
|
|
|
35633
35671
|
maxReconnectAttempts: -1,
|
|
35634
35672
|
reconnectTimeWait: 2000
|
|
35635
35673
|
});
|
|
35674
|
+
if (!live() || shuttingDown || connection?.isClosed() === false) {
|
|
35675
|
+
await dialled.close().catch(() => {});
|
|
35676
|
+
if (!shuttingDown && connection?.isClosed() === false)
|
|
35677
|
+
return connection;
|
|
35678
|
+
throw new AbandonedTaskError(shuttingDown ? "the session shut down while its NATS dial was in flight; the connection it opened was closed" : "the pass that dialled NATS was abandoned at its deadline while the dial was in flight; the connection it opened was closed");
|
|
35679
|
+
}
|
|
35680
|
+
connection = dialled;
|
|
35636
35681
|
return connection;
|
|
35637
35682
|
};
|
|
35638
35683
|
const postDispatchReply = async (delivery, result, replySessionID = sessionID) => {
|
|
@@ -35868,11 +35913,13 @@ function envoyExtension(pi) {
|
|
|
35868
35913
|
};
|
|
35869
35914
|
retry(RESUBSCRIBE_DELAY_MS);
|
|
35870
35915
|
};
|
|
35871
|
-
const subscribe = async (topic) => {
|
|
35916
|
+
const subscribe = async (topic, live = UNFENCED) => {
|
|
35872
35917
|
const subjects = expandSubscriptionTopics([topic]).filter((candidate) => !subscriptions.has(candidate));
|
|
35873
35918
|
if (subjects.length === 0)
|
|
35874
35919
|
return false;
|
|
35875
|
-
const activeConnection = await ensureConnection();
|
|
35920
|
+
const activeConnection = await ensureConnection(live);
|
|
35921
|
+
if (!live())
|
|
35922
|
+
return false;
|
|
35876
35923
|
for (const subject2 of subjects) {
|
|
35877
35924
|
const subscription = activeConnection.subscribe(subject2);
|
|
35878
35925
|
intentionallyClosed.delete(subject2);
|
|
@@ -35971,7 +36018,7 @@ function envoyExtension(pi) {
|
|
|
35971
36018
|
});
|
|
35972
36019
|
}, RESUBSCRIBE_DELAY_MS);
|
|
35973
36020
|
};
|
|
35974
|
-
const watchAgentStream = async (session) => {
|
|
36021
|
+
const watchAgentStream = async (session, live = UNFENCED) => {
|
|
35975
36022
|
if (agentStreamControlSession === session && agentStreamControl !== undefined)
|
|
35976
36023
|
return;
|
|
35977
36024
|
agentStreamControl?.unsubscribe();
|
|
@@ -35982,8 +36029,10 @@ function envoyExtension(pi) {
|
|
|
35982
36029
|
agentStreamCommandsRefreshedOnWatchAt = -Infinity;
|
|
35983
36030
|
}
|
|
35984
36031
|
agentStreamControlSession = session;
|
|
35985
|
-
const
|
|
35986
|
-
|
|
36032
|
+
const connected = await ensureConnection(live);
|
|
36033
|
+
if (!live())
|
|
36034
|
+
return;
|
|
36035
|
+
const subscription = connected.subscribe(agentStreamControlSubject(session));
|
|
35987
36036
|
agentStreamControl = subscription;
|
|
35988
36037
|
pumpAgentStreamControl(subscription, session);
|
|
35989
36038
|
};
|
|
@@ -36046,12 +36095,15 @@ function envoyExtension(pi) {
|
|
|
36046
36095
|
closeIntentionally(subject2);
|
|
36047
36096
|
return false;
|
|
36048
36097
|
};
|
|
36098
|
+
let registeredFor = "";
|
|
36099
|
+
let registeredAt = -Infinity;
|
|
36049
36100
|
const registerSession = () => {
|
|
36050
|
-
|
|
36101
|
+
const registeringFor = sessionID;
|
|
36102
|
+
for (const [key, hold] of abandonedHolds) {
|
|
36051
36103
|
if (hold.id !== sessionID)
|
|
36052
36104
|
continue;
|
|
36053
|
-
abandonedHolds.delete(
|
|
36054
|
-
pendingHandBacks.set(
|
|
36105
|
+
abandonedHolds.delete(key);
|
|
36106
|
+
pendingHandBacks.set(key, { topic: hold.topic, id: sessionID });
|
|
36055
36107
|
}
|
|
36056
36108
|
return requireEnvoy().client.subscribe({
|
|
36057
36109
|
sessionID,
|
|
@@ -36067,17 +36119,23 @@ function envoyExtension(pi) {
|
|
|
36067
36119
|
capabilities: sideTurn(activeSessionContext) === undefined ? CAPABILITIES_WITHOUT_BTW : DELIVERY_CAPABILITIES,
|
|
36068
36120
|
driving: false,
|
|
36069
36121
|
selfSubscribed: true
|
|
36122
|
+
}).then((result) => {
|
|
36123
|
+
registeredFor = registeringFor;
|
|
36124
|
+
registeredAt = Date.now();
|
|
36125
|
+
return result;
|
|
36070
36126
|
});
|
|
36071
36127
|
};
|
|
36072
|
-
const reassertRoleNow = async (id, role, afterOutage, context) => {
|
|
36128
|
+
const reassertRoleNow = async (id, role, afterOutage, context, live) => {
|
|
36073
36129
|
const topic = ROLE_TOPIC_PREFIX + role;
|
|
36074
36130
|
const { client } = requireEnvoy();
|
|
36131
|
+
const owns = () => live() && sessionOpen(id);
|
|
36132
|
+
const stillHolds = () => claimedRoleTopic === topic;
|
|
36075
36133
|
const holder = await client.getRole(role).then((info) => info.holder, (error48) => {
|
|
36076
36134
|
if (error48 instanceof EnvoyApiError && error48.details.status === 404)
|
|
36077
36135
|
return;
|
|
36078
36136
|
throw error48;
|
|
36079
36137
|
});
|
|
36080
|
-
if (
|
|
36138
|
+
if (!owns() || !stillHolds())
|
|
36081
36139
|
return false;
|
|
36082
36140
|
let reason;
|
|
36083
36141
|
if (holder === id) {
|
|
@@ -36086,7 +36144,7 @@ function envoyExtension(pi) {
|
|
|
36086
36144
|
reason = "reregistered";
|
|
36087
36145
|
} else {
|
|
36088
36146
|
const result = await client.setRole({ sessionID: id, role, soft: true });
|
|
36089
|
-
if (
|
|
36147
|
+
if (!owns() || !stillHolds())
|
|
36090
36148
|
return false;
|
|
36091
36149
|
if (!result.claimed) {
|
|
36092
36150
|
endRole();
|
|
@@ -36118,23 +36176,48 @@ function envoyExtension(pi) {
|
|
|
36118
36176
|
if (heartbeatRegistered)
|
|
36119
36177
|
return;
|
|
36120
36178
|
let heartbeatOutageNotified = false;
|
|
36179
|
+
let stallNotified = false;
|
|
36121
36180
|
let registrationFailed = false;
|
|
36122
36181
|
let healing = false;
|
|
36182
|
+
const noteStall = (detail) => {
|
|
36183
|
+
if (stallNotified)
|
|
36184
|
+
return;
|
|
36185
|
+
stallNotified = true;
|
|
36186
|
+
logger3.warn("envoy: reconcile pass stalled; registration refreshed on its own", {
|
|
36187
|
+
detail,
|
|
36188
|
+
sessionID: context.sessionManager.getSessionId()
|
|
36189
|
+
});
|
|
36190
|
+
context.ui.notify(`envoy: a reconcile pass has stalled (${detail}); the session registration is being refreshed on its own, and recovers once the pass settles`, "warning");
|
|
36191
|
+
};
|
|
36123
36192
|
context.setInterval(() => {
|
|
36124
36193
|
if (context.sessionManager.getSessionId() === "")
|
|
36125
36194
|
return;
|
|
36126
|
-
if (healing)
|
|
36195
|
+
if (healing) {
|
|
36196
|
+
const refreshMs = requireEnvoy().settings.heartbeatMs;
|
|
36197
|
+
if (registeredFor === sessionID && Date.now() - registeredAt < refreshMs)
|
|
36198
|
+
return;
|
|
36199
|
+
noteStall("a pass has not settled a whole refresh interval after the last accepted registration");
|
|
36200
|
+
registerSession().catch(() => {
|
|
36201
|
+
return;
|
|
36202
|
+
});
|
|
36127
36203
|
return;
|
|
36204
|
+
}
|
|
36128
36205
|
healing = true;
|
|
36129
36206
|
const afterOutage = registrationFailed;
|
|
36130
36207
|
const settled = (decided) => {
|
|
36131
36208
|
if (decided)
|
|
36132
36209
|
registrationFailed = false;
|
|
36133
36210
|
heartbeatOutageNotified = false;
|
|
36211
|
+
stallNotified = false;
|
|
36134
36212
|
};
|
|
36135
36213
|
reconcile(context, { heartbeat: true, afterOutage }).then(settled, (error48) => {
|
|
36136
36214
|
if (error48 instanceof InboundError)
|
|
36137
36215
|
return settled(error48.decided);
|
|
36216
|
+
if (error48 instanceof AbandonedTaskError) {
|
|
36217
|
+
registrationFailed = true;
|
|
36218
|
+
noteStall(messageFor(error48));
|
|
36219
|
+
return;
|
|
36220
|
+
}
|
|
36138
36221
|
if (error48 instanceof RegistrationError)
|
|
36139
36222
|
registrationFailed = true;
|
|
36140
36223
|
throw error48 instanceof RegistrationError ? error48.cause : error48;
|
|
@@ -36149,12 +36232,12 @@ function envoyExtension(pi) {
|
|
|
36149
36232
|
}, requireEnvoy().settings.heartbeatMs);
|
|
36150
36233
|
heartbeatRegistered = true;
|
|
36151
36234
|
};
|
|
36152
|
-
const recoverRegisteredInterests = async () => {
|
|
36235
|
+
const recoverRegisteredInterests = async (live) => {
|
|
36153
36236
|
const { client } = requireEnvoy();
|
|
36154
36237
|
const registry2 = await client.getInterest(sessionID).catch(() => {
|
|
36155
36238
|
return;
|
|
36156
36239
|
});
|
|
36157
|
-
if (registry2 === undefined)
|
|
36240
|
+
if (registry2 === undefined || !live())
|
|
36158
36241
|
return;
|
|
36159
36242
|
const retired = publishedInterface?.roleClaim.slackRoleOwned === true ? registry2.topics.filter((topic) => slackTopicChannel(topic) !== undefined) : [];
|
|
36160
36243
|
if (retired.length > 0) {
|
|
@@ -36166,31 +36249,34 @@ function envoyExtension(pi) {
|
|
|
36166
36249
|
});
|
|
36167
36250
|
});
|
|
36168
36251
|
}
|
|
36252
|
+
if (!live())
|
|
36253
|
+
return;
|
|
36169
36254
|
for (const topic of registry2.topics) {
|
|
36170
36255
|
if (topic === agentSubject(sessionID) || topic.startsWith(ROLE_TOPIC_PREFIX))
|
|
36171
36256
|
continue;
|
|
36172
36257
|
if (retired.includes(topic))
|
|
36173
36258
|
continue;
|
|
36174
|
-
await subscribe(topic);
|
|
36259
|
+
await subscribe(topic, live);
|
|
36175
36260
|
}
|
|
36176
36261
|
};
|
|
36177
|
-
const observeAbandonedHold = async (topic, role, holder) => {
|
|
36178
|
-
const
|
|
36179
|
-
|
|
36262
|
+
const observeAbandonedHold = async (topic, role, holder, live) => {
|
|
36263
|
+
const key = `${topic}@${holder}`;
|
|
36264
|
+
const abandoned = abandonedHolds.get(key);
|
|
36265
|
+
if (abandoned === undefined)
|
|
36180
36266
|
return false;
|
|
36181
36267
|
const seen = await requireEnvoy().client.getRole(role).then((info) => info.holder === holder ? info.last_seen : undefined, () => {
|
|
36182
36268
|
return;
|
|
36183
36269
|
});
|
|
36184
|
-
if (seen === undefined)
|
|
36270
|
+
if (!live() || seen === undefined)
|
|
36185
36271
|
return true;
|
|
36186
36272
|
if (abandoned.lastSeen !== undefined && seen !== abandoned.lastSeen) {
|
|
36187
|
-
abandonedHolds.delete(
|
|
36273
|
+
abandonedHolds.delete(key);
|
|
36188
36274
|
return false;
|
|
36189
36275
|
}
|
|
36190
|
-
abandonedHolds.set(
|
|
36276
|
+
abandonedHolds.set(key, { topic, id: abandoned.id, lastSeen: seen });
|
|
36191
36277
|
return true;
|
|
36192
36278
|
};
|
|
36193
|
-
const setEnvoyRole = async (role, forSession, options = {}) => {
|
|
36279
|
+
const setEnvoyRole = async (role, forSession, live, options = {}) => {
|
|
36194
36280
|
if (!sessionOpen(forSession)) {
|
|
36195
36281
|
throw new SessionLeftError(`the session moved from ${forSession} to ${hostSessionID()} before its claim of role ${role} ran; nothing was claimed`);
|
|
36196
36282
|
}
|
|
@@ -36205,10 +36291,18 @@ function envoyExtension(pi) {
|
|
|
36205
36291
|
soft: options.soft,
|
|
36206
36292
|
previousSessionID: options.previousSessionID
|
|
36207
36293
|
});
|
|
36294
|
+
if (!live()) {
|
|
36295
|
+
if (result.claimed)
|
|
36296
|
+
pendingHandBacks.set(`${topic}@${id}`, { topic, id });
|
|
36297
|
+
throw new AbandonedTaskError(`the role move that claimed ${role} for session ${id} was abandoned at its deadline while the claim was in flight; ${result.claimed ? "the hold the listener granted is left for the next pass to hand back" : "nothing was claimed"}`);
|
|
36298
|
+
}
|
|
36208
36299
|
if (result.claimed) {
|
|
36209
36300
|
refusedRoles.delete(id);
|
|
36210
36301
|
holdUnconfirmed = false;
|
|
36211
|
-
abandonedHolds
|
|
36302
|
+
for (const [key, hold] of abandonedHolds) {
|
|
36303
|
+
if (hold.topic === topic)
|
|
36304
|
+
abandonedHolds.delete(key);
|
|
36305
|
+
}
|
|
36212
36306
|
for (const [key, hold] of pendingHandBacks) {
|
|
36213
36307
|
if (hold.topic === topic)
|
|
36214
36308
|
pendingHandBacks.delete(key);
|
|
@@ -36228,10 +36322,11 @@ function envoyExtension(pi) {
|
|
|
36228
36322
|
}
|
|
36229
36323
|
if (!result.claimed) {
|
|
36230
36324
|
endRole();
|
|
36231
|
-
const waitingOnReap = await observeAbandonedHold(topic, role, result.holder);
|
|
36232
|
-
|
|
36325
|
+
const waitingOnReap = await observeAbandonedHold(topic, role, result.holder, live);
|
|
36326
|
+
const notRecorded = !live() ? "the move was abandoned at its deadline before the refusal was recorded" : sessionOpen(id) ? undefined : `the session moved to ${hostSessionID()} before the refusal was recorded`;
|
|
36327
|
+
if (notRecorded === undefined && !waitingOnReap)
|
|
36233
36328
|
refusedRoles.set(id, role);
|
|
36234
|
-
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 });
|
|
36329
|
+
logger3.warn(notRecorded !== undefined ? `envoy: role held by another live session; ${notRecorded}` : 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 });
|
|
36235
36330
|
return false;
|
|
36236
36331
|
}
|
|
36237
36332
|
if (previousTopic !== undefined && previousTopic !== topic)
|
|
@@ -36340,12 +36435,13 @@ function envoyExtension(pi) {
|
|
|
36340
36435
|
knownSessions.add(host3);
|
|
36341
36436
|
};
|
|
36342
36437
|
const knownSessions = new Set;
|
|
36343
|
-
const passes = serialQueue();
|
|
36344
|
-
const reconcile = (context, cause = {}) => passes.run(() => reconcileNow(context, cause));
|
|
36438
|
+
const passes = serialQueue(passDeadlineMs);
|
|
36439
|
+
const reconcile = (context, cause = {}) => passes.run((live) => reconcileNow(context, cause, live));
|
|
36345
36440
|
const agentSessions = new Set;
|
|
36346
36441
|
let inboundOpenFor = "";
|
|
36347
|
-
const reconcileNow = async (context, cause) => {
|
|
36442
|
+
const reconcileNow = async (context, cause, live) => {
|
|
36348
36443
|
const host3 = context.sessionManager.getSessionId();
|
|
36444
|
+
const owns = () => live() && sessionOpen(host3);
|
|
36349
36445
|
noteMove(host3);
|
|
36350
36446
|
if (sessionID !== host3)
|
|
36351
36447
|
restoreLocalSessionState(context);
|
|
@@ -36375,27 +36471,39 @@ function envoyExtension(pi) {
|
|
|
36375
36471
|
if (inbound && inboundOpenFor !== host3) {
|
|
36376
36472
|
try {
|
|
36377
36473
|
agentSessions.add(host3);
|
|
36378
|
-
await ensureConnection();
|
|
36379
|
-
|
|
36380
|
-
|
|
36474
|
+
await ensureConnection(live);
|
|
36475
|
+
if (!owns())
|
|
36476
|
+
return false;
|
|
36477
|
+
await subscribe(agentSubject(host3), live);
|
|
36478
|
+
if (!owns())
|
|
36479
|
+
return false;
|
|
36480
|
+
await watchAgentStream(host3, live);
|
|
36481
|
+
if (!owns())
|
|
36482
|
+
return false;
|
|
36381
36483
|
if ((context.sessionManager.getBranch?.() ?? []).length > 0) {
|
|
36382
|
-
await recoverRegisteredInterests();
|
|
36484
|
+
await recoverRegisteredInterests(live);
|
|
36485
|
+
if (!owns())
|
|
36486
|
+
return false;
|
|
36383
36487
|
}
|
|
36384
36488
|
inboundOpenFor = host3;
|
|
36385
36489
|
} catch (error48) {
|
|
36386
36490
|
inboundFailure = error48;
|
|
36387
36491
|
}
|
|
36388
36492
|
}
|
|
36389
|
-
if (
|
|
36493
|
+
if (!owns())
|
|
36390
36494
|
return false;
|
|
36391
36495
|
const registration = await registerSession().then(() => {
|
|
36392
36496
|
return;
|
|
36393
36497
|
}, (error48) => ({ error: error48 }));
|
|
36498
|
+
if (!owns())
|
|
36499
|
+
return false;
|
|
36394
36500
|
if (registration === undefined) {
|
|
36395
36501
|
establishedSession = host3;
|
|
36396
36502
|
ensureHeartbeat(context);
|
|
36397
36503
|
}
|
|
36398
|
-
const decided = await roleMoves.run(() => settleRole(context, host3, cause, registration === undefined));
|
|
36504
|
+
const decided = await roleMoves.run((moveLive) => settleRole(context, host3, cause, registration === undefined, moveLive));
|
|
36505
|
+
if (!owns())
|
|
36506
|
+
return false;
|
|
36399
36507
|
if (registration !== undefined) {
|
|
36400
36508
|
throw new RegistrationError(messageFor(registration.error), { cause: registration.error });
|
|
36401
36509
|
}
|
|
@@ -36406,12 +36514,14 @@ function envoyExtension(pi) {
|
|
|
36406
36514
|
};
|
|
36407
36515
|
let roleSettledFor = "";
|
|
36408
36516
|
let lastHeard = "";
|
|
36409
|
-
const settleEndedHolds = async (
|
|
36517
|
+
const settleEndedHolds = async (wanted, live) => {
|
|
36410
36518
|
let restored = false;
|
|
36411
36519
|
const failed = [];
|
|
36412
36520
|
for (const [key, hold] of pendingHandBacks) {
|
|
36521
|
+
if (!live())
|
|
36522
|
+
break;
|
|
36413
36523
|
pendingHandBacks.delete(key);
|
|
36414
|
-
if (wanted !== undefined && hold.topic === ROLE_TOPIC_PREFIX + wanted) {
|
|
36524
|
+
if (wanted !== undefined && hold.topic === ROLE_TOPIC_PREFIX + wanted && sessionOpen(hold.id)) {
|
|
36415
36525
|
claimedRoleTopic = hold.topic;
|
|
36416
36526
|
claimedRoleSessionID = hold.id;
|
|
36417
36527
|
holdUnconfirmed = true;
|
|
@@ -36427,21 +36537,20 @@ function envoyExtension(pi) {
|
|
|
36427
36537
|
});
|
|
36428
36538
|
});
|
|
36429
36539
|
}
|
|
36430
|
-
for (const { key, topic, id } of failed)
|
|
36431
|
-
|
|
36432
|
-
|
|
36433
|
-
|
|
36434
|
-
abandonedHolds.set(topic, { id });
|
|
36435
|
-
}
|
|
36540
|
+
for (const { key, topic, id } of failed)
|
|
36541
|
+
abandonedHolds.set(key, { topic, id });
|
|
36542
|
+
if (!live())
|
|
36543
|
+
return false;
|
|
36436
36544
|
return restored;
|
|
36437
36545
|
};
|
|
36438
|
-
const settleRole = async (context, host3, cause, registered) => {
|
|
36439
|
-
|
|
36546
|
+
const settleRole = async (context, host3, cause, registered, live) => {
|
|
36547
|
+
const owns = () => live() && sessionOpen(host3);
|
|
36548
|
+
if (!owns())
|
|
36440
36549
|
return false;
|
|
36441
36550
|
const branch = context.sessionManager.getBranch?.() ?? [];
|
|
36442
36551
|
const readRows = roleSettledFor !== host3 && (branch.length > 0 || roleSettledFor !== "");
|
|
36443
36552
|
const desired = await desiredRole(host3, branch, readRows);
|
|
36444
|
-
if (!
|
|
36553
|
+
if (!owns())
|
|
36445
36554
|
return false;
|
|
36446
36555
|
const unheard = lastHeard !== host3;
|
|
36447
36556
|
const held = heldRole();
|
|
@@ -36449,13 +36558,13 @@ function envoyExtension(pi) {
|
|
|
36449
36558
|
const ended = await handBackRole();
|
|
36450
36559
|
if (ended !== undefined && unheard)
|
|
36451
36560
|
warnRoleEnded(context, host3, ended);
|
|
36452
|
-
if (!
|
|
36561
|
+
if (!owns())
|
|
36453
36562
|
return false;
|
|
36454
36563
|
}
|
|
36455
|
-
if (await settleEndedHolds(
|
|
36564
|
+
if (await settleEndedHolds(desired, live) && unheard && desired !== undefined) {
|
|
36456
36565
|
warnNoticesClosed(context, desired);
|
|
36457
36566
|
}
|
|
36458
|
-
if (!
|
|
36567
|
+
if (!owns())
|
|
36459
36568
|
return false;
|
|
36460
36569
|
if (!registered)
|
|
36461
36570
|
return false;
|
|
@@ -36465,7 +36574,7 @@ function envoyExtension(pi) {
|
|
|
36465
36574
|
}
|
|
36466
36575
|
if (!holdUnconfirmed && heldRole() === desired && claimedRoleSessionID === host3) {
|
|
36467
36576
|
roleSettledFor = host3;
|
|
36468
|
-
return cause.heartbeat === true ? reassertRoleNow(host3, desired, cause.afterOutage === true, context) : true;
|
|
36577
|
+
return cause.heartbeat === true ? reassertRoleNow(host3, desired, cause.afterOutage === true, context, live) : true;
|
|
36469
36578
|
}
|
|
36470
36579
|
if (!inbound) {
|
|
36471
36580
|
roleSettledFor = host3;
|
|
@@ -36479,18 +36588,18 @@ function envoyExtension(pi) {
|
|
|
36479
36588
|
});
|
|
36480
36589
|
if (holder !== undefined && holder !== host3 && lineage.includes(holder))
|
|
36481
36590
|
supersedes = holder;
|
|
36482
|
-
if (!
|
|
36591
|
+
if (!owns())
|
|
36483
36592
|
return false;
|
|
36484
36593
|
}
|
|
36485
36594
|
try {
|
|
36486
|
-
const claimed = await setEnvoyRole(desired, host3, {
|
|
36595
|
+
const claimed = await setEnvoyRole(desired, host3, live, {
|
|
36487
36596
|
soft: true,
|
|
36488
36597
|
previousSessionID: supersedes
|
|
36489
36598
|
});
|
|
36490
36599
|
if (claimed && unheard)
|
|
36491
36600
|
warnNoticesClosed(context, desired);
|
|
36492
36601
|
} catch (error48) {
|
|
36493
|
-
if (error48 instanceof SessionLeftError)
|
|
36602
|
+
if (error48 instanceof SessionLeftError || error48 instanceof AbandonedTaskError)
|
|
36494
36603
|
return false;
|
|
36495
36604
|
logger3.warn("envoy: role reclaim failed", {
|
|
36496
36605
|
role: desired,
|
|
@@ -36498,6 +36607,8 @@ function envoyExtension(pi) {
|
|
|
36498
36607
|
error: messageFor(error48)
|
|
36499
36608
|
});
|
|
36500
36609
|
}
|
|
36610
|
+
if (!owns())
|
|
36611
|
+
return false;
|
|
36501
36612
|
roleSettledFor = host3;
|
|
36502
36613
|
return true;
|
|
36503
36614
|
};
|
|
@@ -36530,7 +36641,7 @@ function envoyExtension(pi) {
|
|
|
36530
36641
|
if (establishedSession !== targetSessionID)
|
|
36531
36642
|
await establishForBridge(context);
|
|
36532
36643
|
await registerSession();
|
|
36533
|
-
await roleMoves.run(() => setEnvoyRole(role, targetSessionID));
|
|
36644
|
+
await roleMoves.run((live) => setEnvoyRole(role, targetSessionID, live));
|
|
36534
36645
|
};
|
|
36535
36646
|
const subscribeNotice = async (targetSessionID, topic, callerContext, whileHolding) => {
|
|
36536
36647
|
const context = callerContext ?? activeSessionContext;
|
|
@@ -36963,13 +37074,13 @@ Not delivered: that holder is the session this subagent sends as, and the listen
|
|
|
36963
37074
|
}
|
|
36964
37075
|
case EnvoyToolOperation.setRole: {
|
|
36965
37076
|
const role = stringFor(parameters, "role");
|
|
36966
|
-
const released = await roleMoves.run(async () => {
|
|
37077
|
+
const released = await roleMoves.run(async (live) => {
|
|
36967
37078
|
const held = heldRole();
|
|
36968
37079
|
const moving = held !== undefined && held !== role;
|
|
36969
37080
|
if (moving && roleNoticeSubjects.size > 0) {
|
|
36970
37081
|
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}`);
|
|
36971
37082
|
}
|
|
36972
|
-
await setEnvoyRole(role, sessionID);
|
|
37083
|
+
await setEnvoyRole(role, sessionID, live);
|
|
36973
37084
|
return moving ? held : undefined;
|
|
36974
37085
|
});
|
|
36975
37086
|
return toolSuccess(released === undefined ? `Now holding role: ${role}` : `Now holding role: ${role} (released role ${released})`, { role, ...released === undefined ? {} : { released } });
|