@sigrun/ext-gateway 0.0.0-stage → 0.1.2

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.
Files changed (82) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/LICENSE +21 -0
  3. package/README.md +241 -2
  4. package/dist/access/codes.d.ts +18 -0
  5. package/dist/access/codes.js +34 -0
  6. package/dist/access/link-codes.d.ts +33 -0
  7. package/dist/access/link-codes.js +63 -0
  8. package/dist/access/loop-guard.d.ts +17 -0
  9. package/dist/access/loop-guard.js +42 -0
  10. package/dist/access/pairing.d.ts +74 -0
  11. package/dist/access/pairing.js +131 -0
  12. package/dist/commands/chat.d.ts +66 -0
  13. package/dist/commands/chat.js +371 -0
  14. package/dist/commands/link.d.ts +63 -0
  15. package/dist/commands/link.js +77 -0
  16. package/dist/commands/pair.d.ts +72 -0
  17. package/dist/commands/pair.js +110 -0
  18. package/dist/config.d.ts +65 -0
  19. package/dist/config.js +219 -0
  20. package/dist/engine.d.ts +108 -0
  21. package/dist/engine.js +645 -0
  22. package/dist/inbound/albums.d.ts +39 -0
  23. package/dist/inbound/albums.js +62 -0
  24. package/dist/inbound/envelope.d.ts +19 -0
  25. package/dist/inbound/envelope.js +39 -0
  26. package/dist/inbound/groups.d.ts +29 -0
  27. package/dist/inbound/groups.js +55 -0
  28. package/dist/inbound/media.d.ts +20 -0
  29. package/dist/inbound/media.js +88 -0
  30. package/dist/inbound/pipeline.d.ts +144 -0
  31. package/dist/inbound/pipeline.js +600 -0
  32. package/dist/inbound/tokens.d.ts +38 -0
  33. package/dist/inbound/tokens.js +42 -0
  34. package/dist/index.d.ts +86 -0
  35. package/dist/index.js +136 -0
  36. package/dist/interactions/answer.d.ts +42 -0
  37. package/dist/interactions/answer.js +112 -0
  38. package/dist/interactions/chat.d.ts +28 -0
  39. package/dist/interactions/chat.js +199 -0
  40. package/dist/interactions/prompts.d.ts +68 -0
  41. package/dist/interactions/prompts.js +246 -0
  42. package/dist/interactions/receipts.d.ts +7 -0
  43. package/dist/interactions/receipts.js +69 -0
  44. package/dist/interactions/render.d.ts +39 -0
  45. package/dist/interactions/render.js +122 -0
  46. package/dist/jobs.d.ts +34 -0
  47. package/dist/jobs.js +72 -0
  48. package/dist/log.d.ts +21 -0
  49. package/dist/log.js +42 -0
  50. package/dist/notify.d.ts +21 -0
  51. package/dist/notify.js +64 -0
  52. package/dist/outbound/delivery.d.ts +131 -0
  53. package/dist/outbound/delivery.js +404 -0
  54. package/dist/outbound/markdown.d.ts +36 -0
  55. package/dist/outbound/markdown.js +286 -0
  56. package/dist/outbound/render.d.ts +24 -0
  57. package/dist/outbound/render.js +65 -0
  58. package/dist/outbound/settled.d.ts +81 -0
  59. package/dist/outbound/settled.js +271 -0
  60. package/dist/outbound/stream.d.ts +38 -0
  61. package/dist/outbound/stream.js +205 -0
  62. package/dist/section.d.ts +16 -0
  63. package/dist/section.js +55 -0
  64. package/dist/session-meta.d.ts +18 -0
  65. package/dist/session-meta.js +39 -0
  66. package/dist/state/bindings.d.ts +55 -0
  67. package/dist/state/bindings.js +128 -0
  68. package/dist/state/identities.d.ts +65 -0
  69. package/dist/state/identities.js +135 -0
  70. package/dist/state/json-file.d.ts +64 -0
  71. package/dist/state/json-file.js +156 -0
  72. package/dist/state/ledger.d.ts +176 -0
  73. package/dist/state/ledger.js +256 -0
  74. package/dist/state/seen.d.ts +41 -0
  75. package/dist/state/seen.js +77 -0
  76. package/dist/status-route.d.ts +32 -0
  77. package/dist/status-route.js +26 -0
  78. package/dist/supervisor.d.ts +153 -0
  79. package/dist/supervisor.js +273 -0
  80. package/dist/tools.d.ts +27 -0
  81. package/dist/tools.js +207 -0
  82. package/package.json +54 -5
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Markdown splitting and stripping (spec §8.6): the outbound chunker's
3
+ * step 1 (split at paragraph, then line, then hard boundaries, with
4
+ * fence close-and-reopen) and the plain-format renderer's stripper
5
+ * (headings, emphasis, links → `text (url)`, fences → plain blocks).
6
+ *
7
+ * Pure functions over strings — no imports, no clock, no I/O.
8
+ */
9
+ /**
10
+ * Split `text` into chunks of at most `limit` characters, preferring
11
+ * paragraph boundaries, then line boundaries, then hard cuts.
12
+ *
13
+ * Fence rule: a code fence cut by a split is CLOSED in the earlier
14
+ * chunk and REOPENED with its original info string at the head of the
15
+ * next. An unterminated fence in the input is treated as running to
16
+ * the end (the last chunk's close terminates it — every emitted chunk
17
+ * is well-formed on its own).
18
+ *
19
+ * Every EMITTED chunk respects the limit: the close suffix a
20
+ * fence-interior chunk carries ("\n```") is reserved in each fit
21
+ * check, and hard-split pieces shrink by the ACTUAL reopen width
22
+ * (info string included — "typescript" reopens 13 chars wide, so a
23
+ * fixed reserve under-sizes pieces).
24
+ *
25
+ * The join of the chunks is a lossless representation of the input
26
+ * apart from the inserted fence markers and the whitespace the
27
+ * paragraph/line splits consumed.
28
+ */
29
+ export declare function splitMarkdown(text: string, limit: number): string[];
30
+ /**
31
+ * Strip markdown to plain text (the `plain` capability's formatter):
32
+ * headings lose their `#` markers, emphasis markers are removed, links
33
+ * render as `text (url)`, and fenced code blocks become their inner
34
+ * lines. Structure (paragraphs, line breaks) is preserved.
35
+ */
36
+ export declare function stripMarkdown(text: string): string;
@@ -0,0 +1,286 @@
1
+ /**
2
+ * Markdown splitting and stripping (spec §8.6): the outbound chunker's
3
+ * step 1 (split at paragraph, then line, then hard boundaries, with
4
+ * fence close-and-reopen) and the plain-format renderer's stripper
5
+ * (headings, emphasis, links → `text (url)`, fences → plain blocks).
6
+ *
7
+ * Pure functions over strings — no imports, no clock, no I/O.
8
+ */
9
+ /**
10
+ * Split `text` into chunks of at most `limit` characters, preferring
11
+ * paragraph boundaries, then line boundaries, then hard cuts.
12
+ *
13
+ * Fence rule: a code fence cut by a split is CLOSED in the earlier
14
+ * chunk and REOPENED with its original info string at the head of the
15
+ * next. An unterminated fence in the input is treated as running to
16
+ * the end (the last chunk's close terminates it — every emitted chunk
17
+ * is well-formed on its own).
18
+ *
19
+ * Every EMITTED chunk respects the limit: the close suffix a
20
+ * fence-interior chunk carries ("\n```") is reserved in each fit
21
+ * check, and hard-split pieces shrink by the ACTUAL reopen width
22
+ * (info string included — "typescript" reopens 13 chars wide, so a
23
+ * fixed reserve under-sizes pieces).
24
+ *
25
+ * The join of the chunks is a lossless representation of the input
26
+ * apart from the inserted fence markers and the whitespace the
27
+ * paragraph/line splits consumed.
28
+ */
29
+ export function splitMarkdown(text, limit) {
30
+ if (limit <= 0) {
31
+ // A zero/negative limit has no legal chunk; the caller's config
32
+ // validation owns that error. Hard-splitting into empty strings
33
+ // would loop forever, so the honest fallback is the whole text.
34
+ return text === "" ? [] : [text];
35
+ }
36
+ const lines = text.split("\n");
37
+ const chunks = [];
38
+ /** The chunk under construction, joined with "\n". */
39
+ let current = [];
40
+ let currentLength = 0;
41
+ /** Whether a code fence is currently open (independent of its info). */
42
+ let fenceOpen = false;
43
+ /** The open fence's info string (e.g. "ts"), undefined when bare. */
44
+ let openFence;
45
+ /** Reconstructed open-fence line for reopening (with info string). */
46
+ const fenceReopen = () => "```" + (openFence === undefined ? "" : openFence);
47
+ /** The close suffix a chunk ending inside an open fence carries
48
+ * ("\n```") — reserved in EVERY fit check so an emitted chunk can
49
+ * never exceed the limit (the reservation is pessimistic: a chunk
50
+ * the input's own close line terminates simply comes out smaller). */
51
+ const CLOSE_COST = 4;
52
+ const flush = () => {
53
+ if (current.length === 0)
54
+ return;
55
+ let chunk = current.join("\n");
56
+ // A chunk that IS just the fence (re)open marker — bare or with an
57
+ // info string — is INTERIOR: it was armed as the head of a split
58
+ // chunk whose body went to the next chunk. endsWith("```") would
59
+ // mistake it for the input's own close line and emit an UNCLOSED
60
+ // marker (the lone-``` regression); the close suffix belongs here.
61
+ const loneMarker = fenceOpen &&
62
+ /^```[^`]*$/.test(chunk) &&
63
+ !chunk.includes("\n");
64
+ if (fenceOpen && (loneMarker || !chunk.endsWith("```"))) {
65
+ // The chunk ends INSIDE a fence: close it so the chunk stands
66
+ // alone (the next chunk reopens).
67
+ chunk += "\n```";
68
+ }
69
+ chunks.push(chunk);
70
+ current = [];
71
+ currentLength = 0;
72
+ };
73
+ /**
74
+ * Close the current chunk and reopen the fence when one is open —
75
+ * the shared split point for every path that starts a fresh chunk
76
+ * (line overflow, paragraph boundary, hard-split pieces).
77
+ */
78
+ const closeAndReopen = () => {
79
+ flush();
80
+ if (fenceOpen) {
81
+ const reopen = fenceReopen();
82
+ current.push(reopen);
83
+ currentLength = reopen.length;
84
+ }
85
+ };
86
+ const pushLine = (line, lineLength) => {
87
+ // A chunk that STARTS inside an open fence carries the reopen as
88
+ // its head line — the well-formed invariant (every chunk is a
89
+ // standalone fence) holds no matter which path emptied the chunk.
90
+ if (current.length === 0 && fenceOpen) {
91
+ const reopen = fenceReopen();
92
+ current.push(reopen);
93
+ currentLength = reopen.length;
94
+ }
95
+ const joinCost = current.length === 0 ? 0 : 1;
96
+ const closeCost = fenceOpen ? CLOSE_COST : 0;
97
+ if (currentLength + joinCost + lineLength + closeCost <= limit) {
98
+ current.push(line);
99
+ currentLength += joinCost + lineLength;
100
+ return;
101
+ }
102
+ // The line does not fit: split the chunk (close + reopen when a
103
+ // fence is open), then place the line — hard-splitting it when
104
+ // even a fresh chunk cannot hold reopen + line + close.
105
+ closeAndReopen();
106
+ const prefixCost = fenceOpen ? currentLength + 1 : 0;
107
+ if (prefixCost + lineLength + closeCost <= limit) {
108
+ current.push(line);
109
+ currentLength += (prefixCost === 0 ? 0 : 1) + lineLength;
110
+ return;
111
+ }
112
+ // The line alone exceeds even a fresh chunk's budget: hard-split
113
+ // it (the reopen prefix is dropped first — hardSplit re-arms its
114
+ // own reopen per piece, so every piece's overhead is accounted).
115
+ current = [];
116
+ currentLength = 0;
117
+ hardSplit(line);
118
+ };
119
+ /**
120
+ * Hard-split one overlong line into pieces sized for the fence
121
+ * overhead (the ACTUAL info string, not a fixed reserve). Each piece
122
+ * chunk carries reopen + join + piece + close when a fence is open,
123
+ * so the piece limit is the limit minus that overhead — and every
124
+ * piece chunk STARTS with the reopen (the prefix is armed per piece,
125
+ * never inherited from a dropped head).
126
+ */
127
+ const hardSplit = (line) => {
128
+ const reopen = fenceOpen ? fenceReopen() : undefined;
129
+ const overhead = reopen === undefined ? 0 : reopen.length + 1 + CLOSE_COST;
130
+ const pieceLimit = Math.max(1, limit - overhead);
131
+ const emit = (piece) => {
132
+ if (reopen === undefined) {
133
+ current.push(piece);
134
+ currentLength = piece.length;
135
+ }
136
+ else {
137
+ current.push(reopen, piece);
138
+ currentLength = reopen.length + 1 + piece.length;
139
+ }
140
+ // flush appends the close when a fence is open; the next piece
141
+ // re-arms its own reopen.
142
+ flush();
143
+ };
144
+ let start = 0;
145
+ for (;;) {
146
+ const remaining = line.length - start;
147
+ if (remaining <= pieceLimit) {
148
+ emit(line.slice(start));
149
+ return;
150
+ }
151
+ emit(line.slice(start, start + pieceLimit));
152
+ start += pieceLimit;
153
+ }
154
+ };
155
+ let paragraphBreak = false;
156
+ for (const line of lines) {
157
+ const trimmed = line.trim();
158
+ if (trimmed === "") {
159
+ // A blank line: a paragraph boundary candidate — INSIDE a fence
160
+ // too (the split still closes and reopens, keeping every chunk
161
+ // well-formed; a mid-fence blank line is content, but a split
162
+ // point there is honest when the chunk is full).
163
+ if (current.length > 0)
164
+ paragraphBreak = true;
165
+ continue;
166
+ }
167
+ if (paragraphBreak) {
168
+ // Split at the paragraph boundary: close (+ reopen inside a
169
+ // fence) so the next chunk starts well-formed. Fence marker
170
+ // lines arrive here too — a blank line before a close marker
171
+ // still splits with the pair.
172
+ if (current.length > 0)
173
+ closeAndReopen();
174
+ paragraphBreak = false;
175
+ }
176
+ const fenceMatch = /^```(.*)$/.exec(trimmed);
177
+ if (fenceMatch !== null) {
178
+ if (!fenceOpen) {
179
+ // Opening a fence. The fit decision is made against the
180
+ // PRE-fence state (fenceOpen still false): flipping it first
181
+ // would make the overflow flush append a CLOSE suffix to a
182
+ // plain-text chunk that never opened a fence (the stray-marker
183
+ // regression). Only when the marker is PLACED does the fence
184
+ // open.
185
+ const info = fenceMatch[1] ?? "";
186
+ const bareInfo = info === "" ? undefined : info;
187
+ const marker = "```" + (bareInfo === undefined ? "" : bareInfo);
188
+ const joinCost = current.length === 0 ? 0 : 1;
189
+ const closeCost = CLOSE_COST;
190
+ if (currentLength + joinCost + marker.length + closeCost <= limit) {
191
+ // The marker fits the current chunk: the fence opens in place.
192
+ current.push(marker);
193
+ currentLength += joinCost + marker.length;
194
+ }
195
+ else {
196
+ // The marker does not fit: flush the plain-text chunk AS
197
+ // PLAIN TEXT (no fence is open — no close suffix), then open
198
+ // the fence at the head of the fresh chunk.
199
+ flush();
200
+ current.push(marker);
201
+ currentLength = marker.length;
202
+ }
203
+ fenceOpen = true;
204
+ openFence = bareInfo;
205
+ continue;
206
+ }
207
+ // Closing a fence: when the current chunk is empty (or holds only
208
+ // the armed reopen), the PREVIOUS chunk's flush suffix already
209
+ // closed the fence — emitting the marker would add an empty-fence
210
+ // chunk, so it is dropped. Otherwise the marker lands normally,
211
+ // WITHOUT arming a fresh reopen when it overflows (the close is
212
+ // terminal: a reopened fence just to hold its own terminator is
213
+ // noise).
214
+ const onlyReopen = current.length === 1 && current[0] === fenceReopen();
215
+ if (current.length === 0 || onlyReopen) {
216
+ current = [];
217
+ currentLength = 0;
218
+ }
219
+ else {
220
+ const joinCost = current.length === 0 ? 0 : 1;
221
+ if (currentLength + joinCost + 3 <= limit) {
222
+ current.push("```");
223
+ currentLength += joinCost + 3;
224
+ }
225
+ else {
226
+ // The marker does not fit: the chunk closes WITHOUT arming a
227
+ // reopen (fenceOpen flips to false first, so flush's suffix
228
+ // closes what stands).
229
+ fenceOpen = false;
230
+ flush();
231
+ }
232
+ }
233
+ fenceOpen = false;
234
+ openFence = undefined;
235
+ continue;
236
+ }
237
+ pushLine(line, line.length);
238
+ }
239
+ flush();
240
+ // An unterminated fence never emits a trailing empty reopen chunk:
241
+ // flush() already closed whatever chunk ended inside the fence.
242
+ return chunks;
243
+ }
244
+ /**
245
+ * Strip markdown to plain text (the `plain` capability's formatter):
246
+ * headings lose their `#` markers, emphasis markers are removed, links
247
+ * render as `text (url)`, and fenced code blocks become their inner
248
+ * lines. Structure (paragraphs, line breaks) is preserved.
249
+ */
250
+ export function stripMarkdown(text) {
251
+ const lines = text.split("\n");
252
+ const out = [];
253
+ let inFence = false;
254
+ for (const line of lines) {
255
+ const trimmed = line.trim();
256
+ if (/^```/.test(trimmed)) {
257
+ inFence = !inFence;
258
+ continue;
259
+ }
260
+ if (inFence) {
261
+ out.push(line);
262
+ continue;
263
+ }
264
+ out.push(stripInline(stripHeading(line)));
265
+ }
266
+ // Collapse the blank lines the fence markers consumed (a fence's
267
+ // open/close lines leave consecutive blanks around the block).
268
+ return out.join("\n");
269
+ }
270
+ /** Drop a heading's leading `#` run (ATX headings only). */
271
+ function stripHeading(line) {
272
+ const match = /^(#{1,6})\s+(.*)$/.exec(line);
273
+ return match === null ? line : match[2] ?? "";
274
+ }
275
+ /** Strip emphasis and rewrite links on ONE line. */
276
+ function stripInline(line) {
277
+ // Links first: their text may carry emphasis.
278
+ const text = line.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_all, label, url) => `${label} (${url})`);
279
+ // Bold + italics markers (`**`, `__`, `*`, `_` pairs).
280
+ return text
281
+ .replace(/\*\*([^*]+)\*\*/g, "$1")
282
+ .replace(/__([^_]+)__/g, "$1")
283
+ .replace(/\*([^*]+)\*/g, "$1")
284
+ // Inline code markers.
285
+ .replace(/`([^`]+)`/g, "$1");
286
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * renderChunks (spec §8.6's three-step order, verbatim):
3
+ *
4
+ * 1. Split the markdown final at `maxMessageLength` (paragraph, line,
5
+ * hard — {@link splitMarkdown}, with the fence close/reopen rule).
6
+ * 2. Format each chunk per `capabilities.format`: `markdown` as-is,
7
+ * `native` through the adapter's `format`, `plain` through the
8
+ * engine's markdown stripper.
9
+ * 3. A formatted chunk that STILL exceeds `maxMessageLength` (native
10
+ * markup grew it) is hard-split.
11
+ *
12
+ * Empty input renders to zero chunks (the caller owns the empty-answer
13
+ * notice — Review Focus 4).
14
+ */
15
+ import type { ChannelCapabilities } from "@sigrun/sigrun/extension";
16
+ /**
17
+ * Render the markdown final into platform-ready chunks. `format` is the
18
+ * adapter's formatter (`ChannelContribution.format`), required only for
19
+ * `native` capabilities — a missing formatter on a native channel is
20
+ * the loader's contract violation, handled by the caller; here the
21
+ * chunk passes through unformatted (the honest degraded behavior for
22
+ * an adapter that declared native markup and shipped no renderer).
23
+ */
24
+ export declare function renderChunks(markdown: string, caps: ChannelCapabilities, format?: (md: string) => string): string[];
@@ -0,0 +1,65 @@
1
+ /**
2
+ * renderChunks (spec §8.6's three-step order, verbatim):
3
+ *
4
+ * 1. Split the markdown final at `maxMessageLength` (paragraph, line,
5
+ * hard — {@link splitMarkdown}, with the fence close/reopen rule).
6
+ * 2. Format each chunk per `capabilities.format`: `markdown` as-is,
7
+ * `native` through the adapter's `format`, `plain` through the
8
+ * engine's markdown stripper.
9
+ * 3. A formatted chunk that STILL exceeds `maxMessageLength` (native
10
+ * markup grew it) is hard-split.
11
+ *
12
+ * Empty input renders to zero chunks (the caller owns the empty-answer
13
+ * notice — Review Focus 4).
14
+ */
15
+ import { splitMarkdown, stripMarkdown } from "./markdown.js";
16
+ /**
17
+ * Render the markdown final into platform-ready chunks. `format` is the
18
+ * adapter's formatter (`ChannelContribution.format`), required only for
19
+ * `native` capabilities — a missing formatter on a native channel is
20
+ * the loader's contract violation, handled by the caller; here the
21
+ * chunk passes through unformatted (the honest degraded behavior for
22
+ * an adapter that declared native markup and shipped no renderer).
23
+ */
24
+ export function renderChunks(markdown, caps, format) {
25
+ if (markdown === "")
26
+ return [];
27
+ // Step 1: split at the platform's message ceiling.
28
+ const split = splitMarkdown(markdown, caps.maxMessageLength);
29
+ // Step 2 + 3: format, then hard-split whatever formatting grew.
30
+ const out = [];
31
+ for (const chunk of split) {
32
+ const formatted = formatChunk(chunk, caps, format);
33
+ if (formatted.length <= caps.maxMessageLength) {
34
+ out.push(formatted);
35
+ continue;
36
+ }
37
+ // Native markup grew the chunk past the ceiling: hard-split the
38
+ // FORMATTED text (no fence knowledge — the adapter's markup is
39
+ // opaque to the engine; closing it would be guesswork).
40
+ out.push(...hardSplit(formatted, caps.maxMessageLength));
41
+ }
42
+ return out;
43
+ }
44
+ /** Step 2 for one chunk. */
45
+ function formatChunk(chunk, caps, format) {
46
+ switch (caps.format) {
47
+ case "markdown":
48
+ return chunk;
49
+ case "plain":
50
+ return stripMarkdown(chunk);
51
+ case "native":
52
+ return format === undefined ? chunk : format(chunk);
53
+ }
54
+ }
55
+ /** Hard-split one formatted string at exact `limit` boundaries. A
56
+ * non-positive limit (a misconfigured adapter) degrades to length-1
57
+ * pieces — never an infinite loop of empty slices. */
58
+ function hardSplit(text, limit) {
59
+ const step = Math.max(1, limit);
60
+ const pieces = [];
61
+ for (let start = 0; start < text.length; start += step) {
62
+ pieces.push(text.slice(start, start + step));
63
+ }
64
+ return pieces;
65
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * onRunSettled (spec §8.6's "which runs deliver" + the M7 access
3
+ * re-check): the engine's `runSettled` factory handler. ONLY ENQUEUES —
4
+ * the per-channel delivery workers own the platform calls.
5
+ *
6
+ * - Every key in `bindings.keysOf(conversationId)` that is not dead
7
+ * gets a final (whoever started the run — ruling 4: a webui submit
8
+ * into a bound conversation delivers to the chat).
9
+ * - Access is RE-CHECKED per delivery (M7): `meta` + `accessOf` for
10
+ * the binding's user — the LINKED user for DMs, the group owner for
11
+ * groups. A failed check UNBINDS the chat, logs it, and enqueues
12
+ * nothing.
13
+ * - An answered run renders the answer's text blocks (thinking and
14
+ * tool calls are never shown); an answer with NO text blocks gets
15
+ * the pinned "(no text reply)" notice (Review Focus 4 — an empty
16
+ * send is a platform rejection, never an honest reply).
17
+ * - The origin-chat streaming hand-off runs through `deps.streams`:
18
+ * rendered chunks cross to the registry at this layer while the
19
+ * boolean answer decides whether the origin's finals enqueue.
20
+ * - An unanswered run notifies the ORIGIN chat only (from
21
+ * `deps.origins`): the pinned "Stopped." for an abort reason, a
22
+ * short reason notice otherwise.
23
+ */
24
+ import type { ChannelContribution, RunSettledEvent, SigrunHost } from "@sigrun/sigrun/extension";
25
+ import type { SubmissionId } from "@earendil-works/pi-durable";
26
+ import type { SessionMeta } from "@sigrun/shared/session-meta";
27
+ import type { ChannelConfig } from "../config.js";
28
+ import type { BindingStore } from "../state/bindings.js";
29
+ import type { Ledger } from "../state/ledger.js";
30
+ import { type EngineLog } from "../log.js";
31
+ import type { IdentityStore } from "../state/identities.js";
32
+ import type { SubmissionOrigin } from "../inbound/pipeline.js";
33
+ /** The pinned notice for a textless answer (Review Focus 4). */
34
+ export declare const NO_TEXT_REPLY = "(no text reply)";
35
+ /** The pinned abort notice (spec §8.6: "An abort from /stop produces Stopped."). */
36
+ export declare const STOPPED = "Stopped.";
37
+ /**
38
+ * The streaming hand-off seam. `finalize` answers ONE question: did
39
+ * the stream own this conversation's origin-chat delivery? Chunks are
40
+ * rendered by the settled path (which knows the answer and caps) and
41
+ * consumed by the registry; the boolean-only fan-out decision remains
42
+ * independent of that delivery work. `abandon` releases unanswered runs.
43
+ */
44
+ export interface Streams {
45
+ finalize(conversationId: number, chunks: string[]): Promise<boolean>;
46
+ abandon(conversationId: number): void;
47
+ }
48
+ /**
49
+ * The job-delivery metadata the settled fan-out reads. Derived from the
50
+ * canonical shared session-meta wire type (type-only: no runtime core
51
+ * dependency) so field drift fails the contract fixture's typecheck.
52
+ */
53
+ export type GatewayJobMeta = Pick<SessionMeta, "owner" | "name" | "autoName" | "jobId" | "deliver">;
54
+ /** onRunSettled's injectable dependencies. */
55
+ export interface SettledDeps {
56
+ readonly host: SigrunHost;
57
+ /** The event bus's bridge-free document read (factory-supplied). */
58
+ readonly readMeta?: (conversationId: number) => Promise<GatewayJobMeta | undefined>;
59
+ /** The engine-wide bindings (the delivery fan-out read). */
60
+ readonly bindings: BindingStore;
61
+ /** The engine-wide identity links (the binding's user resolution). */
62
+ readonly identities: IdentityStore;
63
+ /** The ledger the finals enqueue into. */
64
+ readonly ledger: Ledger;
65
+ /** The channel id → adapter lookup (the render step's caps/format). */
66
+ readonly channelOf: (id: string) => ChannelContribution | undefined;
67
+ /** The live channel configs (the group owner read). */
68
+ readonly configOf: (id: string) => Promise<ChannelConfig | undefined>;
69
+ /** submissionId → the origin record (the unanswered notice's target). */
70
+ readonly origins: Map<SubmissionId, SubmissionOrigin>;
71
+ /** The origin-chat stream registry. */
72
+ readonly streams: Streams;
73
+ readonly log: EngineLog;
74
+ }
75
+ /**
76
+ * The settled-run handler: enqueue finals (or the empty-answer notice)
77
+ * for every live bound chat, and the unanswered notice for the origin
78
+ * chat. Never throws (the event bus's bounded handler owns isolation,
79
+ * but a ledger failure must not lose the rest of the fan-out).
80
+ */
81
+ export declare function onRunSettled(event: RunSettledEvent, deps: SettledDeps): Promise<void>;