@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 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 = 120000;
19346
- var MIN_HEARTBEAT_MS = 25;
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 rawHeartbeatMs = Number(env.ENVOY_HEARTBEAT_MS);
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: Number.isFinite(rawHeartbeatMs) && rawHeartbeatMs > 0 ? Math.max(rawHeartbeatMs, MIN_HEARTBEAT_MS) : DEFAULT_HEARTBEAT_MS
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 = 120000;
33592
- var MIN_HEARTBEAT_MS = 25;
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 rawHeartbeatMs = Number(env.ENVOY_HEARTBEAT_MS);
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: Number.isFinite(rawHeartbeatMs) && rawHeartbeatMs > 0 ? Math.max(rawHeartbeatMs, MIN_HEARTBEAT_MS) : DEFAULT_HEARTBEAT_MS
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 { dispatch, envoy, notice: startNotice(config2, dispatch, envoy, options.nats !== false) };
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
- function serialQueue() {
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(task);
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
- connection = await import_nats2.connect({
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 active2 = await ensureConnection();
35986
- const subscription = active2.subscribe(agentStreamControlSubject(session));
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
- for (const [topic, hold] of abandonedHolds) {
36101
+ const registeringFor = sessionID;
36102
+ for (const [key, hold] of abandonedHolds) {
36051
36103
  if (hold.id !== sessionID)
36052
36104
  continue;
36053
- abandonedHolds.delete(topic);
36054
- pendingHandBacks.set(`${topic}@${sessionID}`, { topic, id: sessionID });
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 (claimedRoleTopic !== topic || !sessionOpen(id))
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 (claimedRoleTopic !== topic || !sessionOpen(id))
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 abandoned = abandonedHolds.get(topic);
36179
- if (abandoned === undefined || abandoned.id !== holder)
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(topic);
36273
+ abandonedHolds.delete(key);
36188
36274
  return false;
36189
36275
  }
36190
- abandonedHolds.set(topic, { id: abandoned.id, lastSeen: seen });
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.delete(topic);
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
- if (!waitingOnReap)
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
- await subscribe(agentSubject(host3));
36380
- await watchAgentStream(host3);
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 (hostSessionID() !== host3)
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 (host3, wanted) => {
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
- if (id === host3)
36432
- pendingHandBacks.set(key, { topic, id });
36433
- else
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
- if (!sessionOpen(host3))
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 (!sessionOpen(host3))
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 (!sessionOpen(host3))
36561
+ if (!owns())
36453
36562
  return false;
36454
36563
  }
36455
- if (await settleEndedHolds(host3, desired) && unheard && desired !== undefined) {
36564
+ if (await settleEndedHolds(desired, live) && unheard && desired !== undefined) {
36456
36565
  warnNoticesClosed(context, desired);
36457
36566
  }
36458
- if (!sessionOpen(host3))
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 (!sessionOpen(host3))
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 } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/pi-envoy",
3
- "version": "10.20.0",
3
+ "version": "10.22.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "omp": {