@sjawhar/opencode-legion-envoy 9.2.0 → 9.3.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 +88 -25
- package/dist/src/server.js +49 -10
- package/package.json +1 -1
- package/skills/envoy/SKILL.md +1 -1
- package/skills/envoy/references/deliveries.md +1 -1
- package/skills/legion-architect/SKILL.md +2 -0
- package/skills/legion-architect/references/wake-routing.md +1 -1
- package/skills/legion-controller/references/admission.md +8 -8
- package/skills/legion-worker/references/tools.md +3 -2
package/dist/dispatch.js
CHANGED
|
@@ -13602,6 +13602,15 @@ var ArtifactVersionEventPayloadSchema = exports_external.object({
|
|
|
13602
13602
|
version: exports_external.object({ number: exports_external.number().optional(), summary: exports_external.string().nullish() }).optional(),
|
|
13603
13603
|
diff: exports_external.string().optional()
|
|
13604
13604
|
});
|
|
13605
|
+
var ArtifactHumanEditedEventPayloadSchema = exports_external.object({
|
|
13606
|
+
artifact_id: exports_external.string().optional(),
|
|
13607
|
+
name: exports_external.string().optional(),
|
|
13608
|
+
from_version: exports_external.number().int().optional(),
|
|
13609
|
+
to_version: exports_external.number().int().optional(),
|
|
13610
|
+
editors: exports_external.array(exports_external.string()).optional(),
|
|
13611
|
+
diff: exports_external.string().optional(),
|
|
13612
|
+
diff_truncated: exports_external.boolean().optional()
|
|
13613
|
+
});
|
|
13605
13614
|
var ArtifactReviewEventPayloadSchema = exports_external.object({
|
|
13606
13615
|
artifact_id: exports_external.string().optional(),
|
|
13607
13616
|
name: exports_external.string().optional(),
|
|
@@ -14312,7 +14321,7 @@ var dispatchToolSpecs = [
|
|
|
14312
14321
|
ops: [{ op: "delete_column", block: "table-123", index: 1 }],
|
|
14313
14322
|
precondition: { blocks: [{ id: "table-123", token: "sha256:current-table-token" }] }
|
|
14314
14323
|
},
|
|
14315
|
-
description: "Apply deterministic document edits: replace or delete quoted text, insert markdown at an anchor, retype an identified paragraph or typed block into a schema-declared typed block, delete or move a whole block by its id, or delete a table row or column in place. " + "Do not use it for review feedback or for reading; use dispatch comment, dispatch suggest, or dispatch doc-read instead. " + `For replace, delete, and quote anchors, find text as rendered: inline Markdown (**bold**, \`code\`) is tolerated and must be balanced; a leading '# ' matches a heading at any level. replace is inline: with is the new text of the matched span, so a marker of a different kind from the block's own stays literal text ('4. Design' written into a heading). A with that opens with a marker of the same kind as the matched block's own would write it twice and is INVALID_OP - including prose that merely looks like one ('1999. was a year' into an ordered item), which you write as text by escaping it ('1999\\. was a year'). The exception is a heading rename whose find carried a heading marker: replace(find="## Old", with="## New") gives '## New', and a different level applies only when find named the heading's actual level (find "## Old" with "### New" makes it an h3), since '# ' selects a heading without naming its level. A task item (\`- [ ]\` / \`- [x]\`) is ticked the same way: when find matches from the start of the item's text and with opens with '[x] ' or '[ ] ', the opening sets the item's box and the rest is its new text (replace(find="Write it", with="[x] Write it") ticks it); the same opening over a plain list item or a paragraph stays literal text. Any non-empty with that renders to no text - a line indented four spaces or a tab, which markdown reads as a code block, or whitespace alone - is INVALID_OP rather than a silent deletion; pass an empty with to delete the matched text on purpose - a list item, quote, typed block or footnote definition left holding only the emptied paragraph keeps it. ` + "with cannot open a new block: after a hard line break inside with (two trailing spaces, or a backslash, before the newline) a heading, bullet, '1.'/'1)' ordered, or '>' blockquote marker is INVALID_OP too, since that line would stay escaped text inside the matched block - use insert, plus delete for what it replaces, to add the block. A hard break in with is itself INVALID_OP when the matched text is in a heading or a table cell, which are written on one line. " + "A delete whose find is a block's entire text removes the block (a list emptied of its items goes too); delete with block removes any block by id, and move with block relocates one. A delete or retype that would take an ask block out of the document while its ask is open is refused, with nothing sent. delete_row and delete_column take a table block and a zero-based index, preserving the table block id and refusing to remove cells with open asks or unresolved comments. " + `Insert and move anchors also accept "start", "end", "heading:<exact heading text>", and "block:<id>"; dispatch doc-read --blocks lists every block's id, type, opening words and token, and each table's rows and cells with the indexes delete_row and delete_column take (GET /api/v1/artifacts/{artifact UUID}/blocks serves the same list). ` + "Optionally require the state just read: precondition selects exactly one of a document token from dispatch doc-read, or block {id, token} values from dispatch doc-read --blocks. A block guard must include every block the batch changes; Dispatch resolves quote targets and rejects an uncovered batch rather than applying it. Use a document token for insert or move, which depend on document order. Prefer block tokens when the covered content blocks are independent sections. Tokens include inline marks, so a fresh human comment also makes a stale edit fail. PRECONDITION_FAILED means re-read; EDIT_QUEUE_FULL means back off before retrying. " + "The result carries the document token this edit produced, so a chain of guarded edits passes each result's token as the next edit's precondition with no dispatch doc-read between them. " + "A batch that leaves the document exactly as it was mints no version, named or not, and the result says nothing changed and names each operation that did nothing. " + "A change a browser removes while the edit is in flight is never reported as applied: EDIT_LOST_TO_CONCURRENT_CHANGE means the write was refused and nothing was written, so re-read the document and decide again, as with PRECONDITION_FAILED; lost_ops on a successful result names operations whose text the live document no longer has, because the deletion landed after the version was written. " + "One replace per paragraph: more new paragraphs go in one insert after the last one you rewrote, since two inserts at one anchor come out reversed; read the passage back with dispatch doc-read afterwards. " + "A block id written as {#id} longer than 256 bytes is stored, but its block is no graph node, so no reference reaches it. " + `The spec holds requirements, design, and decisions - never progress, status, or timestamps; a record of open items or progress is a document of its own. ${OWNER_REFERENCE} ${SPEC_WRITING_POINTER}`,
|
|
14324
|
+
description: "Apply deterministic document edits: replace or delete quoted text, insert markdown at an anchor, retype an identified paragraph or typed block into a schema-declared typed block, delete or move a whole block by its id, or delete a table row or column in place. " + "Do not use it for review feedback or for reading; use dispatch comment, dispatch suggest, or dispatch doc-read instead. " + `For replace, delete, and quote anchors, find text as rendered: inline Markdown (**bold**, \`code\`) is tolerated and must be balanced; a leading '# ' matches a heading at any level. replace is inline: with is the new text of the matched span, so a marker of a different kind from the block's own stays literal text ('4. Design' written into a heading). A with that opens with a marker of the same kind as the matched block's own would write it twice and is INVALID_OP - including prose that merely looks like one ('1999. was a year' into an ordered item), which you write as text by escaping it ('1999\\. was a year'). The exception is a heading rename whose find carried a heading marker: replace(find="## Old", with="## New") gives '## New', and a different level applies only when find named the heading's actual level (find "## Old" with "### New" makes it an h3), since '# ' selects a heading without naming its level. A task item (\`- [ ]\` / \`- [x]\`) is ticked the same way: when find matches from the start of the item's text and with opens with '[x] ' or '[ ] ', the opening sets the item's box and the rest is its new text (replace(find="Write it", with="[x] Write it") ticks it); the same opening over a plain list item or a paragraph stays literal text. Any non-empty with that renders to no text - a line indented four spaces or a tab, which markdown reads as a code block, or whitespace alone - is INVALID_OP rather than a silent deletion; pass an empty with to delete the matched text on purpose - a list item, quote, typed block or footnote definition left holding only the emptied paragraph keeps it. ` + "with cannot open a new block: after a hard line break inside with (two trailing spaces, or a backslash, before the newline) a heading, bullet, '1.'/'1)' ordered, or '>' blockquote marker is INVALID_OP too, since that line would stay escaped text inside the matched block - use insert, plus delete for what it replaces, to add the block. A hard break in with is itself INVALID_OP when the matched text is in a heading or a table cell, which are written on one line. In a table cell, with is the cell's text: write a | in it bare, in code too, since code whose text has an odd run of backslashes right before a | cannot be stored in a cell and is INVALID_OP, naming the cell. " + "A delete whose find is a block's entire text removes the block (a list emptied of its items goes too); delete with block removes any block by id, and move with block relocates one. A delete or retype that would take an ask block out of the document while its ask is open is refused, with nothing sent. delete_row and delete_column take a table block and a zero-based index, preserving the table block id and refusing to remove cells with open asks or unresolved comments. " + `Insert and move anchors also accept "start", "end", "heading:<exact heading text>", and "block:<id>"; dispatch doc-read --blocks lists every block's id, type, opening words and token, and each table's rows and cells with the indexes delete_row and delete_column take (GET /api/v1/artifacts/{artifact UUID}/blocks serves the same list). ` + "Optionally require the state just read: precondition selects exactly one of a document token from dispatch doc-read, or block {id, token} values from dispatch doc-read --blocks. A block guard must include every block the batch changes; Dispatch resolves quote targets and rejects an uncovered batch rather than applying it. Use a document token for insert or move, which depend on document order. Prefer block tokens when the covered content blocks are independent sections. Tokens include inline marks, so a fresh human comment also makes a stale edit fail. PRECONDITION_FAILED means re-read; EDIT_QUEUE_FULL means back off before retrying. " + "The result carries the document token this edit produced, so a chain of guarded edits passes each result's token as the next edit's precondition with no dispatch doc-read between them. " + "A batch that leaves the document exactly as it was mints no version, named or not, and the result says nothing changed and names each operation that did nothing. " + "A change a browser removes while the edit is in flight is never reported as applied: EDIT_LOST_TO_CONCURRENT_CHANGE means the write was refused and nothing was written, so re-read the document and decide again, as with PRECONDITION_FAILED; lost_ops on a successful result names operations whose text the live document no longer has, because the deletion landed after the version was written. " + "One replace per paragraph: more new paragraphs go in one insert after the last one you rewrote, since two inserts at one anchor come out reversed; read the passage back with dispatch doc-read afterwards. " + "A block id written as {#id} longer than 256 bytes is stored, but its block is no graph node, so no reference reaches it. " + `The spec holds requirements, design, and decisions - never progress, status, or timestamps; a record of open items or progress is a document of its own. ${OWNER_REFERENCE} ${SPEC_WRITING_POINTER}`,
|
|
14316
14325
|
arguments: (z2) => ({
|
|
14317
14326
|
issue: z2.string().describe(ISSUE_REFERENCE).optional(),
|
|
14318
14327
|
project: z2.string().describe("Project key owning the document.").optional(),
|
|
@@ -14372,7 +14381,7 @@ var dispatchToolSpecs = [
|
|
|
14372
14381
|
name: "dispatch_artifact",
|
|
14373
14382
|
example: { issue: "DSP-1", name: "design.md", content: `# Design
|
|
14374
14383
|
` },
|
|
14375
|
-
description: "Attach a local file or inline text as an issue artifact or project document. Do not use it to edit a live document; use " + "dispatch doc-edit instead. Never upload a document's own name to change it (spec.md replaces the spec whole); edit it with dispatch doc-edit. Uploading the same name again makes its next version. Exactly one of path or content is required; a markdown document is at most 1 MiB and any other file at most 25 MiB. " + "Markdown is also refused with 413 CAP_EXCEEDED when it makes more than 65,536 elements (a block weighs 3, and an empty list item, quote, footnote definition or typed block 3 more for the empty paragraph it holds; a table cell 4, a hard line break 3, an image 3, a piece of inline HTML 2, an autolink 2, a footnote reference 2; inline syntax, marks, lines of text and backslash escapes or character references 1 each), or when a new version would leave the document's stored markdown (what its text reads back as) longer than 1 MiB or making more than 65,536 elements, and longer or heavier than before; shorten it or split it across documents. " + "Markdown holding an ask block whose body breaks its content rule (one or more question paragraphs, then at most one bullet list of options, last) is refused with 400 INVALID_ASK_BLOCK; a new version of a document is held to it only for the asks it writes or changes. " + `${OWNER_REFERENCE}`,
|
|
14384
|
+
description: "Attach a local file or inline text as an issue artifact or project document. Do not use it to edit a live document; use " + "dispatch doc-edit instead. Never upload a document's own name to change it (spec.md replaces the spec whole); edit it with dispatch doc-edit. Uploading the same name again makes its next version. Exactly one of path or content is required; a markdown document is at most 1 MiB and any other file at most 25 MiB. " + "Markdown is also refused with 413 CAP_EXCEEDED when it makes more than 65,536 elements (a block weighs 3, and an empty list item, quote, footnote definition or typed block 3 more for the empty paragraph it holds; a table cell 4, a hard line break 3, an image 3, a piece of inline HTML 2, an autolink 2, a footnote reference 2; inline syntax, marks, lines of text and backslash escapes or character references 1 each), or when a new version would leave the document's stored markdown (what its text reads back as) longer than 1 MiB or making more than 65,536 elements, and longer or heavier than before; shorten it or split it across documents. " + "In a table row, write every | in a cell as \\|, in code too: a | after two backslashes ends the cell, and a row with more cells than its table is refused. " + "Markdown holding an ask block whose body breaks its content rule (one or more question paragraphs, then at most one bullet list of options, last) is refused with 400 INVALID_ASK_BLOCK; a new version of a document is held to it only for the asks it writes or changes. " + `${OWNER_REFERENCE}`,
|
|
14376
14385
|
arguments: (z2) => ({
|
|
14377
14386
|
issue: z2.string().describe(ISSUE_REFERENCE).optional(),
|
|
14378
14387
|
project: z2.string().describe("Project key for an unlinked document.").optional(),
|
|
@@ -16318,6 +16327,13 @@ function subscriptionCovers(topic, owner) {
|
|
|
16318
16327
|
return false;
|
|
16319
16328
|
}
|
|
16320
16329
|
|
|
16330
|
+
// ../envoy-client/src/human-edit.ts
|
|
16331
|
+
function humanEditHead(edit) {
|
|
16332
|
+
const editors = edit.editors ?? [];
|
|
16333
|
+
const by = editors.length === 0 ? "" : `, edited by ${editors.join(", ")}`;
|
|
16334
|
+
return `${edit.name ?? "document"} v${edit.from_version ?? "?"} to v${edit.to_version ?? "?"}${by}`;
|
|
16335
|
+
}
|
|
16336
|
+
|
|
16321
16337
|
// ../envoy-client/src/search-answer.ts
|
|
16322
16338
|
function pageSummaryText(offset, count, total) {
|
|
16323
16339
|
return `showing ${offset + 1}-${offset + count} of ${total}`;
|
|
@@ -17407,12 +17423,12 @@ function issueSummary(issue2, events, references, graph, linkBase, titles) {
|
|
|
17407
17423
|
].join(`
|
|
17408
17424
|
`);
|
|
17409
17425
|
}
|
|
17410
|
-
function referenceLines(
|
|
17411
|
-
if (typeof
|
|
17412
|
-
return [`- ${
|
|
17413
|
-
if (edges.length === 0)
|
|
17426
|
+
function referenceLines(read) {
|
|
17427
|
+
if (typeof read === "string")
|
|
17428
|
+
return [`- ${read}`];
|
|
17429
|
+
if (read.edges.length === 0)
|
|
17414
17430
|
return ["- none"];
|
|
17415
|
-
return edges.map((edge) => {
|
|
17431
|
+
return read.edges.map((edge) => {
|
|
17416
17432
|
const excerpt2 = edge.excerpt === undefined ? "" : `${textHead(edge.excerpt.text)} \xB7 `;
|
|
17417
17433
|
const block = edge.block === undefined ? "" : ` (block ${edge.block.ref ?? edge.block.id})`;
|
|
17418
17434
|
return `- ${edge.kind} ${edge.node.kind} ${edge.node.ref ?? edge.node.id} (${excerpt2}${edge.created_at})${block}`;
|
|
@@ -17421,9 +17437,9 @@ function referenceLines(edges) {
|
|
|
17421
17437
|
function unavailableReason(error48) {
|
|
17422
17438
|
return dispatchAnswered(error48, 404) ? "unavailable" : `unavailable: ${messageFor(error48)}`;
|
|
17423
17439
|
}
|
|
17424
|
-
async function
|
|
17440
|
+
async function graphReferences(client, query) {
|
|
17425
17441
|
try {
|
|
17426
|
-
return
|
|
17442
|
+
return await client.getReferences(query);
|
|
17427
17443
|
} catch (error48) {
|
|
17428
17444
|
return unavailableReason(error48);
|
|
17429
17445
|
}
|
|
@@ -17437,13 +17453,31 @@ async function issueReferencesOrUnavailable(client, key) {
|
|
|
17437
17453
|
}
|
|
17438
17454
|
async function graphSections(client, ref) {
|
|
17439
17455
|
const [incoming, outgoing] = await Promise.all([
|
|
17440
|
-
|
|
17441
|
-
|
|
17456
|
+
graphReferences(client, { to: ref }),
|
|
17457
|
+
graphReferences(client, { from: ref })
|
|
17442
17458
|
]);
|
|
17443
17459
|
return graphSectionLines(incoming, outgoing);
|
|
17444
17460
|
}
|
|
17445
17461
|
function graphSectionLines(incoming, outgoing) {
|
|
17446
|
-
return [
|
|
17462
|
+
return [
|
|
17463
|
+
...typeof incoming === "string" ? [] : nodeLine(incoming.node),
|
|
17464
|
+
"Referenced by:",
|
|
17465
|
+
...referenceLines(incoming),
|
|
17466
|
+
"Links:",
|
|
17467
|
+
...referenceLines(outgoing)
|
|
17468
|
+
];
|
|
17469
|
+
}
|
|
17470
|
+
function nodeLine(node) {
|
|
17471
|
+
if (node.made_by === undefined)
|
|
17472
|
+
return [];
|
|
17473
|
+
const fact = (by, time3) => `${by === undefined || by.length === 0 ? "nobody recorded" : by.map(actorText).join(", ")}${time3 == null ? "" : ` at ${time3}`}`;
|
|
17474
|
+
const made = fact(node.made_by, node.made_at);
|
|
17475
|
+
const changed = fact(node.changed_by, node.changed_at);
|
|
17476
|
+
const { parent } = node;
|
|
17477
|
+
const partOf = parent == null ? "" : ` \xB7 part of ${parent.ref ?? `${parent.kind} ${parent.id}`}${node.parent_version == null ? "" : ` (version ${node.parent_version})`}`;
|
|
17478
|
+
return [
|
|
17479
|
+
`Node: ${node.kind} ${node.ref ?? node.id} \xB7 made by ${made} \xB7 changed by ${changed}${partOf}`
|
|
17480
|
+
];
|
|
17447
17481
|
}
|
|
17448
17482
|
function blockLines(node, ref) {
|
|
17449
17483
|
const head = [
|
|
@@ -17497,6 +17531,8 @@ function eventHead(event) {
|
|
|
17497
17531
|
return textHead(event.payload.body);
|
|
17498
17532
|
case "artifact.version":
|
|
17499
17533
|
return textHead(`${event.payload.name} v${event.payload.version.number}${event.payload.version.summary ? `: ${event.payload.version.summary}` : ""}`);
|
|
17534
|
+
case "artifact.human_edited":
|
|
17535
|
+
return textHead(humanEditHead(event.payload));
|
|
17500
17536
|
case "issue.created":
|
|
17501
17537
|
case "issue.updated":
|
|
17502
17538
|
case "issue.closed":
|
|
@@ -18839,13 +18875,10 @@ ${trailer.join(`
|
|
|
18839
18875
|
}
|
|
18840
18876
|
throw error48;
|
|
18841
18877
|
}),
|
|
18842
|
-
|
|
18878
|
+
graphReferences(client, { from: blockRef })
|
|
18843
18879
|
]);
|
|
18844
18880
|
return {
|
|
18845
|
-
text: [
|
|
18846
|
-
...blockLines(incoming.node, ref),
|
|
18847
|
-
...graphSectionLines(incoming.edges, outgoing)
|
|
18848
|
-
].join(`
|
|
18881
|
+
text: [...blockLines(incoming.node, ref), ...graphSectionLines(incoming, outgoing)].join(`
|
|
18849
18882
|
`),
|
|
18850
18883
|
details: {
|
|
18851
18884
|
...ref.owner.kind === "project" ? { project: ref.owner.project } : { issue: ref.owner.issue },
|
|
@@ -19793,6 +19826,7 @@ function sessionEnvoySettings(file2, env, options) {
|
|
|
19793
19826
|
// ../envoy-client/src/transport.ts
|
|
19794
19827
|
var DEFAULT_TIMEOUT_MS = 5000;
|
|
19795
19828
|
var RETRY_DELAY_MS = 250;
|
|
19829
|
+
var REPLAY_TIMEOUT_MS = 1e4;
|
|
19796
19830
|
var InterestWireSchema = exports_external.object({
|
|
19797
19831
|
session_id: exports_external.string(),
|
|
19798
19832
|
machine_id: exports_external.string(),
|
|
@@ -19827,12 +19861,33 @@ var RoleHeldWireSchema = exports_external.object({
|
|
|
19827
19861
|
});
|
|
19828
19862
|
var ErrorWireSchema = exports_external.object({
|
|
19829
19863
|
error: exports_external.string(),
|
|
19864
|
+
code: exports_external.string().optional(),
|
|
19830
19865
|
expected: exports_external.array(exports_external.string()).optional()
|
|
19831
19866
|
});
|
|
19832
19867
|
var EnvelopeResponseSchema = EnvelopeSchema.extend({
|
|
19833
19868
|
recipient: exports_external.string().optional(),
|
|
19834
19869
|
holder: exports_external.string().optional()
|
|
19835
19870
|
});
|
|
19871
|
+
var ReplayWireSchema = exports_external.object({
|
|
19872
|
+
stream: exports_external.object({
|
|
19873
|
+
id: exports_external.string().min(1).optional(),
|
|
19874
|
+
created: exports_external.string(),
|
|
19875
|
+
first_seq: exports_external.number().int().nonnegative(),
|
|
19876
|
+
last_seq: exports_external.number().int().nonnegative()
|
|
19877
|
+
}),
|
|
19878
|
+
subjects: exports_external.array(exports_external.object({
|
|
19879
|
+
subject: exports_external.string(),
|
|
19880
|
+
messages: exports_external.array(exports_external.object({
|
|
19881
|
+
seq: exports_external.number().int().positive(),
|
|
19882
|
+
subject: exports_external.string(),
|
|
19883
|
+
time: exports_external.string(),
|
|
19884
|
+
data: exports_external.string()
|
|
19885
|
+
})),
|
|
19886
|
+
next_after: exports_external.number().int().nonnegative(),
|
|
19887
|
+
more: exports_external.boolean(),
|
|
19888
|
+
refused: exports_external.string().optional()
|
|
19889
|
+
}))
|
|
19890
|
+
});
|
|
19836
19891
|
function expandSubscriptionTopics(topics) {
|
|
19837
19892
|
const expanded = new Set;
|
|
19838
19893
|
for (const topic of topics) {
|
|
@@ -19855,6 +19910,7 @@ class EnvoyApiError extends Error {
|
|
|
19855
19910
|
details;
|
|
19856
19911
|
name = "EnvoyApiError";
|
|
19857
19912
|
expected;
|
|
19913
|
+
code;
|
|
19858
19914
|
constructor(details) {
|
|
19859
19915
|
let parsed;
|
|
19860
19916
|
try {
|
|
@@ -19865,16 +19921,18 @@ class EnvoyApiError extends Error {
|
|
|
19865
19921
|
super(parsed === undefined ? `${details.method} ${details.url} failed with ${details.status}: ${details.responseBody}` : `${parsed.error}${parsed.expected === undefined ? "" : ` (expected: ${parsed.expected.join(", ")})`}`);
|
|
19866
19922
|
this.details = details;
|
|
19867
19923
|
this.expected = parsed?.expected;
|
|
19924
|
+
this.code = parsed?.code;
|
|
19868
19925
|
}
|
|
19869
19926
|
}
|
|
19870
19927
|
function createEnvoyClient(config2) {
|
|
19871
19928
|
const baseUrl = normalizeEnvoyUrl(config2.baseUrl);
|
|
19872
19929
|
const timeoutMs = config2.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
19873
19930
|
const apiToken = config2.token;
|
|
19874
|
-
const request = async (path2, init) => {
|
|
19931
|
+
const request = async (path2, init, options = {}) => {
|
|
19875
19932
|
const url2 = `${baseUrl}${path2}`;
|
|
19876
19933
|
const attempts = config2.retry === false ? 1 : 2;
|
|
19877
|
-
|
|
19934
|
+
const tries = options.retry === false ? 1 : attempts;
|
|
19935
|
+
for (let attempt = 0;attempt < tries; attempt += 1) {
|
|
19878
19936
|
let response;
|
|
19879
19937
|
try {
|
|
19880
19938
|
const headers = new Headers(init.headers);
|
|
@@ -19883,10 +19941,10 @@ function createEnvoyClient(config2) {
|
|
|
19883
19941
|
response = await config2.fetch(url2, {
|
|
19884
19942
|
...init,
|
|
19885
19943
|
headers,
|
|
19886
|
-
signal: AbortSignal.timeout(timeoutMs)
|
|
19944
|
+
signal: AbortSignal.timeout(options.timeoutMs ?? timeoutMs)
|
|
19887
19945
|
});
|
|
19888
19946
|
} catch (error49) {
|
|
19889
|
-
if (attempt + 1 <
|
|
19947
|
+
if (attempt + 1 < tries) {
|
|
19890
19948
|
await waitForRetry();
|
|
19891
19949
|
continue;
|
|
19892
19950
|
}
|
|
@@ -19901,7 +19959,7 @@ function createEnvoyClient(config2) {
|
|
|
19901
19959
|
status: response.status,
|
|
19902
19960
|
responseBody
|
|
19903
19961
|
});
|
|
19904
|
-
if (response.status >= 500 && attempt + 1 <
|
|
19962
|
+
if (response.status >= 500 && attempt + 1 < tries) {
|
|
19905
19963
|
await waitForRetry();
|
|
19906
19964
|
continue;
|
|
19907
19965
|
}
|
|
@@ -19909,11 +19967,11 @@ function createEnvoyClient(config2) {
|
|
|
19909
19967
|
}
|
|
19910
19968
|
throw new Error("Envoy request exhausted retries");
|
|
19911
19969
|
};
|
|
19912
|
-
const post = (path2, body) => request(path2, {
|
|
19970
|
+
const post = (path2, body, options) => request(path2, {
|
|
19913
19971
|
method: "POST",
|
|
19914
19972
|
headers: { "Content-Type": "application/json" },
|
|
19915
19973
|
body: JSON.stringify(body)
|
|
19916
|
-
});
|
|
19974
|
+
}, options);
|
|
19917
19975
|
return {
|
|
19918
19976
|
subscribe: async (input) => InterestWireSchema.parse(JSON.parse(await post("/v1/interests/subscribe", {
|
|
19919
19977
|
session_id: input.sessionID,
|
|
@@ -19999,7 +20057,12 @@ function createEnvoyClient(config2) {
|
|
|
19999
20057
|
search.set("title", input.title);
|
|
20000
20058
|
const query = search.size === 0 ? "" : `?${search.toString()}`;
|
|
20001
20059
|
return SessionWireSchema.array().parse(JSON.parse(await request(`/v1/sessions${query}`, {})));
|
|
20002
|
-
}
|
|
20060
|
+
},
|
|
20061
|
+
replay: async (input) => ReplayWireSchema.parse(JSON.parse(await post("/v1/replay", {
|
|
20062
|
+
session_id: input.sessionID,
|
|
20063
|
+
subjects: input.subjects.map((cursor) => cursor.after === undefined ? { subject: cursor.subject } : { subject: cursor.subject, after: cursor.after }),
|
|
20064
|
+
...input.maxMessages === undefined ? {} : { max_messages: input.maxMessages }
|
|
20065
|
+
}, { timeoutMs: REPLAY_TIMEOUT_MS, retry: false })))
|
|
20003
20066
|
};
|
|
20004
20067
|
}
|
|
20005
20068
|
function waitForRetry() {
|
package/dist/src/server.js
CHANGED
|
@@ -13596,6 +13596,15 @@ var ArtifactVersionEventPayloadSchema = exports_external.object({
|
|
|
13596
13596
|
version: exports_external.object({ number: exports_external.number().optional(), summary: exports_external.string().nullish() }).optional(),
|
|
13597
13597
|
diff: exports_external.string().optional()
|
|
13598
13598
|
});
|
|
13599
|
+
var ArtifactHumanEditedEventPayloadSchema = exports_external.object({
|
|
13600
|
+
artifact_id: exports_external.string().optional(),
|
|
13601
|
+
name: exports_external.string().optional(),
|
|
13602
|
+
from_version: exports_external.number().int().optional(),
|
|
13603
|
+
to_version: exports_external.number().int().optional(),
|
|
13604
|
+
editors: exports_external.array(exports_external.string()).optional(),
|
|
13605
|
+
diff: exports_external.string().optional(),
|
|
13606
|
+
diff_truncated: exports_external.boolean().optional()
|
|
13607
|
+
});
|
|
13599
13608
|
var ArtifactReviewEventPayloadSchema = exports_external.object({
|
|
13600
13609
|
artifact_id: exports_external.string().optional(),
|
|
13601
13610
|
name: exports_external.string().optional(),
|
|
@@ -14135,7 +14144,7 @@ var dispatchToolSpecs = [
|
|
|
14135
14144
|
ops: [{ op: "delete_column", block: "table-123", index: 1 }],
|
|
14136
14145
|
precondition: { blocks: [{ id: "table-123", token: "sha256:current-table-token" }] }
|
|
14137
14146
|
},
|
|
14138
|
-
description: "Apply deterministic document edits: replace or delete quoted text, insert markdown at an anchor, retype an identified paragraph or typed block into a schema-declared typed block, delete or move a whole block by its id, or delete a table row or column in place. " + "Do not use it for review feedback or for reading; use dispatch comment, dispatch suggest, or dispatch doc-read instead. " + `For replace, delete, and quote anchors, find text as rendered: inline Markdown (**bold**, \`code\`) is tolerated and must be balanced; a leading '# ' matches a heading at any level. replace is inline: with is the new text of the matched span, so a marker of a different kind from the block's own stays literal text ('4. Design' written into a heading). A with that opens with a marker of the same kind as the matched block's own would write it twice and is INVALID_OP - including prose that merely looks like one ('1999. was a year' into an ordered item), which you write as text by escaping it ('1999\\. was a year'). The exception is a heading rename whose find carried a heading marker: replace(find="## Old", with="## New") gives '## New', and a different level applies only when find named the heading's actual level (find "## Old" with "### New" makes it an h3), since '# ' selects a heading without naming its level. A task item (\`- [ ]\` / \`- [x]\`) is ticked the same way: when find matches from the start of the item's text and with opens with '[x] ' or '[ ] ', the opening sets the item's box and the rest is its new text (replace(find="Write it", with="[x] Write it") ticks it); the same opening over a plain list item or a paragraph stays literal text. Any non-empty with that renders to no text - a line indented four spaces or a tab, which markdown reads as a code block, or whitespace alone - is INVALID_OP rather than a silent deletion; pass an empty with to delete the matched text on purpose - a list item, quote, typed block or footnote definition left holding only the emptied paragraph keeps it. ` + "with cannot open a new block: after a hard line break inside with (two trailing spaces, or a backslash, before the newline) a heading, bullet, '1.'/'1)' ordered, or '>' blockquote marker is INVALID_OP too, since that line would stay escaped text inside the matched block - use insert, plus delete for what it replaces, to add the block. A hard break in with is itself INVALID_OP when the matched text is in a heading or a table cell, which are written on one line. " + "A delete whose find is a block's entire text removes the block (a list emptied of its items goes too); delete with block removes any block by id, and move with block relocates one. A delete or retype that would take an ask block out of the document while its ask is open is refused, with nothing sent. delete_row and delete_column take a table block and a zero-based index, preserving the table block id and refusing to remove cells with open asks or unresolved comments. " + `Insert and move anchors also accept "start", "end", "heading:<exact heading text>", and "block:<id>"; dispatch doc-read --blocks lists every block's id, type, opening words and token, and each table's rows and cells with the indexes delete_row and delete_column take (GET /api/v1/artifacts/{artifact UUID}/blocks serves the same list). ` + "Optionally require the state just read: precondition selects exactly one of a document token from dispatch doc-read, or block {id, token} values from dispatch doc-read --blocks. A block guard must include every block the batch changes; Dispatch resolves quote targets and rejects an uncovered batch rather than applying it. Use a document token for insert or move, which depend on document order. Prefer block tokens when the covered content blocks are independent sections. Tokens include inline marks, so a fresh human comment also makes a stale edit fail. PRECONDITION_FAILED means re-read; EDIT_QUEUE_FULL means back off before retrying. " + "The result carries the document token this edit produced, so a chain of guarded edits passes each result's token as the next edit's precondition with no dispatch doc-read between them. " + "A batch that leaves the document exactly as it was mints no version, named or not, and the result says nothing changed and names each operation that did nothing. " + "A change a browser removes while the edit is in flight is never reported as applied: EDIT_LOST_TO_CONCURRENT_CHANGE means the write was refused and nothing was written, so re-read the document and decide again, as with PRECONDITION_FAILED; lost_ops on a successful result names operations whose text the live document no longer has, because the deletion landed after the version was written. " + "One replace per paragraph: more new paragraphs go in one insert after the last one you rewrote, since two inserts at one anchor come out reversed; read the passage back with dispatch doc-read afterwards. " + "A block id written as {#id} longer than 256 bytes is stored, but its block is no graph node, so no reference reaches it. " + `The spec holds requirements, design, and decisions - never progress, status, or timestamps; a record of open items or progress is a document of its own. ${OWNER_REFERENCE} ${SPEC_WRITING_POINTER}`,
|
|
14147
|
+
description: "Apply deterministic document edits: replace or delete quoted text, insert markdown at an anchor, retype an identified paragraph or typed block into a schema-declared typed block, delete or move a whole block by its id, or delete a table row or column in place. " + "Do not use it for review feedback or for reading; use dispatch comment, dispatch suggest, or dispatch doc-read instead. " + `For replace, delete, and quote anchors, find text as rendered: inline Markdown (**bold**, \`code\`) is tolerated and must be balanced; a leading '# ' matches a heading at any level. replace is inline: with is the new text of the matched span, so a marker of a different kind from the block's own stays literal text ('4. Design' written into a heading). A with that opens with a marker of the same kind as the matched block's own would write it twice and is INVALID_OP - including prose that merely looks like one ('1999. was a year' into an ordered item), which you write as text by escaping it ('1999\\. was a year'). The exception is a heading rename whose find carried a heading marker: replace(find="## Old", with="## New") gives '## New', and a different level applies only when find named the heading's actual level (find "## Old" with "### New" makes it an h3), since '# ' selects a heading without naming its level. A task item (\`- [ ]\` / \`- [x]\`) is ticked the same way: when find matches from the start of the item's text and with opens with '[x] ' or '[ ] ', the opening sets the item's box and the rest is its new text (replace(find="Write it", with="[x] Write it") ticks it); the same opening over a plain list item or a paragraph stays literal text. Any non-empty with that renders to no text - a line indented four spaces or a tab, which markdown reads as a code block, or whitespace alone - is INVALID_OP rather than a silent deletion; pass an empty with to delete the matched text on purpose - a list item, quote, typed block or footnote definition left holding only the emptied paragraph keeps it. ` + "with cannot open a new block: after a hard line break inside with (two trailing spaces, or a backslash, before the newline) a heading, bullet, '1.'/'1)' ordered, or '>' blockquote marker is INVALID_OP too, since that line would stay escaped text inside the matched block - use insert, plus delete for what it replaces, to add the block. A hard break in with is itself INVALID_OP when the matched text is in a heading or a table cell, which are written on one line. In a table cell, with is the cell's text: write a | in it bare, in code too, since code whose text has an odd run of backslashes right before a | cannot be stored in a cell and is INVALID_OP, naming the cell. " + "A delete whose find is a block's entire text removes the block (a list emptied of its items goes too); delete with block removes any block by id, and move with block relocates one. A delete or retype that would take an ask block out of the document while its ask is open is refused, with nothing sent. delete_row and delete_column take a table block and a zero-based index, preserving the table block id and refusing to remove cells with open asks or unresolved comments. " + `Insert and move anchors also accept "start", "end", "heading:<exact heading text>", and "block:<id>"; dispatch doc-read --blocks lists every block's id, type, opening words and token, and each table's rows and cells with the indexes delete_row and delete_column take (GET /api/v1/artifacts/{artifact UUID}/blocks serves the same list). ` + "Optionally require the state just read: precondition selects exactly one of a document token from dispatch doc-read, or block {id, token} values from dispatch doc-read --blocks. A block guard must include every block the batch changes; Dispatch resolves quote targets and rejects an uncovered batch rather than applying it. Use a document token for insert or move, which depend on document order. Prefer block tokens when the covered content blocks are independent sections. Tokens include inline marks, so a fresh human comment also makes a stale edit fail. PRECONDITION_FAILED means re-read; EDIT_QUEUE_FULL means back off before retrying. " + "The result carries the document token this edit produced, so a chain of guarded edits passes each result's token as the next edit's precondition with no dispatch doc-read between them. " + "A batch that leaves the document exactly as it was mints no version, named or not, and the result says nothing changed and names each operation that did nothing. " + "A change a browser removes while the edit is in flight is never reported as applied: EDIT_LOST_TO_CONCURRENT_CHANGE means the write was refused and nothing was written, so re-read the document and decide again, as with PRECONDITION_FAILED; lost_ops on a successful result names operations whose text the live document no longer has, because the deletion landed after the version was written. " + "One replace per paragraph: more new paragraphs go in one insert after the last one you rewrote, since two inserts at one anchor come out reversed; read the passage back with dispatch doc-read afterwards. " + "A block id written as {#id} longer than 256 bytes is stored, but its block is no graph node, so no reference reaches it. " + `The spec holds requirements, design, and decisions - never progress, status, or timestamps; a record of open items or progress is a document of its own. ${OWNER_REFERENCE} ${SPEC_WRITING_POINTER}`,
|
|
14139
14148
|
arguments: (z2) => ({
|
|
14140
14149
|
issue: z2.string().describe(ISSUE_REFERENCE).optional(),
|
|
14141
14150
|
project: z2.string().describe("Project key owning the document.").optional(),
|
|
@@ -14195,7 +14204,7 @@ var dispatchToolSpecs = [
|
|
|
14195
14204
|
name: "dispatch_artifact",
|
|
14196
14205
|
example: { issue: "DSP-1", name: "design.md", content: `# Design
|
|
14197
14206
|
` },
|
|
14198
|
-
description: "Attach a local file or inline text as an issue artifact or project document. Do not use it to edit a live document; use " + "dispatch doc-edit instead. Never upload a document's own name to change it (spec.md replaces the spec whole); edit it with dispatch doc-edit. Uploading the same name again makes its next version. Exactly one of path or content is required; a markdown document is at most 1 MiB and any other file at most 25 MiB. " + "Markdown is also refused with 413 CAP_EXCEEDED when it makes more than 65,536 elements (a block weighs 3, and an empty list item, quote, footnote definition or typed block 3 more for the empty paragraph it holds; a table cell 4, a hard line break 3, an image 3, a piece of inline HTML 2, an autolink 2, a footnote reference 2; inline syntax, marks, lines of text and backslash escapes or character references 1 each), or when a new version would leave the document's stored markdown (what its text reads back as) longer than 1 MiB or making more than 65,536 elements, and longer or heavier than before; shorten it or split it across documents. " + "Markdown holding an ask block whose body breaks its content rule (one or more question paragraphs, then at most one bullet list of options, last) is refused with 400 INVALID_ASK_BLOCK; a new version of a document is held to it only for the asks it writes or changes. " + `${OWNER_REFERENCE}`,
|
|
14207
|
+
description: "Attach a local file or inline text as an issue artifact or project document. Do not use it to edit a live document; use " + "dispatch doc-edit instead. Never upload a document's own name to change it (spec.md replaces the spec whole); edit it with dispatch doc-edit. Uploading the same name again makes its next version. Exactly one of path or content is required; a markdown document is at most 1 MiB and any other file at most 25 MiB. " + "Markdown is also refused with 413 CAP_EXCEEDED when it makes more than 65,536 elements (a block weighs 3, and an empty list item, quote, footnote definition or typed block 3 more for the empty paragraph it holds; a table cell 4, a hard line break 3, an image 3, a piece of inline HTML 2, an autolink 2, a footnote reference 2; inline syntax, marks, lines of text and backslash escapes or character references 1 each), or when a new version would leave the document's stored markdown (what its text reads back as) longer than 1 MiB or making more than 65,536 elements, and longer or heavier than before; shorten it or split it across documents. " + "In a table row, write every | in a cell as \\|, in code too: a | after two backslashes ends the cell, and a row with more cells than its table is refused. " + "Markdown holding an ask block whose body breaks its content rule (one or more question paragraphs, then at most one bullet list of options, last) is refused with 400 INVALID_ASK_BLOCK; a new version of a document is held to it only for the asks it writes or changes. " + `${OWNER_REFERENCE}`,
|
|
14199
14208
|
arguments: (z2) => ({
|
|
14200
14209
|
issue: z2.string().describe(ISSUE_REFERENCE).optional(),
|
|
14201
14210
|
project: z2.string().describe("Project key for an unlinked document.").optional(),
|
|
@@ -15293,6 +15302,7 @@ var envoyToolSchemas = new Map;
|
|
|
15293
15302
|
// ../envoy-client/src/transport.ts
|
|
15294
15303
|
var DEFAULT_TIMEOUT_MS = 5000;
|
|
15295
15304
|
var RETRY_DELAY_MS = 250;
|
|
15305
|
+
var REPLAY_TIMEOUT_MS = 1e4;
|
|
15296
15306
|
var InterestWireSchema = exports_external.object({
|
|
15297
15307
|
session_id: exports_external.string(),
|
|
15298
15308
|
machine_id: exports_external.string(),
|
|
@@ -15327,12 +15337,33 @@ var RoleHeldWireSchema = exports_external.object({
|
|
|
15327
15337
|
});
|
|
15328
15338
|
var ErrorWireSchema = exports_external.object({
|
|
15329
15339
|
error: exports_external.string(),
|
|
15340
|
+
code: exports_external.string().optional(),
|
|
15330
15341
|
expected: exports_external.array(exports_external.string()).optional()
|
|
15331
15342
|
});
|
|
15332
15343
|
var EnvelopeResponseSchema = EnvelopeSchema.extend({
|
|
15333
15344
|
recipient: exports_external.string().optional(),
|
|
15334
15345
|
holder: exports_external.string().optional()
|
|
15335
15346
|
});
|
|
15347
|
+
var ReplayWireSchema = exports_external.object({
|
|
15348
|
+
stream: exports_external.object({
|
|
15349
|
+
id: exports_external.string().min(1).optional(),
|
|
15350
|
+
created: exports_external.string(),
|
|
15351
|
+
first_seq: exports_external.number().int().nonnegative(),
|
|
15352
|
+
last_seq: exports_external.number().int().nonnegative()
|
|
15353
|
+
}),
|
|
15354
|
+
subjects: exports_external.array(exports_external.object({
|
|
15355
|
+
subject: exports_external.string(),
|
|
15356
|
+
messages: exports_external.array(exports_external.object({
|
|
15357
|
+
seq: exports_external.number().int().positive(),
|
|
15358
|
+
subject: exports_external.string(),
|
|
15359
|
+
time: exports_external.string(),
|
|
15360
|
+
data: exports_external.string()
|
|
15361
|
+
})),
|
|
15362
|
+
next_after: exports_external.number().int().nonnegative(),
|
|
15363
|
+
more: exports_external.boolean(),
|
|
15364
|
+
refused: exports_external.string().optional()
|
|
15365
|
+
}))
|
|
15366
|
+
});
|
|
15336
15367
|
function expandSubscriptionTopics(topics) {
|
|
15337
15368
|
const expanded = new Set;
|
|
15338
15369
|
for (const topic of topics) {
|
|
@@ -15355,6 +15386,7 @@ class EnvoyApiError extends Error {
|
|
|
15355
15386
|
details;
|
|
15356
15387
|
name = "EnvoyApiError";
|
|
15357
15388
|
expected;
|
|
15389
|
+
code;
|
|
15358
15390
|
constructor(details) {
|
|
15359
15391
|
let parsed;
|
|
15360
15392
|
try {
|
|
@@ -15365,16 +15397,18 @@ class EnvoyApiError extends Error {
|
|
|
15365
15397
|
super(parsed === undefined ? `${details.method} ${details.url} failed with ${details.status}: ${details.responseBody}` : `${parsed.error}${parsed.expected === undefined ? "" : ` (expected: ${parsed.expected.join(", ")})`}`);
|
|
15366
15398
|
this.details = details;
|
|
15367
15399
|
this.expected = parsed?.expected;
|
|
15400
|
+
this.code = parsed?.code;
|
|
15368
15401
|
}
|
|
15369
15402
|
}
|
|
15370
15403
|
function createEnvoyClient(config2) {
|
|
15371
15404
|
const baseUrl = normalizeEnvoyUrl(config2.baseUrl);
|
|
15372
15405
|
const timeoutMs = config2.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
15373
15406
|
const apiToken = config2.token;
|
|
15374
|
-
const request = async (path2, init) => {
|
|
15407
|
+
const request = async (path2, init, options = {}) => {
|
|
15375
15408
|
const url2 = `${baseUrl}${path2}`;
|
|
15376
15409
|
const attempts = config2.retry === false ? 1 : 2;
|
|
15377
|
-
|
|
15410
|
+
const tries = options.retry === false ? 1 : attempts;
|
|
15411
|
+
for (let attempt = 0;attempt < tries; attempt += 1) {
|
|
15378
15412
|
let response;
|
|
15379
15413
|
try {
|
|
15380
15414
|
const headers = new Headers(init.headers);
|
|
@@ -15383,10 +15417,10 @@ function createEnvoyClient(config2) {
|
|
|
15383
15417
|
response = await config2.fetch(url2, {
|
|
15384
15418
|
...init,
|
|
15385
15419
|
headers,
|
|
15386
|
-
signal: AbortSignal.timeout(timeoutMs)
|
|
15420
|
+
signal: AbortSignal.timeout(options.timeoutMs ?? timeoutMs)
|
|
15387
15421
|
});
|
|
15388
15422
|
} catch (error49) {
|
|
15389
|
-
if (attempt + 1 <
|
|
15423
|
+
if (attempt + 1 < tries) {
|
|
15390
15424
|
await waitForRetry();
|
|
15391
15425
|
continue;
|
|
15392
15426
|
}
|
|
@@ -15401,7 +15435,7 @@ function createEnvoyClient(config2) {
|
|
|
15401
15435
|
status: response.status,
|
|
15402
15436
|
responseBody
|
|
15403
15437
|
});
|
|
15404
|
-
if (response.status >= 500 && attempt + 1 <
|
|
15438
|
+
if (response.status >= 500 && attempt + 1 < tries) {
|
|
15405
15439
|
await waitForRetry();
|
|
15406
15440
|
continue;
|
|
15407
15441
|
}
|
|
@@ -15409,11 +15443,11 @@ function createEnvoyClient(config2) {
|
|
|
15409
15443
|
}
|
|
15410
15444
|
throw new Error("Envoy request exhausted retries");
|
|
15411
15445
|
};
|
|
15412
|
-
const post = (path2, body) => request(path2, {
|
|
15446
|
+
const post = (path2, body, options) => request(path2, {
|
|
15413
15447
|
method: "POST",
|
|
15414
15448
|
headers: { "Content-Type": "application/json" },
|
|
15415
15449
|
body: JSON.stringify(body)
|
|
15416
|
-
});
|
|
15450
|
+
}, options);
|
|
15417
15451
|
return {
|
|
15418
15452
|
subscribe: async (input) => InterestWireSchema.parse(JSON.parse(await post("/v1/interests/subscribe", {
|
|
15419
15453
|
session_id: input.sessionID,
|
|
@@ -15499,7 +15533,12 @@ function createEnvoyClient(config2) {
|
|
|
15499
15533
|
search.set("title", input.title);
|
|
15500
15534
|
const query = search.size === 0 ? "" : `?${search.toString()}`;
|
|
15501
15535
|
return SessionWireSchema.array().parse(JSON.parse(await request(`/v1/sessions${query}`, {})));
|
|
15502
|
-
}
|
|
15536
|
+
},
|
|
15537
|
+
replay: async (input) => ReplayWireSchema.parse(JSON.parse(await post("/v1/replay", {
|
|
15538
|
+
session_id: input.sessionID,
|
|
15539
|
+
subjects: input.subjects.map((cursor) => cursor.after === undefined ? { subject: cursor.subject } : { subject: cursor.subject, after: cursor.after }),
|
|
15540
|
+
...input.maxMessages === undefined ? {} : { max_messages: input.maxMessages }
|
|
15541
|
+
}, { timeoutMs: REPLAY_TIMEOUT_MS, retry: false })))
|
|
15503
15542
|
};
|
|
15504
15543
|
}
|
|
15505
15544
|
function waitForRetry() {
|
package/package.json
CHANGED
package/skills/envoy/SKILL.md
CHANGED
|
@@ -66,6 +66,6 @@ Every topic starts with `notifications.`.
|
|
|
66
66
|
|
|
67
67
|
- Send to one session with `envoy_send`, to the one session that holds a named duty with a role, and to a group with a team topic.
|
|
68
68
|
- Publish to a role with `envoy_publish(topic="notifications.role.<role>", ...)`; never subscribe to a role topic, because the listener forwards a role message to its holder only. A publish returns the live `holder`; an unheld role returns an error. `envoy_role_get(role=...)` finds the holder first.
|
|
69
|
-
- For a team, the group subscribes to `notifications.team.<name>.>`, and any member publishes to `notifications.team.<name>.all`. Envoy treats any other `notifications.` topic that no source in the table uses the same way. It reaches only the sessions subscribed when it is published (yours too, when you subscribe, unless you are a serve-port session, which receives it only through a listener run with `ENVOY_SUBSCRIBE_ALL_TOPICS=1` and never its own publish), and is not kept for a session that subscribes later. A topic outside `notifications.` is refused.
|
|
69
|
+
- For a team, the group subscribes to `notifications.team.<name>.>`, and any member publishes to `notifications.team.<name>.all`. Envoy treats any other `notifications.` topic that no source in the table uses the same way. It reaches only the sessions subscribed and connected when it is published (yours too, when you subscribe, unless you are a serve-port session, which receives it only through a listener run with `ENVOY_SUBSCRIBE_ALL_TOPICS=1` and never its own publish), and is not kept for a session that subscribes later or whose connection was down. A topic outside `notifications.` is refused.
|
|
70
70
|
- How a role claim survives a resume, a fork or a dead process: `skill://envoy/references/roles.md`.
|
|
71
71
|
- In a Legion pane, the plugin claims your role at session start; read `skill://envoy/references/legion.md` before you claim, release or publish to a role, or answer a role delivery failure there.
|
|
@@ -53,7 +53,7 @@ envoy:
|
|
|
53
53
|
|
|
54
54
|
It depends on where a session takes its deliveries, not on whether the topic is built in or your own.
|
|
55
55
|
|
|
56
|
-
- A session that holds its own NATS connection (every Oh My Pi session, and a Claude Code one) receives every message published on a subject it subscribes to, whoever sent it: its own publish included, and a subagent's publish to a topic the parent subscribes to. This holds for a topic the stream keeps and for a live team topic alike.
|
|
56
|
+
- A session that holds its own NATS connection (every Oh My Pi session, and a Claude Code one) receives every message published on a subject it subscribes to, whoever sent it: its own publish included, and a subagent's publish to a topic the parent subscribes to. This holds for a topic the stream keeps and for a live team topic alike while it is connected. After an outage they differ: once its connection or subscription is back, the session recovers from the stream what was published meanwhile on its own inbox and on each kept topic it follows, up to 72 hours back, and is shown each message once, marked as published while its connection was down; what a team topic carried meanwhile is gone.
|
|
57
57
|
- A session the listener delivers to by push, over a serve port (such as the OpenCode plugin), does not receive its own publish, and receives a team topic only through the listener that owns it when that listener runs with `ENVOY_SUBSCRIBE_ALL_TOPICS=1` (the shared hub's listener does not).
|
|
58
58
|
- A role is not a subject its holder subscribes to: the listener finds the live holder and forwards the message to it, and drops the message when that holder is the message's source session.
|
|
59
59
|
|
|
@@ -38,6 +38,8 @@ You own one issue tree from its first decision to the merge that closes it. The
|
|
|
38
38
|
- On each `child-closed`, read the child's handoff. Edit the spec of each open child the result changes, and take out each child it makes unneeded with `park_child` and say why with `dispatch comment`.
|
|
39
39
|
- Before you park a child for good (one made unneeded, or parked so its tree can finish), set the `--blocked-by` list of each sibling that waits on it again without it (one call with the rest of its list, or `--clear-blocked-by` when it was the only one), or park that sibling too: a parked child is stopped, not done, and its dependents stay held with no notice. A park followed by `rerun_child` keeps its dependents' links: they wait through the rerun and start when the rerun's merge closes the child.
|
|
40
40
|
- A released child runs in your tree's admission slot, never its own, and never becomes a root while your tree is live.
|
|
41
|
+
- A root that waited in the admission queue, never ran and never left another tree joins your tree once a person makes it your child, if it is `todo`, and otherwise at your release of it (the `child-status` row of [wake routing](skill://legion-architect/references/wake-routing.md)); once in, it is held for its blockers like any child you release. A root that left another tree, handed over by its finished tree or admitted as an orphan, stays its own tree and never joins yours, even one that was your root's child before your root was admitted again. A labelled `todo` issue under a child you have not released, unless it ran as a tree of its own or left another tree, is held under that unreleased child (not a blocked-by hold): it waits for no slot until your release of it or of that child brings it in, with any `todo` child of its own.
|
|
42
|
+
- A release that cannot take a child in is refused 409 and writes nothing, its message naming the child and why: release the rest of the wave again without it. `CHILD_NOT_ADOPTABLE` is an issue that runs or ran as a tree of its own, carries a run handed over to it, or was handed over (or admitted as an orphan) as a root of its own: leave it there, or ask the controller. `CHILD_IN_OTHER_TREE` is another tree's child. When the tree it names is a root held under an unreleased child of yours (a root that queued and never ran, so it has no architect of its own), release that root, or the unreleased child it sits under, instead: its children come with it. Otherwise re-parent the child back in Dispatch, or ask that tree's architect or the controller.
|
|
41
43
|
|
|
42
44
|
## 3. Children complete
|
|
43
45
|
|
|
@@ -19,7 +19,7 @@ and 7 (a conflict, the hand-over, the close) is in
|
|
|
19
19
|
| Wake | Procedure |
|
|
20
20
|
| --- | --- |
|
|
21
21
|
| `carried-gate-opened` | Only a root whose catch-up said `carried` gets it. Its `version` and `reason`: a human approved the old root's spec document at `version`, which opened the gate this root carried; the daemon has released any READY it refused and started any child waiting on it. It is not `design-approved`: do not proceed to section 2 or release a wave because of it, and start nothing. |
|
|
22
|
-
| `child-status` | A child of your tree left the workflow or re-entered it; the reason names the child and its new status
|
|
22
|
+
| `child-status` | A child of your tree left the workflow or re-entered it, or joined your tree from the admission queue; the reason names the child and its new status, or reads `<KEY> joined the tree of <ROOT> from the admission queue`. `todo` (a human's move, your `release_children`, or your `rerun_child`) means it runs again under your tree from planning, and the daemon starts it; you start nothing. A child held for a blocker starts the same way once nothing holds it, and the reason, naming it and its root, says what cleared the hold: its last blocker's close, naming the issues it waited on, now done, or a person or you taking its blocked-by links off; you start nothing for it either. `backlog`, `icebox` or `triage` (a human's move, or your `park_child`) means the daemon has suspended its workers, and it advances no further until it is set back to `todo`. Joined from the admission queue means a root that waited for a slot and never ran, which a person made a child of your tree or your `release_children` adopted, now runs in your tree with no slot or architect of its own, from planning when it is `todo` and parked otherwise; one that waits on an unfinished blocked-by issue is held for that blocker like any other child, and starts when nothing holds it; you start nothing. What the rest of the tree does is your decision. |
|
|
23
23
|
| `child-closed` | Read the child completion and remaining open children. Re-scope open work, park obsolete work for good with `park_child` (first take its link off each sibling that waits on it, or park that sibling too, as section 2 says), and file any new child the result calls for and release it at once with `release_children`, blocked by what it builds on (section 2). When a worker waits on this child for a missing surface (see `phase-finished`), tell it to continue with `envoy_publish` to its role topic. The last child's close is not the end-game (section 3). |
|
|
24
24
|
| `child-release-stalled` | Its `reason` names a child your `release_children` released while Dispatch already showed it `todo`, and that child's parent, whose release has not reached the daemon after three tries. Read the parent's record with the `legion` tool's `read_record` (`issue` the parent); run `release_children` for the child again once the parent is recorded in your tree, and tell the controller if it never is. |
|
|
25
25
|
| `child-not-recorded` | Its `reason` names a child your tree did not record and why: its repository resolves to none (neither it nor an ancestor names one, and the project has no default), or to one not mapped to the project, or to one the daemon leaves out for a missing GitHub App. The daemon said why on the child too. Set the child's repository to one of the project's (`dispatch issue-update --repo owner/name`), then release it again with `release_children`; tell the controller when the repository it needs is not served. |
|
|
@@ -7,16 +7,16 @@ waits, or neither. Read its key against `admission` and its tree's root.
|
|
|
7
7
|
| What `read_state` shows | What it means | What you do |
|
|
8
8
|
|---|---|---|
|
|
9
9
|
| The key is in `admission.active` | A root holding a slot: its tree runs, `admitted` until its architect starts the first phase | Nothing, beyond [trees waiting on a root claim](skill://legion-controller/SKILL.md#trees-waiting-on-a-root-claim) |
|
|
10
|
-
| The key is in `admission.waiting` and not in `admission.blocked` | A root handed to Legion in `todo`, queued for the next free slot: a root a person answered while it waited in `awaiting_person` comes first, oldest answer first, then every root that never ran, in Dispatch rank order | Nothing: it takes the next free slot by itself |
|
|
10
|
+
| The key is in `admission.waiting` and not in `admission.blocked` | A root handed to Legion in `todo`, queued for the next free slot: a root a person answered while it waited in `awaiting_person` comes first, oldest answer first, then every root that never ran, in Dispatch rank order. A root that left another tree (handed over by its finished tree, or admitted as an orphan) waits here as its own root even under a live tree, the old root's return included: no tree adopts it | Nothing: it takes the next free slot by itself |
|
|
11
11
|
| `phase: awaiting_person`, `waitingOn` | A root that already ran and now waits only on a person: an answer to its architect's question (`waitingOn.kind` `ask`) or a review of its spec (`gate`). It holds no slot, its architect session is suspended, and its tree stays live. Unanswered, it is in neither list; `waitingOn.answeredAt` means the daemon has promoted it to the front of `admission.waiting` | Nothing: it is not something to admit, label or park, and the slot it returned is no reason to take an issue ([Free slots](skill://legion-controller/SKILL.md#free-slots)). The daemon resumes it in its own architect session at the next free slot. If its label goes or a person or outside session claims it on Dispatch, the daemon sends `unslotted` and does not resume it |
|
|
12
12
|
| The key is in `admission.waiting` and in `admission.blocked` | A root in the line that waits on the Dispatch issues `admission.blocked` names: admission passes it over, and it takes no slot, until every one of them is `done` | Nothing: the wait ends when each blocker reaches `done` (a person's close, or the blocker's own merge) or its link is taken off (by a person or the tree's architect). Name the wait in the day's report |
|
|
13
|
-
| `phase: admitted`, in neither list, and Dispatch shows a `child_of` parent | A child: it runs or waits with its tree, the root its `child_of` chain reaches. Under a root whose `phase` is `done` it waits with it | Leave it to its tree. It runs again when its root does, or as a root of its own once it is `todo` with the `legion` label |
|
|
13
|
+
| `phase: admitted`, in neither list, and Dispatch shows a `child_of` parent | A child: it runs or waits with its tree, the root its `child_of` chain reaches. Under a root whose `phase` is `done` it waits with it. A labelled `todo` issue here is not in the line either: one whose recorded parent is in a live tree runs in that tree (the daemon adopted it there), or is held with that parent while the parent itself is held under an unreleased child, and one whose parent is not recorded but sits under a live tree is held under an unreleased child of that tree until its architect releases it or its parent. That hold is not `admission.blocked`, which names only blocked-by links | Leave it to its tree. It runs again when its root does, or as a root of its own once it is `todo` with the `legion` label and no live tree is above it |
|
|
14
14
|
| `phase: admitted`, `status: todo`, in neither list, no parent | A root a person took out of the line by taking its label off | Nothing: the label puts it back in the line, and a person's label is their decision |
|
|
15
15
|
| `phase: done` while `status` is `in_progress`, `testing`, `needs_review` or `retro` | Legion is not running it under that status, and it sent the `unslotted` wake. Either someone other than Legion moved a root that waited for a slot there, and the daemon took it out of the workflow at that move since no slot would ever reach it; or a root waiting on a person could not be resumed — its `legion` label is gone, or a person or an outside session took its Dispatch claim — and the daemon took it out the same way, leaving the `in_progress` it ran with. A parked child stays parked. Legion starts nothing for any of them | Re-admit nothing yourself: `dispatch read` shows who works it now. Name it in your summary; `todo` with the `legion` label queues it again |
|
|
16
16
|
|
|
17
|
-
A root in the workflow with no slot that is in neither `admission.waiting
|
|
18
|
-
row above does not stay that way: every pass of admission (a Dispatch
|
|
19
|
-
read, its tick) takes it out of the workflow and sends the `unslotted`
|
|
20
|
-
`awaiting_person` root only when its label or its Dispatch claim makes a
|
|
21
|
-
only, so a controller that starts later finds such a root by the
|
|
22
|
-
does.
|
|
17
|
+
A root in the workflow with no slot that is in neither `admission.waiting`, nor the label-removed
|
|
18
|
+
row above, nor `todo` under a live tree does not stay that way: every pass of admission (a Dispatch
|
|
19
|
+
event, the daemon's boot read, its tick) takes it out of the workflow and sends the `unslotted`
|
|
20
|
+
wake — an unanswered `awaiting_person` root only when its label or its Dispatch claim makes a
|
|
21
|
+
resume unsafe. The wake is live only, so a controller that starts later finds such a root by the
|
|
22
|
+
last row, as its start procedure does.
|
|
@@ -14,8 +14,9 @@ which you have; those need no runner.
|
|
|
14
14
|
- `psql` — the Dispatch browser suite (`bun run e2e`) resets its database for every spec through
|
|
15
15
|
`packages/dispatch/e2e/psql.ts`, which `seed.ts` calls, and the local stack (`bun run
|
|
16
16
|
dispatch:local`, `packages/dispatch/e2e/local-instance.ts`) refuses to start without `go`,
|
|
17
|
-
`psql`, `flock` and `
|
|
18
|
-
given. The image carries `go`, `flock` and `
|
|
17
|
+
`psql`, `flock`, `openssl` and `python3` on `PATH`, and without `docker` too unless a
|
|
18
|
+
`DATABASE_URL` is given. The image carries `go`, `flock`, `openssl` and `python3`, a
|
|
19
|
+
`DATABASE_URL` stands in for `docker`,
|
|
19
20
|
and `psql` was the one a role had no way to supply — so a role whose change owes that suite can
|
|
20
21
|
run it.
|
|
21
22
|
- `redis-cli` — no repository of the project speaks Redis today; it is carried anyway, at about a
|