@sjawhar/pi-envoy 10.21.0 → 10.23.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
@@ -13576,6 +13576,7 @@ var DispatchServerStartedPayloadSchema = exports_external.strictObject({
13576
13576
  commit: exports_external.string().min(1),
13577
13577
  started_at: exports_external.iso.datetime()
13578
13578
  });
13579
+ var DEFAULT_ISSUE_READ_EVENTS = 10;
13579
13580
  var DispatchEventSchema = exports_external.object({
13580
13581
  issue_key: exports_external.string().nullable(),
13581
13582
  artifact_id: exports_external.string().nullish(),
@@ -14353,13 +14354,15 @@ var dispatchToolSpecs = [
14353
14354
  {
14354
14355
  name: "dispatch_read",
14355
14356
  example: { issue: "DSP-1" },
14356
- description: "Read an issue or document summary, targeted ask, or targeted comment reply chain, or the conversation " + "a message belongs to. Do not use it for document contents; use dispatch doc-read instead. Supply ref, issue, " + "issue plus artifact (one of its documents), or project plus artifact; or message alone, which reads a human's direct message to this session and every " + "reply to it (they belong to no issue). A document summary names its issue, reference, versions and approval. " + "An anchored comment or ask also says where its quote sits, as `Position:`: the block's path from the top, " + "and in a table the row (0 is the header), the cells before the anchored one, and the column's header; " + "`Position: unavailable (<code>)` when Dispatch could not read the document: `DOC_SERVICE_UNAVAILABLE` " + "(try again shortly), `DOC_SCHEMA` (the document needs repair), `DOCUMENT_UNLOADABLE` (the document " + "needs a rebuild) or `INTERNAL`. " + "An issue read carries a `Progress:` line, `tasks 3/7, children 2/5`: its spec's task-list items " + "(`- [ ]` / `- [x]`, nested lists included) and its direct children, each done of total and each only " + "when it has any (`Progress: none` otherwise). " + "Every read ends with `Referenced by:` (what cites or hangs off this node, each with its dispatch:// address, " + "an excerpt, and when) and `Links:` (what it cites), so tracing provenance is one call. " + "The pictures the shown messages, asks and comments embed come back as images you see, newest first, at most 8 " + "and 10 MiB of them per read, each a PNG, JPEG, GIF or WebP of at most 3,750,000 bytes (5 MB of base64); a `Pictures:` section names each " + "one shown, in order, and the rest by reference, for dispatch doc-read. A picture this session was already shown is named, not sent again " + "(every request carries the session's history, and the provider refuses one over 32 MB); dispatch doc-read shows it again. " + OWNER_REFERENCE,
14357
+ description: "Read an issue or document summary, targeted ask, or targeted comment reply chain, or the conversation " + "a message belongs to. Do not use it for document contents; use dispatch doc-read instead. Supply ref, issue, " + "issue plus artifact (one of its documents), or project plus artifact; or message alone, which reads a human's direct message to this session and every " + "reply to it (they belong to no issue). A document summary names its issue, reference, versions and approval. " + "An anchored comment or ask also says where its quote sits, as `Position:`: the block's path from the top, " + "and in a table the row (0 is the header), the cells before the anchored one, and the column's header; " + "`Position: unavailable (<code>)` when Dispatch could not read the document: `DOC_SERVICE_UNAVAILABLE` " + "(try again shortly), `DOC_SCHEMA` (the document needs repair), `DOCUMENT_UNLOADABLE` (the document " + "needs a rebuild) or `INTERNAL`. " + "An issue read carries a `Progress:` line, `tasks 3/7, children 2/5`: its spec's task-list items " + "(`- [ ]` / `- [x]`, nested lists included) and its direct children, each done of total and each only " + "when it has any (`Progress: none` otherwise). " + `An issue read, and its dispatch://KEY/log, lists the issue's last ${DEFAULT_ISSUE_READ_EVENTS} events, oldest first; ` + "--events N lists the last N, and --all-events every one. " + "Every read ends with `Referenced by:` (what cites or hangs off this node, each with its dispatch:// address, " + "an excerpt, and when) and `Links:` (what it cites), so tracing provenance is one call. " + "The pictures the shown messages, asks and comments embed come back as images you see, newest first, at most 8 " + "and 10 MiB of them per read, each a PNG, JPEG, GIF or WebP of at most 3,750,000 bytes (5 MB of base64); a `Pictures:` section names each " + "one shown, in order, and the rest by reference, for dispatch doc-read. A picture this session was already shown is named, not sent again " + "(every request carries the session's history, and the provider refuses one over 32 MB); dispatch doc-read shows it again. " + OWNER_REFERENCE,
14357
14358
  arguments: (z2) => ({
14358
14359
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
14359
14360
  project: z2.string().describe("Project key owning the document.").optional(),
14360
14361
  artifact: z2.string().describe("Document artifact id, slug, or filename: one of the issue's, or a project document.").optional(),
14361
14362
  ref: z2.string().describe("Optional dispatch:// issue or document reference.").optional(),
14362
- message: z2.string().describe("A message id (uuid): reads the conversation it belongs to, the root message and every " + "reply. Name nothing else with it.").optional()
14363
+ message: z2.string().describe("A message id (uuid): reads the conversation it belongs to, the root message and every " + "reply. Name nothing else with it.").optional(),
14364
+ events: z2.number({ int: true, min: 1 }).describe(`How many of the issue's newest events an issue read or its dispatch://KEY/log lists, oldest first; default ${DEFAULT_ISSUE_READ_EVENTS}. Name no --all-events with it.`).optional(),
14365
+ all_events: z2.boolean().describe(`List every one of the issue's events, oldest first, on an issue read or its dispatch://KEY/log, in place of the last ${DEFAULT_ISSUE_READ_EVENTS}. Name no --events with it.`).optional()
14363
14366
  }),
14364
14367
  validation: readValidation
14365
14368
  },
@@ -15161,6 +15164,7 @@ function asErrorShape(value) {
15161
15164
  function isJson(response) {
15162
15165
  return response.headers.get("content-type")?.includes("application/json") ?? false;
15163
15166
  }
15167
+ var ISSUE_EVENTS_PAGE_MAX = 200;
15164
15168
  var DISPATCH_TOOL_DEADLINE_MS = 60000;
15165
15169
  function requestSignal(signal) {
15166
15170
  const deadline = AbortSignal.timeout(DISPATCH_TOOL_DEADLINE_MS);
@@ -15302,13 +15306,28 @@ class DispatchClient {
15302
15306
  async releaseIssueClaim(issue2, input = {}) {
15303
15307
  return this.#json("DELETE", ["api", "v1", "issues", await this.#resolveIssue(issue2), "claim"], input);
15304
15308
  }
15305
- async getIssueEvents(issue2, after = 0, limit = 200) {
15309
+ async getIssueEvents(issue2, after = 0, limit = ISSUE_EVENTS_PAGE_MAX) {
15306
15310
  return this.#json("GET", ["api", "v1", "issues", await this.#resolveIssue(issue2), "events"], undefined, { after, limit });
15307
15311
  }
15308
- async read(issueReference) {
15312
+ async read(issueReference, options = {}) {
15309
15313
  const issueKey = await this.#resolveIssue(issueReference);
15310
15314
  const issue2 = await this.getIssue(issueKey);
15311
- const events = await this.getIssueEvents(issueKey, Math.max(0, issue2.last_seq - 10), 10);
15315
+ const requested = options.events;
15316
+ const count = requested === undefined ? DEFAULT_ISSUE_READ_EVENTS : requested === "all" ? issue2.last_seq : Math.min(requested, issue2.last_seq);
15317
+ const events = [];
15318
+ let after = Math.max(0, issue2.last_seq - count);
15319
+ while (events.length < count) {
15320
+ const limit = Math.min(ISSUE_EVENTS_PAGE_MAX, count - events.length);
15321
+ const page = await this.getIssueEvents(issueKey, after, limit);
15322
+ events.push(...page);
15323
+ const last = page.at(-1);
15324
+ if (page.length < limit || last === undefined || events.length >= count)
15325
+ break;
15326
+ if (last.seq <= after) {
15327
+ throw new Error(`GET /api/v1/issues/${issueKey}/events?after=${after} answered no event after seq ${after}`);
15328
+ }
15329
+ after = last.seq;
15330
+ }
15312
15331
  return { issue: issue2, events };
15313
15332
  }
15314
15333
  async ask(issue2, input) {
@@ -16659,6 +16678,16 @@ function argumentProblems(tool, args) {
16659
16678
  if (message !== undefined && messageIdOf(message) === undefined) {
16660
16679
  problems.push("message must be a full message id (uuid) or a dispatch://KEY/message/<id> reference");
16661
16680
  }
16681
+ const events = optionalNumber(args, "events");
16682
+ const allEvents = optionalBoolean(args, "all_events") === true;
16683
+ if (events !== undefined && allEvents) {
16684
+ problems.push("--events and --all-events both say how many events the read lists: name one, --events N for the last N or --all-events for every one");
16685
+ }
16686
+ const count = events !== undefined ? "--events" : allEvents ? "--all-events" : undefined;
16687
+ const eventless = count === undefined ? undefined : readWithoutEvents(args);
16688
+ if (eventless !== undefined) {
16689
+ problems.push(`${count} lists an issue's events, which dispatch read prints only for an issue or its dispatch://KEY/log, not for ${eventless}: drop ${count}`);
16690
+ }
16662
16691
  break;
16663
16692
  }
16664
16693
  case "dispatch_doc_read": {
@@ -16670,6 +16699,27 @@ function argumentProblems(tool, args) {
16670
16699
  }
16671
16700
  return problems;
16672
16701
  }
16702
+ function readWithoutEvents(args) {
16703
+ if (optionalString(args, "message") !== undefined)
16704
+ return "a message's conversation";
16705
+ const ref = optionalString(args, "ref");
16706
+ switch (ref === undefined ? undefined : parseDispatchRef(ref)?.kind) {
16707
+ case "ask":
16708
+ return "an ask";
16709
+ case "comment":
16710
+ return "a comment";
16711
+ case "message":
16712
+ return "a message's conversation";
16713
+ case "children":
16714
+ return "the issue's children";
16715
+ case "log":
16716
+ return;
16717
+ }
16718
+ if (optionalString(args, "artifact") !== undefined || optionalString(args, "project") !== undefined) {
16719
+ return "a document summary";
16720
+ }
16721
+ return;
16722
+ }
16673
16723
  function dispatchRefFromUrl(value, serverUrl) {
16674
16724
  let url2;
16675
16725
  let origin;
@@ -18581,8 +18631,11 @@ ${trailer.join(`
18581
18631
  };
18582
18632
  }
18583
18633
  const issueKey = issue2();
18634
+ const readEvents = {
18635
+ events: optionalBoolean(args, "all_events") === true ? "all" : optionalNumber(args, "events")
18636
+ };
18584
18637
  if (ownerArguments.ref?.kind === "log") {
18585
- const read2 = await client.read(issueKey);
18638
+ const read2 = await client.read(issueKey, readEvents);
18586
18639
  const pictures3 = await picturesOf(read2.events.flatMap(eventTexts));
18587
18640
  return {
18588
18641
  text: [logSummary(read2.issue, read2.events), ...pictures3.lines].join(`
@@ -18592,13 +18645,13 @@ ${trailer.join(`
18592
18645
  };
18593
18646
  }
18594
18647
  if (ownerArguments.ref?.kind === "children") {
18595
- const read2 = await client.read(issueKey);
18648
+ const parent = await client.getIssue(issueKey);
18596
18649
  return {
18597
- text: childrenSummary(read2.issue),
18598
- details: { issue: read2.issue.key }
18650
+ text: childrenSummary(parent),
18651
+ details: { issue: parent.key }
18599
18652
  };
18600
18653
  }
18601
- const readPromise = client.read(issueKey);
18654
+ const readPromise = client.read(issueKey, readEvents);
18602
18655
  const referencesPromise = issueReferencesOrUnavailable(client, issueKey);
18603
18656
  const read = await readPromise;
18604
18657
  const [references, graph, titles, pictures2] = await Promise.all([
@@ -19342,8 +19395,14 @@ function dispatchSettings(env, config2) {
19342
19395
 
19343
19396
  // ../envoy-client/src/envoy-settings.ts
19344
19397
  var DEFAULT_ENVOY_URL = "http://127.0.0.1:9020";
19345
- var DEFAULT_HEARTBEAT_MS = 120000;
19346
- var MIN_HEARTBEAT_MS = 25;
19398
+ var DEFAULT_HEARTBEAT_MS = 90000;
19399
+ var MIN_PASS_DEADLINE_MS = 30000;
19400
+ var MIN_INTERVAL_MS = 25;
19401
+ var MAX_INTERVAL_MS = 2147483647;
19402
+ function namedInterval(raw) {
19403
+ const parsed = Number(raw);
19404
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
19405
+ }
19347
19406
  function natsSeedFromEnvironment(env) {
19348
19407
  const { NATS_NKEY_SEED_FILE: file2, NATS_NKEY_SEED: plain } = env;
19349
19408
  if (file2 !== undefined) {
@@ -19430,13 +19489,17 @@ function envoySettings(env, config2, options = {}) {
19430
19489
  inbound = { off: messageFor(error48) };
19431
19490
  }
19432
19491
  }
19433
- const rawHeartbeatMs = Number(env.ENVOY_HEARTBEAT_MS);
19492
+ const namedHeartbeatMs = namedInterval(env.ENVOY_HEARTBEAT_MS);
19493
+ const heartbeatMs = namedHeartbeatMs === undefined ? DEFAULT_HEARTBEAT_MS : Math.min(Math.max(namedHeartbeatMs, MIN_INTERVAL_MS), MAX_INTERVAL_MS);
19494
+ const namedPassDeadlineMs = namedInterval(env.ENVOY_PASS_DEADLINE_MS);
19434
19495
  return {
19435
19496
  enabled: true,
19436
19497
  url: normalizeEnvoyUrl(url2),
19437
19498
  token,
19438
19499
  inbound,
19439
- heartbeatMs: Number.isFinite(rawHeartbeatMs) && rawHeartbeatMs > 0 ? Math.max(rawHeartbeatMs, MIN_HEARTBEAT_MS) : DEFAULT_HEARTBEAT_MS
19500
+ heartbeatMs,
19501
+ passDeadlineMs: Math.min(Math.max(namedPassDeadlineMs ?? MIN_PASS_DEADLINE_MS, heartbeatMs), MAX_INTERVAL_MS),
19502
+ passDeadlineRaisedFrom: namedPassDeadlineMs !== undefined && namedPassDeadlineMs < heartbeatMs ? namedPassDeadlineMs : undefined
19440
19503
  };
19441
19504
  }
19442
19505
  function normalizeEnvoyUrl(value) {
package/dist/envoy.js CHANGED
@@ -29665,6 +29665,7 @@ var DispatchServerStartedPayloadSchema = exports_external.strictObject({
29665
29665
  commit: exports_external.string().min(1),
29666
29666
  started_at: exports_external.iso.datetime()
29667
29667
  });
29668
+ var DEFAULT_ISSUE_READ_EVENTS = 10;
29668
29669
  var DispatchEventSchema = exports_external.object({
29669
29670
  issue_key: exports_external.string().nullable(),
29670
29671
  artifact_id: exports_external.string().nullish(),
@@ -30340,13 +30341,15 @@ var dispatchToolSpecs = [
30340
30341
  {
30341
30342
  name: "dispatch_read",
30342
30343
  example: { issue: "DSP-1" },
30343
- description: "Read an issue or document summary, targeted ask, or targeted comment reply chain, or the conversation " + "a message belongs to. Do not use it for document contents; use dispatch doc-read instead. Supply ref, issue, " + "issue plus artifact (one of its documents), or project plus artifact; or message alone, which reads a human's direct message to this session and every " + "reply to it (they belong to no issue). A document summary names its issue, reference, versions and approval. " + "An anchored comment or ask also says where its quote sits, as `Position:`: the block's path from the top, " + "and in a table the row (0 is the header), the cells before the anchored one, and the column's header; " + "`Position: unavailable (<code>)` when Dispatch could not read the document: `DOC_SERVICE_UNAVAILABLE` " + "(try again shortly), `DOC_SCHEMA` (the document needs repair), `DOCUMENT_UNLOADABLE` (the document " + "needs a rebuild) or `INTERNAL`. " + "An issue read carries a `Progress:` line, `tasks 3/7, children 2/5`: its spec's task-list items " + "(`- [ ]` / `- [x]`, nested lists included) and its direct children, each done of total and each only " + "when it has any (`Progress: none` otherwise). " + "Every read ends with `Referenced by:` (what cites or hangs off this node, each with its dispatch:// address, " + "an excerpt, and when) and `Links:` (what it cites), so tracing provenance is one call. " + "The pictures the shown messages, asks and comments embed come back as images you see, newest first, at most 8 " + "and 10 MiB of them per read, each a PNG, JPEG, GIF or WebP of at most 3,750,000 bytes (5 MB of base64); a `Pictures:` section names each " + "one shown, in order, and the rest by reference, for dispatch doc-read. A picture this session was already shown is named, not sent again " + "(every request carries the session's history, and the provider refuses one over 32 MB); dispatch doc-read shows it again. " + OWNER_REFERENCE,
30344
+ description: "Read an issue or document summary, targeted ask, or targeted comment reply chain, or the conversation " + "a message belongs to. Do not use it for document contents; use dispatch doc-read instead. Supply ref, issue, " + "issue plus artifact (one of its documents), or project plus artifact; or message alone, which reads a human's direct message to this session and every " + "reply to it (they belong to no issue). A document summary names its issue, reference, versions and approval. " + "An anchored comment or ask also says where its quote sits, as `Position:`: the block's path from the top, " + "and in a table the row (0 is the header), the cells before the anchored one, and the column's header; " + "`Position: unavailable (<code>)` when Dispatch could not read the document: `DOC_SERVICE_UNAVAILABLE` " + "(try again shortly), `DOC_SCHEMA` (the document needs repair), `DOCUMENT_UNLOADABLE` (the document " + "needs a rebuild) or `INTERNAL`. " + "An issue read carries a `Progress:` line, `tasks 3/7, children 2/5`: its spec's task-list items " + "(`- [ ]` / `- [x]`, nested lists included) and its direct children, each done of total and each only " + "when it has any (`Progress: none` otherwise). " + `An issue read, and its dispatch://KEY/log, lists the issue's last ${DEFAULT_ISSUE_READ_EVENTS} events, oldest first; ` + "--events N lists the last N, and --all-events every one. " + "Every read ends with `Referenced by:` (what cites or hangs off this node, each with its dispatch:// address, " + "an excerpt, and when) and `Links:` (what it cites), so tracing provenance is one call. " + "The pictures the shown messages, asks and comments embed come back as images you see, newest first, at most 8 " + "and 10 MiB of them per read, each a PNG, JPEG, GIF or WebP of at most 3,750,000 bytes (5 MB of base64); a `Pictures:` section names each " + "one shown, in order, and the rest by reference, for dispatch doc-read. A picture this session was already shown is named, not sent again " + "(every request carries the session's history, and the provider refuses one over 32 MB); dispatch doc-read shows it again. " + OWNER_REFERENCE,
30344
30345
  arguments: (z2) => ({
30345
30346
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
30346
30347
  project: z2.string().describe("Project key owning the document.").optional(),
30347
30348
  artifact: z2.string().describe("Document artifact id, slug, or filename: one of the issue's, or a project document.").optional(),
30348
30349
  ref: z2.string().describe("Optional dispatch:// issue or document reference.").optional(),
30349
- message: z2.string().describe("A message id (uuid): reads the conversation it belongs to, the root message and every " + "reply. Name nothing else with it.").optional()
30350
+ message: z2.string().describe("A message id (uuid): reads the conversation it belongs to, the root message and every " + "reply. Name nothing else with it.").optional(),
30351
+ events: z2.number({ int: true, min: 1 }).describe(`How many of the issue's newest events an issue read or its dispatch://KEY/log lists, oldest first; default ${DEFAULT_ISSUE_READ_EVENTS}. Name no --all-events with it.`).optional(),
30352
+ all_events: z2.boolean().describe(`List every one of the issue's events, oldest first, on an issue read or its dispatch://KEY/log, in place of the last ${DEFAULT_ISSUE_READ_EVENTS}. Name no --events with it.`).optional()
30350
30353
  }),
30351
30354
  validation: readValidation
30352
30355
  },
@@ -32705,6 +32708,7 @@ function asErrorShape(value) {
32705
32708
  function isJson(response) {
32706
32709
  return response.headers.get("content-type")?.includes("application/json") ?? false;
32707
32710
  }
32711
+ var ISSUE_EVENTS_PAGE_MAX = 200;
32708
32712
  var DISPATCH_TOOL_DEADLINE_MS = 60000;
32709
32713
  function requestSignal(signal) {
32710
32714
  const deadline = AbortSignal.timeout(DISPATCH_TOOL_DEADLINE_MS);
@@ -32846,13 +32850,28 @@ class DispatchClient {
32846
32850
  async releaseIssueClaim(issue2, input = {}) {
32847
32851
  return this.#json("DELETE", ["api", "v1", "issues", await this.#resolveIssue(issue2), "claim"], input);
32848
32852
  }
32849
- async getIssueEvents(issue2, after = 0, limit = 200) {
32853
+ async getIssueEvents(issue2, after = 0, limit = ISSUE_EVENTS_PAGE_MAX) {
32850
32854
  return this.#json("GET", ["api", "v1", "issues", await this.#resolveIssue(issue2), "events"], undefined, { after, limit });
32851
32855
  }
32852
- async read(issueReference) {
32856
+ async read(issueReference, options = {}) {
32853
32857
  const issueKey = await this.#resolveIssue(issueReference);
32854
32858
  const issue2 = await this.getIssue(issueKey);
32855
- const events = await this.getIssueEvents(issueKey, Math.max(0, issue2.last_seq - 10), 10);
32859
+ const requested = options.events;
32860
+ const count = requested === undefined ? DEFAULT_ISSUE_READ_EVENTS : requested === "all" ? issue2.last_seq : Math.min(requested, issue2.last_seq);
32861
+ const events = [];
32862
+ let after = Math.max(0, issue2.last_seq - count);
32863
+ while (events.length < count) {
32864
+ const limit = Math.min(ISSUE_EVENTS_PAGE_MAX, count - events.length);
32865
+ const page = await this.getIssueEvents(issueKey, after, limit);
32866
+ events.push(...page);
32867
+ const last = page.at(-1);
32868
+ if (page.length < limit || last === undefined || events.length >= count)
32869
+ break;
32870
+ if (last.seq <= after) {
32871
+ throw new Error(`GET /api/v1/issues/${issueKey}/events?after=${after} answered no event after seq ${after}`);
32872
+ }
32873
+ after = last.seq;
32874
+ }
32856
32875
  return { issue: issue2, events };
32857
32876
  }
32858
32877
  async ask(issue2, input) {
@@ -33567,29 +33586,16 @@ function writeSessionTitle(dir, title) {
33567
33586
  writeAtomically(join3(dir, "title"), title);
33568
33587
  }
33569
33588
 
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
33589
  // ../envoy-client/src/envoy-settings.ts
33590
33590
  var DEFAULT_ENVOY_URL = "http://127.0.0.1:9020";
33591
- var DEFAULT_HEARTBEAT_MS = 120000;
33592
- var MIN_HEARTBEAT_MS = 25;
33591
+ var DEFAULT_HEARTBEAT_MS = 90000;
33592
+ var MIN_PASS_DEADLINE_MS = 30000;
33593
+ var MIN_INTERVAL_MS = 25;
33594
+ var MAX_INTERVAL_MS = 2147483647;
33595
+ function namedInterval(raw) {
33596
+ const parsed = Number(raw);
33597
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
33598
+ }
33593
33599
  function natsSeedFromEnvironment(env) {
33594
33600
  const { NATS_NKEY_SEED_FILE: file2, NATS_NKEY_SEED: plain } = env;
33595
33601
  if (file2 !== undefined) {
@@ -33676,19 +33682,42 @@ function envoySettings(env, config2, options = {}) {
33676
33682
  inbound = { off: messageFor(error48) };
33677
33683
  }
33678
33684
  }
33679
- const rawHeartbeatMs = Number(env.ENVOY_HEARTBEAT_MS);
33685
+ const namedHeartbeatMs = namedInterval(env.ENVOY_HEARTBEAT_MS);
33686
+ const heartbeatMs = namedHeartbeatMs === undefined ? DEFAULT_HEARTBEAT_MS : Math.min(Math.max(namedHeartbeatMs, MIN_INTERVAL_MS), MAX_INTERVAL_MS);
33687
+ const namedPassDeadlineMs = namedInterval(env.ENVOY_PASS_DEADLINE_MS);
33680
33688
  return {
33681
33689
  enabled: true,
33682
33690
  url: normalizeEnvoyUrl(url2),
33683
33691
  token,
33684
33692
  inbound,
33685
- heartbeatMs: Number.isFinite(rawHeartbeatMs) && rawHeartbeatMs > 0 ? Math.max(rawHeartbeatMs, MIN_HEARTBEAT_MS) : DEFAULT_HEARTBEAT_MS
33693
+ heartbeatMs,
33694
+ passDeadlineMs: Math.min(Math.max(namedPassDeadlineMs ?? MIN_PASS_DEADLINE_MS, heartbeatMs), MAX_INTERVAL_MS),
33695
+ passDeadlineRaisedFrom: namedPassDeadlineMs !== undefined && namedPassDeadlineMs < heartbeatMs ? namedPassDeadlineMs : undefined
33686
33696
  };
33687
33697
  }
33688
33698
  function normalizeEnvoyUrl(value) {
33689
33699
  return value.replace(/\/+$/, "");
33690
33700
  }
33691
33701
 
33702
+ // ../envoy-client/src/nats-auth.ts
33703
+ var import_nats = __toESM(require_mod4(), 1);
33704
+ var seedKeys = import_nats.nkeys;
33705
+ function natsAuthOptions(seed) {
33706
+ if (seed === undefined)
33707
+ return {};
33708
+ const bytes = new TextEncoder().encode(seed.value);
33709
+ let publicKey;
33710
+ try {
33711
+ publicKey = seedKeys.fromSeed(bytes).getPublicKey();
33712
+ } catch (error48) {
33713
+ throw new Error(`${seed.source} does not hold a valid nkey seed: ${messageFor(error48)}`);
33714
+ }
33715
+ if (!publicKey.startsWith("U")) {
33716
+ throw new Error(`${seed.source} holds an nkey seed that is not a user's (public key ${publicKey})`);
33717
+ }
33718
+ return { authenticator: import_nats.nkeyAuthenticator(bytes) };
33719
+ }
33720
+
33692
33721
  // ../envoy-client/src/session-settings.ts
33693
33722
  function sessionConfigFile(harness, env) {
33694
33723
  try {
@@ -33727,6 +33756,9 @@ function startNotice(config2, dispatch, envoy, nats) {
33727
33756
  if (envoy.enabled && nats && "off" in envoy.inbound) {
33728
33757
  parts.push(`Envoy's inbound messages are off: ${envoy.inbound.off}; the Envoy tools still work`);
33729
33758
  }
33759
+ if (envoy.enabled && envoy.passDeadlineRaisedFrom !== undefined) {
33760
+ 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`);
33761
+ }
33730
33762
  return parts.length === 0 ? null : parts.join("; ");
33731
33763
  }
33732
33764
  function sessionSettings(file2, env, options) {
@@ -33737,7 +33769,11 @@ function sessionSettings(file2, env, options) {
33737
33769
  const config2 = readEnvoyConfig(file2.path, env, options);
33738
33770
  const dispatch = dispatchSettings(env, config2);
33739
33771
  const envoy = envoySettings(env, config2, { nats: options.nats });
33740
- return { dispatch, envoy, notice: startNotice(config2, dispatch, envoy, options.nats !== false) };
33772
+ return {
33773
+ dispatch,
33774
+ envoy,
33775
+ notice: startNotice(config2, dispatch, envoy, options.nats !== false)
33776
+ };
33741
33777
  }
33742
33778
  function sessionDispatchSettings(file2, env, options) {
33743
33779
  if (file2.path === null)
@@ -35301,11 +35337,31 @@ import("@oh-my-pi/pi-coding-agent/extensibility/extensions/send-user-input-handl
35301
35337
  var codec2 = import_nats2.StringCodec();
35302
35338
  var NATS_RETRY_INTERVAL_MS = 15000;
35303
35339
  var DELIVERY_PICTURES_TIMEOUT_MS = 20000;
35304
- function serialQueue() {
35340
+ var UNFENCED = () => true;
35341
+
35342
+ class AbandonedTaskError extends Error {
35343
+ }
35344
+ function serialQueue(deadlineMs) {
35305
35345
  let tail = Promise.resolve();
35306
35346
  return {
35307
35347
  run(task) {
35308
- const next = tail.then(task);
35348
+ const next = tail.then(() => new Promise((resolve3, reject) => {
35349
+ let abandoned = false;
35350
+ const taskDeadlineMs = deadlineMs();
35351
+ const timer = setTimeout(() => {
35352
+ abandoned = true;
35353
+ reject(new AbandonedTaskError(`a queued task did not settle within ${taskDeadlineMs}ms and was abandoned; the queue moved on`));
35354
+ }, taskDeadlineMs);
35355
+ (async () => task(() => !abandoned))().then((value) => {
35356
+ clearTimeout(timer);
35357
+ if (!abandoned)
35358
+ resolve3(value);
35359
+ }, (error48) => {
35360
+ clearTimeout(timer);
35361
+ if (!abandoned)
35362
+ reject(error48);
35363
+ });
35364
+ }));
35309
35365
  tail = next.catch(() => {
35310
35366
  return;
35311
35367
  });
@@ -35456,6 +35512,7 @@ function envoyExtension(pi) {
35456
35512
  }
35457
35513
  return active;
35458
35514
  };
35515
+ const passDeadlineMs = () => active?.settings.passDeadlineMs ?? MIN_PASS_DEADLINE_MS;
35459
35516
  const subscriptions = new Map;
35460
35517
  const delivered = createDeliveryDedupe();
35461
35518
  let connection;
@@ -35485,7 +35542,7 @@ function envoyExtension(pi) {
35485
35542
  let roleNoticesFor;
35486
35543
  const roleNoticeSubjects = new Set;
35487
35544
  let roleEnds = 0;
35488
- const roleMoves = serialQueue();
35545
+ const roleMoves = serialQueue(passDeadlineMs);
35489
35546
  const isRoleBound = (subject2) => roleNoticeSubjects.has(subject2);
35490
35547
  const heldRole = () => claimedRoleTopic?.slice(ROLE_TOPIC_PREFIX.length);
35491
35548
  let activeSessionContext;
@@ -35621,11 +35678,11 @@ function envoyExtension(pi) {
35621
35678
  }
35622
35679
  return inbound2;
35623
35680
  };
35624
- const ensureConnection = async () => {
35681
+ const ensureConnection = async (live = UNFENCED) => {
35625
35682
  if (connection?.isClosed() === false)
35626
35683
  return connection;
35627
35684
  const { natsUrls, natsSeed } = requireInbound();
35628
- connection = await import_nats2.connect({
35685
+ const dialled = await import_nats2.connect({
35629
35686
  servers: [...natsUrls],
35630
35687
  name: `omp-${sessionID || "unknown"}`,
35631
35688
  ...natsAuthOptions(natsSeed),
@@ -35633,6 +35690,13 @@ function envoyExtension(pi) {
35633
35690
  maxReconnectAttempts: -1,
35634
35691
  reconnectTimeWait: 2000
35635
35692
  });
35693
+ if (!live() || shuttingDown || connection?.isClosed() === false) {
35694
+ await dialled.close().catch(() => {});
35695
+ if (!shuttingDown && connection?.isClosed() === false)
35696
+ return connection;
35697
+ 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");
35698
+ }
35699
+ connection = dialled;
35636
35700
  return connection;
35637
35701
  };
35638
35702
  const postDispatchReply = async (delivery, result, replySessionID = sessionID) => {
@@ -35868,11 +35932,13 @@ function envoyExtension(pi) {
35868
35932
  };
35869
35933
  retry(RESUBSCRIBE_DELAY_MS);
35870
35934
  };
35871
- const subscribe = async (topic) => {
35935
+ const subscribe = async (topic, live = UNFENCED) => {
35872
35936
  const subjects = expandSubscriptionTopics([topic]).filter((candidate) => !subscriptions.has(candidate));
35873
35937
  if (subjects.length === 0)
35874
35938
  return false;
35875
- const activeConnection = await ensureConnection();
35939
+ const activeConnection = await ensureConnection(live);
35940
+ if (!live())
35941
+ return false;
35876
35942
  for (const subject2 of subjects) {
35877
35943
  const subscription = activeConnection.subscribe(subject2);
35878
35944
  intentionallyClosed.delete(subject2);
@@ -35971,7 +36037,7 @@ function envoyExtension(pi) {
35971
36037
  });
35972
36038
  }, RESUBSCRIBE_DELAY_MS);
35973
36039
  };
35974
- const watchAgentStream = async (session) => {
36040
+ const watchAgentStream = async (session, live = UNFENCED) => {
35975
36041
  if (agentStreamControlSession === session && agentStreamControl !== undefined)
35976
36042
  return;
35977
36043
  agentStreamControl?.unsubscribe();
@@ -35982,8 +36048,10 @@ function envoyExtension(pi) {
35982
36048
  agentStreamCommandsRefreshedOnWatchAt = -Infinity;
35983
36049
  }
35984
36050
  agentStreamControlSession = session;
35985
- const active2 = await ensureConnection();
35986
- const subscription = active2.subscribe(agentStreamControlSubject(session));
36051
+ const connected = await ensureConnection(live);
36052
+ if (!live())
36053
+ return;
36054
+ const subscription = connected.subscribe(agentStreamControlSubject(session));
35987
36055
  agentStreamControl = subscription;
35988
36056
  pumpAgentStreamControl(subscription, session);
35989
36057
  };
@@ -36046,12 +36114,15 @@ function envoyExtension(pi) {
36046
36114
  closeIntentionally(subject2);
36047
36115
  return false;
36048
36116
  };
36117
+ let registeredFor = "";
36118
+ let registeredAt = -Infinity;
36049
36119
  const registerSession = () => {
36050
- for (const [topic, hold] of abandonedHolds) {
36120
+ const registeringFor = sessionID;
36121
+ for (const [key, hold] of abandonedHolds) {
36051
36122
  if (hold.id !== sessionID)
36052
36123
  continue;
36053
- abandonedHolds.delete(topic);
36054
- pendingHandBacks.set(`${topic}@${sessionID}`, { topic, id: sessionID });
36124
+ abandonedHolds.delete(key);
36125
+ pendingHandBacks.set(key, { topic: hold.topic, id: sessionID });
36055
36126
  }
36056
36127
  return requireEnvoy().client.subscribe({
36057
36128
  sessionID,
@@ -36067,17 +36138,23 @@ function envoyExtension(pi) {
36067
36138
  capabilities: sideTurn(activeSessionContext) === undefined ? CAPABILITIES_WITHOUT_BTW : DELIVERY_CAPABILITIES,
36068
36139
  driving: false,
36069
36140
  selfSubscribed: true
36141
+ }).then((result) => {
36142
+ registeredFor = registeringFor;
36143
+ registeredAt = Date.now();
36144
+ return result;
36070
36145
  });
36071
36146
  };
36072
- const reassertRoleNow = async (id, role, afterOutage, context) => {
36147
+ const reassertRoleNow = async (id, role, afterOutage, context, live) => {
36073
36148
  const topic = ROLE_TOPIC_PREFIX + role;
36074
36149
  const { client } = requireEnvoy();
36150
+ const owns = () => live() && sessionOpen(id);
36151
+ const stillHolds = () => claimedRoleTopic === topic;
36075
36152
  const holder = await client.getRole(role).then((info) => info.holder, (error48) => {
36076
36153
  if (error48 instanceof EnvoyApiError && error48.details.status === 404)
36077
36154
  return;
36078
36155
  throw error48;
36079
36156
  });
36080
- if (claimedRoleTopic !== topic || !sessionOpen(id))
36157
+ if (!owns() || !stillHolds())
36081
36158
  return false;
36082
36159
  let reason;
36083
36160
  if (holder === id) {
@@ -36086,7 +36163,7 @@ function envoyExtension(pi) {
36086
36163
  reason = "reregistered";
36087
36164
  } else {
36088
36165
  const result = await client.setRole({ sessionID: id, role, soft: true });
36089
- if (claimedRoleTopic !== topic || !sessionOpen(id))
36166
+ if (!owns() || !stillHolds())
36090
36167
  return false;
36091
36168
  if (!result.claimed) {
36092
36169
  endRole();
@@ -36118,23 +36195,48 @@ function envoyExtension(pi) {
36118
36195
  if (heartbeatRegistered)
36119
36196
  return;
36120
36197
  let heartbeatOutageNotified = false;
36198
+ let stallNotified = false;
36121
36199
  let registrationFailed = false;
36122
36200
  let healing = false;
36201
+ const noteStall = (detail) => {
36202
+ if (stallNotified)
36203
+ return;
36204
+ stallNotified = true;
36205
+ logger3.warn("envoy: reconcile pass stalled; registration refreshed on its own", {
36206
+ detail,
36207
+ sessionID: context.sessionManager.getSessionId()
36208
+ });
36209
+ 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");
36210
+ };
36123
36211
  context.setInterval(() => {
36124
36212
  if (context.sessionManager.getSessionId() === "")
36125
36213
  return;
36126
- if (healing)
36214
+ if (healing) {
36215
+ const refreshMs = requireEnvoy().settings.heartbeatMs;
36216
+ if (registeredFor === sessionID && Date.now() - registeredAt < refreshMs)
36217
+ return;
36218
+ noteStall("a pass has not settled a whole refresh interval after the last accepted registration");
36219
+ registerSession().catch(() => {
36220
+ return;
36221
+ });
36127
36222
  return;
36223
+ }
36128
36224
  healing = true;
36129
36225
  const afterOutage = registrationFailed;
36130
36226
  const settled = (decided) => {
36131
36227
  if (decided)
36132
36228
  registrationFailed = false;
36133
36229
  heartbeatOutageNotified = false;
36230
+ stallNotified = false;
36134
36231
  };
36135
36232
  reconcile(context, { heartbeat: true, afterOutage }).then(settled, (error48) => {
36136
36233
  if (error48 instanceof InboundError)
36137
36234
  return settled(error48.decided);
36235
+ if (error48 instanceof AbandonedTaskError) {
36236
+ registrationFailed = true;
36237
+ noteStall(messageFor(error48));
36238
+ return;
36239
+ }
36138
36240
  if (error48 instanceof RegistrationError)
36139
36241
  registrationFailed = true;
36140
36242
  throw error48 instanceof RegistrationError ? error48.cause : error48;
@@ -36149,12 +36251,12 @@ function envoyExtension(pi) {
36149
36251
  }, requireEnvoy().settings.heartbeatMs);
36150
36252
  heartbeatRegistered = true;
36151
36253
  };
36152
- const recoverRegisteredInterests = async () => {
36254
+ const recoverRegisteredInterests = async (live) => {
36153
36255
  const { client } = requireEnvoy();
36154
36256
  const registry2 = await client.getInterest(sessionID).catch(() => {
36155
36257
  return;
36156
36258
  });
36157
- if (registry2 === undefined)
36259
+ if (registry2 === undefined || !live())
36158
36260
  return;
36159
36261
  const retired = publishedInterface?.roleClaim.slackRoleOwned === true ? registry2.topics.filter((topic) => slackTopicChannel(topic) !== undefined) : [];
36160
36262
  if (retired.length > 0) {
@@ -36166,31 +36268,34 @@ function envoyExtension(pi) {
36166
36268
  });
36167
36269
  });
36168
36270
  }
36271
+ if (!live())
36272
+ return;
36169
36273
  for (const topic of registry2.topics) {
36170
36274
  if (topic === agentSubject(sessionID) || topic.startsWith(ROLE_TOPIC_PREFIX))
36171
36275
  continue;
36172
36276
  if (retired.includes(topic))
36173
36277
  continue;
36174
- await subscribe(topic);
36278
+ await subscribe(topic, live);
36175
36279
  }
36176
36280
  };
36177
- const observeAbandonedHold = async (topic, role, holder) => {
36178
- const abandoned = abandonedHolds.get(topic);
36179
- if (abandoned === undefined || abandoned.id !== holder)
36281
+ const observeAbandonedHold = async (topic, role, holder, live) => {
36282
+ const key = `${topic}@${holder}`;
36283
+ const abandoned = abandonedHolds.get(key);
36284
+ if (abandoned === undefined)
36180
36285
  return false;
36181
36286
  const seen = await requireEnvoy().client.getRole(role).then((info) => info.holder === holder ? info.last_seen : undefined, () => {
36182
36287
  return;
36183
36288
  });
36184
- if (seen === undefined)
36289
+ if (!live() || seen === undefined)
36185
36290
  return true;
36186
36291
  if (abandoned.lastSeen !== undefined && seen !== abandoned.lastSeen) {
36187
- abandonedHolds.delete(topic);
36292
+ abandonedHolds.delete(key);
36188
36293
  return false;
36189
36294
  }
36190
- abandonedHolds.set(topic, { id: abandoned.id, lastSeen: seen });
36295
+ abandonedHolds.set(key, { topic, id: abandoned.id, lastSeen: seen });
36191
36296
  return true;
36192
36297
  };
36193
- const setEnvoyRole = async (role, forSession, options = {}) => {
36298
+ const setEnvoyRole = async (role, forSession, live, options = {}) => {
36194
36299
  if (!sessionOpen(forSession)) {
36195
36300
  throw new SessionLeftError(`the session moved from ${forSession} to ${hostSessionID()} before its claim of role ${role} ran; nothing was claimed`);
36196
36301
  }
@@ -36205,10 +36310,18 @@ function envoyExtension(pi) {
36205
36310
  soft: options.soft,
36206
36311
  previousSessionID: options.previousSessionID
36207
36312
  });
36313
+ if (!live()) {
36314
+ if (result.claimed)
36315
+ pendingHandBacks.set(`${topic}@${id}`, { topic, id });
36316
+ 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"}`);
36317
+ }
36208
36318
  if (result.claimed) {
36209
36319
  refusedRoles.delete(id);
36210
36320
  holdUnconfirmed = false;
36211
- abandonedHolds.delete(topic);
36321
+ for (const [key, hold] of abandonedHolds) {
36322
+ if (hold.topic === topic)
36323
+ abandonedHolds.delete(key);
36324
+ }
36212
36325
  for (const [key, hold] of pendingHandBacks) {
36213
36326
  if (hold.topic === topic)
36214
36327
  pendingHandBacks.delete(key);
@@ -36228,10 +36341,11 @@ function envoyExtension(pi) {
36228
36341
  }
36229
36342
  if (!result.claimed) {
36230
36343
  endRole();
36231
- const waitingOnReap = await observeAbandonedHold(topic, role, result.holder);
36232
- if (!waitingOnReap)
36344
+ const waitingOnReap = await observeAbandonedHold(topic, role, result.holder, live);
36345
+ 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`;
36346
+ if (notRecorded === undefined && !waitingOnReap)
36233
36347
  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 });
36348
+ 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
36349
  return false;
36236
36350
  }
36237
36351
  if (previousTopic !== undefined && previousTopic !== topic)
@@ -36340,12 +36454,13 @@ function envoyExtension(pi) {
36340
36454
  knownSessions.add(host3);
36341
36455
  };
36342
36456
  const knownSessions = new Set;
36343
- const passes = serialQueue();
36344
- const reconcile = (context, cause = {}) => passes.run(() => reconcileNow(context, cause));
36457
+ const passes = serialQueue(passDeadlineMs);
36458
+ const reconcile = (context, cause = {}) => passes.run((live) => reconcileNow(context, cause, live));
36345
36459
  const agentSessions = new Set;
36346
36460
  let inboundOpenFor = "";
36347
- const reconcileNow = async (context, cause) => {
36461
+ const reconcileNow = async (context, cause, live) => {
36348
36462
  const host3 = context.sessionManager.getSessionId();
36463
+ const owns = () => live() && sessionOpen(host3);
36349
36464
  noteMove(host3);
36350
36465
  if (sessionID !== host3)
36351
36466
  restoreLocalSessionState(context);
@@ -36375,27 +36490,39 @@ function envoyExtension(pi) {
36375
36490
  if (inbound && inboundOpenFor !== host3) {
36376
36491
  try {
36377
36492
  agentSessions.add(host3);
36378
- await ensureConnection();
36379
- await subscribe(agentSubject(host3));
36380
- await watchAgentStream(host3);
36493
+ await ensureConnection(live);
36494
+ if (!owns())
36495
+ return false;
36496
+ await subscribe(agentSubject(host3), live);
36497
+ if (!owns())
36498
+ return false;
36499
+ await watchAgentStream(host3, live);
36500
+ if (!owns())
36501
+ return false;
36381
36502
  if ((context.sessionManager.getBranch?.() ?? []).length > 0) {
36382
- await recoverRegisteredInterests();
36503
+ await recoverRegisteredInterests(live);
36504
+ if (!owns())
36505
+ return false;
36383
36506
  }
36384
36507
  inboundOpenFor = host3;
36385
36508
  } catch (error48) {
36386
36509
  inboundFailure = error48;
36387
36510
  }
36388
36511
  }
36389
- if (hostSessionID() !== host3)
36512
+ if (!owns())
36390
36513
  return false;
36391
36514
  const registration = await registerSession().then(() => {
36392
36515
  return;
36393
36516
  }, (error48) => ({ error: error48 }));
36517
+ if (!owns())
36518
+ return false;
36394
36519
  if (registration === undefined) {
36395
36520
  establishedSession = host3;
36396
36521
  ensureHeartbeat(context);
36397
36522
  }
36398
- const decided = await roleMoves.run(() => settleRole(context, host3, cause, registration === undefined));
36523
+ const decided = await roleMoves.run((moveLive) => settleRole(context, host3, cause, registration === undefined, moveLive));
36524
+ if (!owns())
36525
+ return false;
36399
36526
  if (registration !== undefined) {
36400
36527
  throw new RegistrationError(messageFor(registration.error), { cause: registration.error });
36401
36528
  }
@@ -36406,12 +36533,14 @@ function envoyExtension(pi) {
36406
36533
  };
36407
36534
  let roleSettledFor = "";
36408
36535
  let lastHeard = "";
36409
- const settleEndedHolds = async (host3, wanted) => {
36536
+ const settleEndedHolds = async (wanted, live) => {
36410
36537
  let restored = false;
36411
36538
  const failed = [];
36412
36539
  for (const [key, hold] of pendingHandBacks) {
36540
+ if (!live())
36541
+ break;
36413
36542
  pendingHandBacks.delete(key);
36414
- if (wanted !== undefined && hold.topic === ROLE_TOPIC_PREFIX + wanted) {
36543
+ if (wanted !== undefined && hold.topic === ROLE_TOPIC_PREFIX + wanted && sessionOpen(hold.id)) {
36415
36544
  claimedRoleTopic = hold.topic;
36416
36545
  claimedRoleSessionID = hold.id;
36417
36546
  holdUnconfirmed = true;
@@ -36427,21 +36556,20 @@ function envoyExtension(pi) {
36427
36556
  });
36428
36557
  });
36429
36558
  }
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
- }
36559
+ for (const { key, topic, id } of failed)
36560
+ abandonedHolds.set(key, { topic, id });
36561
+ if (!live())
36562
+ return false;
36436
36563
  return restored;
36437
36564
  };
36438
- const settleRole = async (context, host3, cause, registered) => {
36439
- if (!sessionOpen(host3))
36565
+ const settleRole = async (context, host3, cause, registered, live) => {
36566
+ const owns = () => live() && sessionOpen(host3);
36567
+ if (!owns())
36440
36568
  return false;
36441
36569
  const branch = context.sessionManager.getBranch?.() ?? [];
36442
36570
  const readRows = roleSettledFor !== host3 && (branch.length > 0 || roleSettledFor !== "");
36443
36571
  const desired = await desiredRole(host3, branch, readRows);
36444
- if (!sessionOpen(host3))
36572
+ if (!owns())
36445
36573
  return false;
36446
36574
  const unheard = lastHeard !== host3;
36447
36575
  const held = heldRole();
@@ -36449,13 +36577,13 @@ function envoyExtension(pi) {
36449
36577
  const ended = await handBackRole();
36450
36578
  if (ended !== undefined && unheard)
36451
36579
  warnRoleEnded(context, host3, ended);
36452
- if (!sessionOpen(host3))
36580
+ if (!owns())
36453
36581
  return false;
36454
36582
  }
36455
- if (await settleEndedHolds(host3, desired) && unheard && desired !== undefined) {
36583
+ if (await settleEndedHolds(desired, live) && unheard && desired !== undefined) {
36456
36584
  warnNoticesClosed(context, desired);
36457
36585
  }
36458
- if (!sessionOpen(host3))
36586
+ if (!owns())
36459
36587
  return false;
36460
36588
  if (!registered)
36461
36589
  return false;
@@ -36465,7 +36593,7 @@ function envoyExtension(pi) {
36465
36593
  }
36466
36594
  if (!holdUnconfirmed && heldRole() === desired && claimedRoleSessionID === host3) {
36467
36595
  roleSettledFor = host3;
36468
- return cause.heartbeat === true ? reassertRoleNow(host3, desired, cause.afterOutage === true, context) : true;
36596
+ return cause.heartbeat === true ? reassertRoleNow(host3, desired, cause.afterOutage === true, context, live) : true;
36469
36597
  }
36470
36598
  if (!inbound) {
36471
36599
  roleSettledFor = host3;
@@ -36479,18 +36607,18 @@ function envoyExtension(pi) {
36479
36607
  });
36480
36608
  if (holder !== undefined && holder !== host3 && lineage.includes(holder))
36481
36609
  supersedes = holder;
36482
- if (!sessionOpen(host3))
36610
+ if (!owns())
36483
36611
  return false;
36484
36612
  }
36485
36613
  try {
36486
- const claimed = await setEnvoyRole(desired, host3, {
36614
+ const claimed = await setEnvoyRole(desired, host3, live, {
36487
36615
  soft: true,
36488
36616
  previousSessionID: supersedes
36489
36617
  });
36490
36618
  if (claimed && unheard)
36491
36619
  warnNoticesClosed(context, desired);
36492
36620
  } catch (error48) {
36493
- if (error48 instanceof SessionLeftError)
36621
+ if (error48 instanceof SessionLeftError || error48 instanceof AbandonedTaskError)
36494
36622
  return false;
36495
36623
  logger3.warn("envoy: role reclaim failed", {
36496
36624
  role: desired,
@@ -36498,6 +36626,8 @@ function envoyExtension(pi) {
36498
36626
  error: messageFor(error48)
36499
36627
  });
36500
36628
  }
36629
+ if (!owns())
36630
+ return false;
36501
36631
  roleSettledFor = host3;
36502
36632
  return true;
36503
36633
  };
@@ -36530,7 +36660,7 @@ function envoyExtension(pi) {
36530
36660
  if (establishedSession !== targetSessionID)
36531
36661
  await establishForBridge(context);
36532
36662
  await registerSession();
36533
- await roleMoves.run(() => setEnvoyRole(role, targetSessionID));
36663
+ await roleMoves.run((live) => setEnvoyRole(role, targetSessionID, live));
36534
36664
  };
36535
36665
  const subscribeNotice = async (targetSessionID, topic, callerContext, whileHolding) => {
36536
36666
  const context = callerContext ?? activeSessionContext;
@@ -36963,13 +37093,13 @@ Not delivered: that holder is the session this subagent sends as, and the listen
36963
37093
  }
36964
37094
  case EnvoyToolOperation.setRole: {
36965
37095
  const role = stringFor(parameters, "role");
36966
- const released = await roleMoves.run(async () => {
37096
+ const released = await roleMoves.run(async (live) => {
36967
37097
  const held = heldRole();
36968
37098
  const moving = held !== undefined && held !== role;
36969
37099
  if (moving && roleNoticeSubjects.size > 0) {
36970
37100
  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
37101
  }
36972
- await setEnvoyRole(role, sessionID);
37102
+ await setEnvoyRole(role, sessionID, live);
36973
37103
  return moving ? held : undefined;
36974
37104
  });
36975
37105
  return toolSuccess(released === undefined ? `Now holding role: ${role}` : `Now holding role: ${role} (released role ${released})`, { role, ...released === undefined ? {} : { released } });
@@ -4,7 +4,8 @@ Read this to catch up after a restart, read a node, trace what cites it, or writ
4
4
 
5
5
  ## What comes back
6
6
 
7
- - After a restart, catch up with `dispatch read --issue <KEY>` (or `--artifact` with `--issue` or `--project`, or `--ref`). It prints the issue's summary, open asks, references and recent events, a document's summary (its issue, reference, versions and approval), or an ask's, comment's or message's thread. Reads do not subscribe. Use `dispatch doc-read` for document text.
7
+ - After a restart, catch up with `dispatch read --issue <KEY>` (or `--artifact` with `--issue` or `--project`, or `--ref`). It prints the issue's summary, open asks, references and last 10 events, a document's summary (its issue, reference, versions and approval), or an ask's, comment's or message's thread. Reads do not subscribe. Use `dispatch doc-read` for document text.
8
+ - To audit an issue's history (who asked what, which ask was answered), add `--events N` for the last N events or `--all-events` for every one. Both work on `--ref dispatch://KEY/log` too.
8
9
  - An anchored comment or ask prints `Position:`, where its quote's block stands. `unavailable (DOC_SERVICE_UNAVAILABLE)` means try again shortly. `unavailable (DOC_SCHEMA)` means the document needs repair.
9
10
  - The API gives the same facts as `anchor_block` (or `anchor_block_error`) on `GET /api/v1/comments/{id}` and `GET /api/v1/asks/{id}`. `GET /api/v1/artifacts/{id}/blocks/{block_id}` gives them for any block.
10
11
  - Each read ends with `Referenced by:` (what cites the node) and `Links:` (what it cites), across projects. "What led to this decision" is one read on the ask.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/pi-envoy",
3
- "version": "10.21.0",
3
+ "version": "10.23.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "omp": {