@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 +77 -14
- package/dist/envoy.js +220 -90
- package/dist/skills/dispatch/references/reading.md +2 -1
- package/package.json +1 -1
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 =
|
|
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
|
|
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
|
|
18648
|
+
const parent = await client.getIssue(issueKey);
|
|
18596
18649
|
return {
|
|
18597
|
-
text: childrenSummary(
|
|
18598
|
-
details: { issue:
|
|
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 =
|
|
19346
|
-
var
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
33592
|
-
var
|
|
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
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
35986
|
-
|
|
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
|
-
|
|
36120
|
+
const registeringFor = sessionID;
|
|
36121
|
+
for (const [key, hold] of abandonedHolds) {
|
|
36051
36122
|
if (hold.id !== sessionID)
|
|
36052
36123
|
continue;
|
|
36053
|
-
abandonedHolds.delete(
|
|
36054
|
-
pendingHandBacks.set(
|
|
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 (
|
|
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 (
|
|
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
|
|
36179
|
-
|
|
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(
|
|
36292
|
+
abandonedHolds.delete(key);
|
|
36188
36293
|
return false;
|
|
36189
36294
|
}
|
|
36190
|
-
abandonedHolds.set(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
36380
|
-
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
36432
|
-
|
|
36433
|
-
|
|
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
|
-
|
|
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 (!
|
|
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 (!
|
|
36580
|
+
if (!owns())
|
|
36453
36581
|
return false;
|
|
36454
36582
|
}
|
|
36455
|
-
if (await settleEndedHolds(
|
|
36583
|
+
if (await settleEndedHolds(desired, live) && unheard && desired !== undefined) {
|
|
36456
36584
|
warnNoticesClosed(context, desired);
|
|
36457
36585
|
}
|
|
36458
|
-
if (!
|
|
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 (!
|
|
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
|
|
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.
|