@arnilo/prism 0.7.0 → 0.9.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.
Files changed (182) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +12 -11
  3. package/dist/agent-approval.d.ts +15 -2
  4. package/dist/agent-approval.js +5 -1
  5. package/dist/agent-event-source.d.ts +9 -1
  6. package/dist/agent-event-source.js +10 -3
  7. package/dist/agent-loops.js +7 -4
  8. package/dist/agent-run-lifecycle.d.ts +15 -1
  9. package/dist/agent-run-lifecycle.js +91 -10
  10. package/dist/agent-run-state.d.ts +34 -2
  11. package/dist/agent-run-state.js +68 -6
  12. package/dist/agent-session/helpers.js +20 -1
  13. package/dist/agent-session/session/assemble.js +250 -27
  14. package/dist/agent-session/session/persist.d.ts +27 -0
  15. package/dist/agent-session/session/persist.js +94 -12
  16. package/dist/agent-session/session/provider-round.d.ts +14 -4
  17. package/dist/agent-session/session/provider-round.js +197 -25
  18. package/dist/agent-session/session/tool-round.js +24 -2
  19. package/dist/agent-session/session/types.d.ts +36 -2
  20. package/dist/agent-session/session.d.ts +40 -4
  21. package/dist/agent-session/session.js +78 -5
  22. package/dist/attention-compiler.d.ts +51 -2
  23. package/dist/attention-compiler.js +282 -21
  24. package/dist/cache-helpers.d.ts +4 -2
  25. package/dist/cache-helpers.js +8 -6
  26. package/dist/checkpoint-restore.d.ts +45 -0
  27. package/dist/checkpoint-restore.js +54 -0
  28. package/dist/checkpoints.js +7 -11
  29. package/dist/context-budget.d.ts +2 -1
  30. package/dist/context-budget.js +24 -2
  31. package/dist/contracts-core/agent.d.ts +30 -0
  32. package/dist/contracts-core/attention.d.ts +95 -0
  33. package/dist/contracts-core/content.d.ts +15 -0
  34. package/dist/contracts-core/guardrail-packs.d.ts +41 -0
  35. package/dist/contracts-core/guardrail-packs.js +2 -0
  36. package/dist/contracts-core/loop.d.ts +42 -0
  37. package/dist/contracts-core/provider.d.ts +25 -0
  38. package/dist/contracts-core/run-limits.d.ts +21 -0
  39. package/dist/contracts-core/session.d.ts +23 -5
  40. package/dist/contracts-core/session.js +21 -2
  41. package/dist/contracts-core/usage.d.ts +40 -0
  42. package/dist/contracts-core/usage.js +8 -0
  43. package/dist/contracts-core.d.ts +2 -0
  44. package/dist/contracts-core.js +2 -0
  45. package/dist/contracts-protocol.d.ts +90 -4
  46. package/dist/contracts-run-state.d.ts +82 -6
  47. package/dist/evidence-grounding.d.ts +29 -0
  48. package/dist/evidence-grounding.js +162 -0
  49. package/dist/guardrail-packs/coding-standard.d.ts +3 -0
  50. package/dist/guardrail-packs/coding-standard.js +63 -0
  51. package/dist/guardrail-packs/destructive-commands.d.ts +3 -0
  52. package/dist/guardrail-packs/destructive-commands.js +46 -0
  53. package/dist/guardrail-packs/errors.d.ts +7 -0
  54. package/dist/guardrail-packs/errors.js +9 -0
  55. package/dist/guardrail-packs/index.d.ts +4 -0
  56. package/dist/guardrail-packs/index.js +15 -0
  57. package/dist/guardrail-packs/secrets-hygiene.d.ts +3 -0
  58. package/dist/guardrail-packs/secrets-hygiene.js +23 -0
  59. package/dist/guardrail-packs/types.d.ts +16 -0
  60. package/dist/guardrail-packs/types.js +2 -0
  61. package/dist/guardrail-packs/validation-respect.d.ts +3 -0
  62. package/dist/guardrail-packs/validation-respect.js +53 -0
  63. package/dist/guardrails.d.ts +20 -1
  64. package/dist/guardrails.js +268 -0
  65. package/dist/host-composition.d.ts +13 -0
  66. package/dist/host-composition.js +33 -2
  67. package/dist/index.d.ts +19 -10
  68. package/dist/index.js +11 -6
  69. package/dist/input.d.ts +8 -1
  70. package/dist/input.js +68 -6
  71. package/dist/middleware.d.ts +37 -2
  72. package/dist/middleware.js +41 -0
  73. package/dist/node/session-store-jsonl.js +18 -3
  74. package/dist/observability.js +6 -0
  75. package/dist/provider-events.d.ts +11 -3
  76. package/dist/provider-events.js +62 -4
  77. package/dist/providers/openai-compatible.js +6 -3
  78. package/dist/providers/transport.d.ts +3 -1
  79. package/dist/providers/transport.js +36 -0
  80. package/dist/redaction.js +18 -2
  81. package/dist/run-bundle.d.ts +89 -0
  82. package/dist/run-bundle.js +150 -0
  83. package/dist/run-limits.d.ts +11 -1
  84. package/dist/run-limits.js +46 -0
  85. package/dist/session-stores.d.ts +12 -1
  86. package/dist/session-stores.js +21 -4
  87. package/dist/testing/agent-event-source-conformance.js +41 -2
  88. package/dist/testing/prefix-stability-conformance.d.ts +30 -0
  89. package/dist/testing/prefix-stability-conformance.js +104 -0
  90. package/dist/testing/session-store-conformance.d.ts +3 -2
  91. package/dist/testing/session-store-conformance.js +48 -0
  92. package/dist/testing/state-concurrency-conformance.js +5 -12
  93. package/dist/tools.d.ts +5 -0
  94. package/dist/tools.js +11 -3
  95. package/dist/usage-estimation.d.ts +29 -0
  96. package/dist/usage-estimation.js +79 -0
  97. package/docs/ag-ui.md +5 -0
  98. package/docs/agent-events.md +68 -1
  99. package/docs/agent-loops.md +33 -0
  100. package/docs/agent-session-runtime.md +5 -3
  101. package/docs/attention-compiler.md +89 -8
  102. package/docs/coding-agent-tools.md +1 -1
  103. package/docs/coding-security.md +1 -0
  104. package/docs/coding-tools.md +0 -1
  105. package/docs/compaction-and-retry.md +1 -1
  106. package/docs/compaction-observational-memory.md +34 -7
  107. package/docs/connected-apps.md +116 -0
  108. package/docs/context-and-skills.md +13 -0
  109. package/docs/core.md +1 -1
  110. package/docs/diagrams.md +6 -6
  111. package/docs/document-reader.md +9 -9
  112. package/docs/documents.md +32 -11
  113. package/docs/durable-runs.md +129 -0
  114. package/docs/embeddings.md +5 -0
  115. package/docs/enterprise-postgres-state.md +4 -0
  116. package/docs/evaluations.md +5 -0
  117. package/docs/execution-timeline.md +84 -1
  118. package/docs/guardrails.md +71 -2
  119. package/docs/history/079-messaging-primitive-review.md +391 -0
  120. package/docs/history/080-messaging-followon-primitive-review.md +234 -0
  121. package/docs/history/081-connected-apps-primitive-review.md +74 -0
  122. package/docs/history/083-prism-work-primitive-review.md +84 -0
  123. package/docs/history/084-primitive-review.md +96 -0
  124. package/docs/history/085-honesty-and-cut-primitive-review.md +91 -0
  125. package/docs/history/README.md +5 -0
  126. package/docs/history/release-handoffs.md +38 -0
  127. package/docs/host-compositions.md +8 -6
  128. package/docs/host-security.md +2 -2
  129. package/docs/index.md +66 -29
  130. package/docs/input-and-prompt-assembly.md +3 -3
  131. package/docs/knowledge-sync.md +4 -0
  132. package/docs/live-testing.md +5 -3
  133. package/docs/mcp-tools.md +1 -0
  134. package/docs/messaging-channel-operations.md +166 -0
  135. package/docs/messaging-channels.md +150 -0
  136. package/docs/middleware-hooks.md +38 -2
  137. package/docs/migrate-to-0.8.md +124 -0
  138. package/docs/migrate-to-0.9.md +210 -0
  139. package/docs/migration.md +43 -0
  140. package/docs/model-registry.md +12 -2
  141. package/docs/multi-agent-patterns.md +25 -2
  142. package/docs/node-jsonl-session-store.md +7 -1
  143. package/docs/observability.md +7 -3
  144. package/docs/openapi-tools.md +1 -1
  145. package/docs/operations.md +1 -3
  146. package/docs/options-index.md +36 -3
  147. package/docs/peer-dependencies.md +6 -6
  148. package/docs/policy-and-audit.md +13 -1
  149. package/docs/postgres-persistence.md +1 -1
  150. package/docs/prefix-stability-conformance.md +93 -0
  151. package/docs/provider-caching.md +4 -4
  152. package/docs/provider-conformance.md +16 -0
  153. package/docs/provider-layer.md +2 -2
  154. package/docs/provider-packages.md +20 -20
  155. package/docs/providers/neuralwatt.md +5 -1
  156. package/docs/public-contracts.md +2 -2
  157. package/docs/rag.md +102 -4
  158. package/docs/release-and-install.md +55 -47
  159. package/docs/run-bundle.md +92 -0
  160. package/docs/runs-and-usage.md +57 -6
  161. package/docs/scoped-agent-memory.md +262 -0
  162. package/docs/server.md +2 -0
  163. package/docs/session-store-conformance.md +1 -2
  164. package/docs/session-stores.md +17 -17
  165. package/docs/sheets.md +9 -9
  166. package/docs/signal-channel.md +112 -0
  167. package/docs/speech.md +5 -1
  168. package/docs/sqlite-persistence.md +1 -1
  169. package/docs/supervisors.md +32 -12
  170. package/docs/telegram-channel.md +157 -0
  171. package/docs/testing.md +2 -2
  172. package/docs/tools.md +17 -0
  173. package/docs/wiki.md +1 -1
  174. package/docs/work-artifacts-and-review.md +1 -1
  175. package/docs/work-connectors.md +9 -9
  176. package/docs/work-sandbox.md +115 -0
  177. package/docs/work-tools.md +38 -16
  178. package/docs/workflows.md +5 -0
  179. package/package.json +9 -3
  180. package/templates/business-worker/manifest.json +2 -1
  181. package/templates/business-worker/src/agent.ts.tmpl +1 -1
  182. package/templates/business-worker/src/tests/agent.test.ts.tmpl +1 -1
package/docs/sheets.md CHANGED
@@ -1,8 +1,8 @@
1
- # Spreadsheets, CSV parsing, and typed schema inference (`@arnilo/prism-office/sheets`)
1
+ # Spreadsheets, CSV parsing, and typed schema inference (`@arnilo/prism-work/sheets`)
2
2
 
3
3
  ## What it does
4
4
 
5
- The `@arnilo/prism-office/sheets` package provides fail-closed, high-fidelity spreadsheet (XLSX) and delimiter-separated (CSV/TSV/PSV) data ingestion with automatic dialect sniffing, typed column schema inference, and **strict financial decimal safety**.
5
+ The `@arnilo/prism-work/sheets` package provides fail-closed, high-fidelity spreadsheet (XLSX) and delimiter-separated (CSV/TSV/PSV) data ingestion with automatic dialect sniffing, typed column schema inference, and **strict financial decimal safety**.
6
6
 
7
7
  ### Headline Guarantee: Strict Financial Decimal Safety
8
8
 
@@ -10,7 +10,7 @@ The `@arnilo/prism-office/sheets` package provides fail-closed, high-fidelity sp
10
10
  > **Zero Float Coercion on Decimal Paths**:
11
11
  > In financial and enterprise data processing, floating-point rounding errors (IEEE-754 `double`) silently distort monetary totals, balance ledgers, and transaction reconciliations.
12
12
  >
13
- > In `@arnilo/prism-office/sheets`:
13
+ > In `@arnilo/prism-work/sheets`:
14
14
  > - Money-like and decimal values are **never converted to JavaScript numbers (`Number()`, `parseFloat()`, or unary `+`)**.
15
15
  > - All decimal and currency values are parsed, normalized, and emitted as exact canonical decimal strings: `{ type: "decimal", value: "1234.56" }`.
16
16
  > - Currency markers (`$`, `€`, `£`, `¥`, `₹`, `CHF`, `USD`, `EUR`, etc.) and accounting parentheses `($1,234.56)` are normalized safely into canonical strings (`"-1234.56"`).
@@ -29,7 +29,7 @@ The `@arnilo/prism-office/sheets` package provides fail-closed, high-fidelity sp
29
29
 
30
30
  ## When to use it
31
31
 
32
- Use `@arnilo/prism-office/sheets` when autonomous agents, data pipelines, or enterprise workflows need to:
32
+ Use `@arnilo/prism-work/sheets` when autonomous agents, data pipelines, or enterprise workflows need to:
33
33
  1. Ingest untrusted customer XLSX or CSV files with strict, unbypassable byte, row, column, and sheet caps.
34
34
  2. Parse tabular financial records, invoices, ledgers, or pricing sheets with mathematical decimal precision guarantees.
35
35
  3. Automatically determine CSV delimiters, quotes, and headers without manual dialect configuration.
@@ -151,7 +151,7 @@ TXN-1003,"Hardware Device","£ 2,500.00",2500.00,"$ 0.00"
151
151
  ## Implementation example
152
152
 
153
153
  ```ts
154
- import { parseWorkbook, parseCsv, type SheetsTelemetry } from "@arnilo/prism-office/sheets";
154
+ import { parseWorkbook, parseCsv, type SheetsTelemetry } from "@arnilo/prism-work/sheets";
155
155
 
156
156
  // 1. Parse XLSX workbook with custom caps
157
157
  const xlsxBytes = new Uint8Array([...]); // Untrusted file bytes
@@ -194,7 +194,7 @@ console.log(`Revenue value:`, csvResult.rows[1][2]);
194
194
  ## Extension and configuration notes
195
195
 
196
196
  ### Sub-package Pinning
197
- To avoid pulling in CLI frameworks or extraneous dependencies, `@arnilo/prism-office/sheets` directly pins the exact underlying modular packages:
197
+ To avoid pulling in CLI frameworks or extraneous dependencies, `@arnilo/prism-work/sheets` directly pins the exact underlying modular packages:
198
198
  - `@office-open/xlsx@0.12.3`
199
199
  - `@office-open/xml@0.12.3`
200
200
 
@@ -211,7 +211,7 @@ const telemetry: SheetsTelemetry = {
211
211
  ```
212
212
 
213
213
  ### Self-Hosting & Operational Notes
214
- - **Zero Network & Storage Dependencies**: `@arnilo/prism-office/sheets` does not write files or contact network services. Host engines own persistence, storage buckets, and lake datasets.
214
+ - **Zero Network & Storage Dependencies**: `@arnilo/prism-work/sheets` does not write files or contact network services. Host engines own persistence, storage buckets, and lake datasets.
215
215
  - **Fail-Closed Container Gating**: Malicious or non-standard files are rejected before allocation or XML decompression occurs.
216
216
 
217
217
  ## Security and performance notes
@@ -224,6 +224,6 @@ const telemetry: SheetsTelemetry = {
224
224
 
225
225
  ## Related APIs
226
226
 
227
- - [`@arnilo/prism-office/documents`](./documents.md): Specification-compliant OpenXML document generation, parsing, patching, and preview rendering for DOCX, XLSX, and PPTX.
228
- - [`@arnilo/prism-coding-tools/document-reader`](./document-reader.md): Bounded literal text extraction from PDF and DOCX documents for coding agent tools.
227
+ - [`@arnilo/prism-work/documents`](./documents.md): Specification-compliant OpenXML document generation, parsing, patching, and preview rendering for DOCX, XLSX, and PPTX.
228
+ - [`@arnilo/prism-work/document-reader`](./document-reader.md): Bounded literal text extraction from PDF and DOCX documents for coding agent tools.
229
229
  - [`@arnilo/prism-core/governance/observability`](./observability.md): OpenTelemetry instrumentation and trace adapters.
@@ -0,0 +1,112 @@
1
+ # Signal channel (experimental)
2
+
3
+ ## What it does
4
+
5
+ `@arnilo/prism-channels/signal` is a Node-only, experimental adapter for an **externally supervised** [signal-cli v0.14.8](https://github.com/AsamK/signal-cli/tree/v0.14.8) Unix-socket daemon. It is not an official Signal bot API, does not make a bot official, and must be enabled only after an operator records the acceptable-use and GPL distribution decisions required for its deployment.
6
+
7
+ Importing or constructing the adapter does nothing: it never downloads, spawns, registers, links, relinks, configures, or health-checks `signal-cli`; opens no network connection; and has no environment or credential lookup. Host owns that lifecycle, account, state directory, encryption, backup/retention and process supervisor.
8
+
9
+ ## When to use it
10
+
11
+ Use only after an operator records acceptable-use and GPL distribution decisions for an already-operated Signal account. Not an official bot API; not for groups, topics, attachments, identity linking, bulk messaging (including `MessagingRuntime.notify`, which is unicast to one existing DM binding only), automated account creation, or streaming previews (Signal has no Bot API draft equivalent, so this adapter ignores `MessagingRuntimeOptions.onAssistantDelta`).
12
+
13
+ ```bash
14
+ npm install @arnilo/prism @arnilo/prism-channels
15
+ ```
16
+
17
+ ## Inputs / request
18
+
19
+ `SignalAdapterOptions`: host `connectionId`, absolute private `socketPath`, daemon `account`, `signalCliVersion` (must equal `SIGNAL_CLI_VERSION`), `policy` attestation, and service-owned `checkpoints`/`leases`/`cursorOwnership`. `signal-cli` is a host-operated binary, not an npm peer.
20
+
21
+ ## Host setup
22
+
23
+ Use only a private, host-selected Unix socket on the same Node/Linux host. Do not expose a TCP or HTTP bridge. Protect socket parent/state directories with service-only permissions (for example `0700`) and protect backups. Signal transport decrypts at `signal-cli`; Prism stores and model providers receive plaintext according to host policy.
24
+
25
+ The supported daemon shape is manual receive mode, selected by the host for the pinned CLI version:
26
+
27
+ ```sh
28
+ signal-cli --data-dir /private/prism-signal -a +15550001111 \
29
+ daemon --socket=/private/run/prism-signal.sock --receive-mode=manual
30
+ ```
31
+
32
+ `signal-cli` is GPLv3 and its upstream compatibility can change after Signal service changes. It requires the pinned release's documented JRE/native dependencies (v0.14.8 documents JRE 25). Account registration/relinking may disrupt existing clients; never automate it from Prism or chat input. Consult current Signal terms and operator policy before enabling automated replies; terms/policy can prohibit an intended deployment.
33
+
34
+ ## Request/response example
35
+
36
+ ```ts
37
+ import { createMessagingRuntime } from "@arnilo/prism-channels";
38
+ import { createSignalAdapter, SIGNAL_CLI_VERSION } from "@arnilo/prism-channels/signal";
39
+
40
+ const adapter = createSignalAdapter({
41
+ connectionId: "support-signal",
42
+ socketPath: "/private/run/prism-signal.sock",
43
+ account: "+15550001111", // host daemon account, never taken from a message
44
+ signalCliVersion: SIGNAL_CLI_VERSION,
45
+ policy: {
46
+ acceptableUse: "operator_approved",
47
+ gplDistribution: "operator_approved",
48
+ termsVersion: "2026-09-review", // host's recorded policy review, not a Signal approval
49
+ },
50
+ checkpoints: serviceCheckpoints,
51
+ leases: serviceLeases,
52
+ cursorOwnership: { tenantId: "signal-service" },
53
+ // multiAccount: true, // only for a daemon where account is supplied on RPC requests
54
+ });
55
+
56
+ const runtime = createMessagingRuntime({
57
+ authorize: hostAuthorizeSignalSender, // verify observed Signal UUID -> Prism identity/grant
58
+ resolveAgent: hostResolveAgent,
59
+ deliver: (reply) => adapter.send(reply),
60
+ checkpoints: userCheckpoints,
61
+ leases: userLeases,
62
+ });
63
+
64
+ await adapter.start((event) => runtime.admit(event));
65
+ // Shutdown: await adapter.stop(); await runtime.stop();
66
+ ```
67
+
68
+ ## Implementation example
69
+
70
+ [`examples/signal-agent.ts`](../examples/signal-agent.ts) contains the same host composition helper.
71
+
72
+ `start()` acquires `prism.channels.v1.signal.receiver`, probes its checkpoint writer, connects the persistent Unix socket, then calls only `subscribeReceive`. It renews its receiver lease; loss fails closed by pausing intake. `stop()` unsubscribes when possible, closes its socket, aborts reconnection and releases only its lease. A second receiver under the same service ownership cannot subscribe.
73
+
74
+ The adapter accepts manual `params.result.envelope` notifications only when they name the selected account and contain a direct text `dataMessage` from a valid Signal UUID. It ignores receipts, sync echoes, reactions, stories, edits, groups (including group topics, which have no supported thread mapping) and attachments before `admit`. UUID (not display name or phone number) becomes the external actor/conversation ID; the runtime must still authorize it. An account mismatch pauses the adapter before admission. `health()` reports independent bounded `bridge`, `subscription`, and `account` states for host monitoring without message text, phone numbers, socket path or key material.
75
+
76
+ ## Outputs / response / events
77
+
78
+ `send()` accepts only replies for its configured `connectionId` and a UUID destination fixed by the runtime's authorized binding. It calls only `send`; chat text cannot choose an RPC method, account, socket, recipient or identity action. Replies are plain text and chunked at 2,000 UTF-16 code units without splitting surrogate pairs.
79
+
80
+ A server-issued approval control is rendered as plain reply text:
81
+
82
+ ```text
83
+ Reply /approve <opaque-token> to allow once.
84
+ Reply /deny <opaque-token> to deny.
85
+ ```
86
+
87
+ Only the shared runtime validates and consumes that opaque token. Free-form text remains untrusted input; no Signal reaction, attachment, identity-trust or linking action is exposed.
88
+
89
+ Known daemon errors map to a bounded reason (`signal_rate_limited`, `signal_captcha_required`, `signal_relink_required`, `signal_identity_changed`, or `signal_rpc_error`). A changed/untrusted identity stops normal sends until an operator verifies it and current authorization is re-established. Timeout/socket loss after a write — or a failure after an earlier chunk — throws an ambiguous-delivery error; the durable reply journal records `delivery_unknown` and must not automatically resend it.
90
+
91
+ ## Extension and configuration notes
92
+
93
+ | Setting | Default | Range |
94
+ | --- | ---: | ---: |
95
+ | JSON-RPC line/frame | 128 KiB | 1 KiB–512 KiB |
96
+ | Pending RPC requests | 16 | 1–64 |
97
+ | Pending admitted notifications | 32 | 1–128 |
98
+ | RPC timeout | 10 s | 100 ms–60 s |
99
+ | Reconnect delay | 1 s | 1 ms–60 s (exponential, 30 s default max) |
100
+ | Receiver lease | 90 s | 5 s–5 min |
101
+ | Outbound chunk | 2,000 UTF-16 units | fixed |
102
+
103
+ The adapter pauses/unsubscribes on journal admission capacity/storage failure, malformed/oversized frames, account mismatch, or lease loss. It reconnects with bounded backoff after an established bridge loss and rechecks writer readiness before subscribing. It has **no documented application acknowledgment or replay log**: a daemon notification received before a crash and before durable commit can be lost. This adapter does not claim lossless intake, exactly-once execution, message read status, or end-to-end encryption through a model/provider.
104
+
105
+ ## Security and performance notes
106
+
107
+ Construction is inert: no spawn, TCP/HTTP, environment lookup, or credential read. Chat text cannot choose RPC method, account, socket, recipient, or identity action. UUID (not phone or display name) is the only observed actor id. A changed identity stops sends until an operator verifies it. Receive-to-commit can lose a notification; treat that as a documented loss window, not a retry.
108
+
109
+ ## Related APIs
110
+
111
+ - [Messaging channels](messaging-channels.md): authorization and durable replies.
112
+ - [Messaging channel operations](messaging-channel-operations.md): recovery and operator review.
package/docs/speech.md CHANGED
@@ -14,7 +14,11 @@ and `runTranscriptionConformance` from `@arnilo/prism/testing/provider-conforman
14
14
  ## When to use it
15
15
 
16
16
  Use it for one-shot voice output and batch/stream transcription where the host
17
- owns playback, capture, and audio storage. Do not use it for interactive
17
+ owns playback, capture, and audio storage. Messaging hosts wrap these providers
18
+ and hand the wrappers to a channel adapter — Telegram `transcribe` turns an
19
+ inbound voice note into turn text and `synthesize` adds a voice note next to a
20
+ final reply ([telegram-channel.md](telegram-channel.md)); `@arnilo/prism-channels`
21
+ never imports this package and never stores audio. Do not use it for interactive
18
22
  bidirectional voice — that is the Realtime session contract
19
23
  ([`RealtimeSession`](public-contracts.md)), which keeps its own
20
24
  `audio_delta`/`transcript_delta` events. Streaming here is one-directional:
@@ -54,7 +54,7 @@ import { createSqlitePersistence } from "@arnilo/prism-core/sessions/sqlite";
54
54
  | `SessionStore.readBranchPath` | Recursive ancestor query from `leafId` (or latest leaf) in root→leaf order. |
55
55
  | `RunLedger.append*` | Inserts run/event/tool/usage rows; events receive monotonic per-run `sequence` values. |
56
56
  | `ProductionPersistenceStore.query*` | Parameterized cursor pagination on indexed columns. |
57
- | `checkpoints` | Generic versioned `CheckpointStore` backed by `prism_checkpoints`; ownership, CAS/fencing checks, bounded pagination, and workflow suspended/denied/schedule/state/replay values without a schema migration. |
57
+ | `checkpoints` | Generic versioned `CheckpointStore` backed by `prism_checkpoints`; ownership, CAS/fencing checks, bounded pagination, and workflow suspended/denied/schedule/state/replay values without a schema migration. A load or delete under a non-matching ownership scope reads as absent and a cross-scope write fails as a generic `ERR_PRISM_CHECKPOINT_CONFLICT` (plan 080 Task 3) — no ownership-shaped existence oracle. |
58
58
  | `leases` | Atomic `LeaseStore` backed by `prism_leases`; database-clock expiry, opaque renew/release token, monotonic takeover fence. |
59
59
  | `close()` | Closes the underlying database when the adapter opened it. |
60
60
 
@@ -10,29 +10,49 @@ Use a supervisor when a host or agent must choose a child dynamically. Use `@arn
10
10
 
11
11
  ## Inputs / request
12
12
 
13
- **Option surfaces** — `CreateSupervisorOptions` (ownership, child catalog, hooks, `childEvents`, limits), `SupervisorLimits` / `ResolvedSupervisorLimits` (depth, active children, child events, bytes), `DelegationWaitOptions` (`timeoutMs`, `signal`), `CreateSpawnAgentToolOptions` / `CreateDelegationControlToolOptions` (supervisor, tool name, sync/async mode), `WorktreeChildFactoryOptions` (workspace lifecycle, repository, roots), and `ObserveSupervisorLifecycleOptions` (supervisor, emit, redactor, steps).
13
+ **Option surfaces** — `CreateSupervisorOptions` (ownership, child catalog, hooks, `childEvents`, `childEventSink`, `signal`, limits), `SupervisorLimits` / `ResolvedSupervisorLimits` (depth, active children, child events, bytes, per-second rate), `SupervisorChildPolicy` (lifetime, report, milestone, budget share), `DelegationRequest` (child, input, thread, limits, lifetime, report, milestone, budget share, signal), `DelegationWaitOptions` (`timeoutMs`, `signal`), `SupervisorRunSummary` / `SupervisorChildSummary` (recovery counters), `CreateSpawnAgentToolOptions` / `CreateDelegationControlToolOptions` (supervisor, tool name, sync/async mode), `WorktreeChildFactoryOptions` (workspace lifecycle, repository, roots), and `ObserveSupervisorLifecycleOptions` (supervisor, emit, redactor, steps).
14
14
 
15
15
  | API/field | Meaning |
16
16
  | --- | --- |
17
- | `createSupervisor({ ownership, children })` | Creates one ownership-scoped supervisor. |
18
- | `SupervisorChild.createAgent(context)` | Child-owned factory; receives derived resource/thread IDs, narrowed permission, abort signal, and nested `delegate`. |
19
- | `delegate({ childId, input, threadId?, limits?, signal? })` | Invokes one allow-listed child. Input is text and byte-bounded. |
20
- | `delegateAsync({ childId, input, threadId?, limits?, signal? })` | Starts one local child and returns `{ delegationId, status: "running" }` without waiting for its result. |
21
- | `wait(delegationId)` / `cancel(delegationId)` | Joins one local async child (capped at supervisor timeout) or aborts it. Unknown and foreign IDs share one denial. |
22
- | `createSpawnAgentTool({ supervisor, name? })` | Returns non-exclusive `spawn_agent` tool for a parent model. Its closed schema exposes only host child IDs, input, optional thread ID, and `mode`. |
17
+ | `createSupervisor({ ownership, children, signal? })` | Creates one ownership-scoped supervisor. Aborting `signal` (session end) ends every running child and closes the event stream. |
18
+ | `SupervisorChild.createAgent(context)` / `policy` | Child-owned factory; receives derived resource/thread IDs, narrowed permission, abort signal, and nested `delegate`. `policy` carries host ceilings/defaults for lifetime, report, milestone cadence, and budget share. |
19
+ | `delegate({ childId, input, threadId?, limits?, lifetime?, report?, milestone?, budgetShare?, signal? })` | Invokes one allow-listed child. Input is text and byte-bounded. Sync `delegate()` rejects `lifetime: "session"`. |
20
+ | `delegateAsync({ ... })` | Starts one local child and returns `{ delegationId, status: "running" }`. `lifetime: "session"` detaches the child from the caller signal so it survives caller turns. |
21
+ | `wait(delegationId)` / `cancel(delegationId)` | Joins one local async child (capped at supervisor timeout) or aborts it, session-lifetime included. Unknown and foreign IDs share one denial. |
22
+ | `createSpawnAgentTool({ supervisor, name? })` | Returns non-exclusive `spawn_agent` tool for a parent model. Its closed schema exposes only host child IDs, input, optional thread ID, `mode`, `lifetime`, `report`, `milestone.everyTurns`, and `budgetShare`. |
23
23
  | `createWaitAgentTool` / `createCancelAgentTool` | Return `wait_agent` / `cancel_agent` tools for host-owned async handles. |
24
24
  | `Supervisor.childIds` | Frozen advertised child-id list the spawn tool's schema enum is built from; model arguments cannot extend it. |
25
25
  | `hooks.before` | May reject, modify redacted input, or narrow limits/policy. |
26
26
  | `hooks.after` | Observes redacted terminal summary; failures cannot alter settled result. |
27
- | `limits` | Depth 4/16, active children 4/32, input 64 KiB/1 MiB, steps 8/64, tools 32/256, tokens 20k/1m, timeout 60s/30m, event queue 128/4096, child events/delegation 256/4096, child-event bytes 32 KiB/256 KiB default/hard. Over-cap `delegate()` throws `SupervisorLimitError` before incrementing `activeChildren`. Hook rejection and timeout decrement the count exactly once (no leaked timers). |
27
+ | `limits` | Depth 4/16, active children 4/32, input 64 KiB/1 MiB, steps 8/64, tools 32/256, tokens 20k/1m, timeout 60s/30m, event queue 128/4096, child events/delegation 256/4096, child-event bytes 32 KiB/256 KiB, child events/second 10/1000 default/hard. Over-cap `delegate()` throws `SupervisorLimitError` before incrementing `activeChildren`. Hook rejection and timeout decrement the count exactly once (no leaked timers). |
28
28
 
29
29
  ## Outputs / response / events
30
30
 
31
- `delegate()` returns the child's `AgentRunResult` or throws its `AgentRunError`/a supervisor denial or limit error. `delegateAsync()` returns a local running handle; `wait()` returns its result (or `{ status: "cancelled" }` after `cancel()`), and stays idempotent while its terminal record is retained (bounded by `limits.maxQueuedEvents`; an evicted or foreign id returns the same non-enumerating error). `subscribe()` emits bounded `delegation_started`, `delegation_finished`, `delegation_rejected`, and `delegation_error` metadata events. Graceful close drains already-queued terminal events before the iterator completes (same core multiplexer contract). Hosts may project those events through observability `handleDelegation()` using the parent Prism run ID; no OpenTelemetry dependency enters this package.
31
+ `delegate()` returns the child's `AgentRunResult` or throws its `AgentRunError`/a supervisor denial or limit error. A failure (an error or a run-limit death) publishes `child_failed` before the terminal `delegation_error`: the redacted `reason`, the terminal `status` and `stopReason`, the plan-086/087 `RunLimitBreach` (`limit`, `maximum`, `observed`) in `limit` when a configured ceiling fired, and terminal `usage`. Host cancels, policy denials, and hook rejections are not failures and never emit it. `delegateAsync()` returns a local running handle; `wait()` returns its result (or `{ status: "cancelled" }` after `cancel()`), and stays idempotent while its terminal record is retained (bounded by `limits.maxQueuedEvents`; an evicted or foreign id returns the same non-enumerating error). `subscribe()` emits bounded `delegation_started`, `delegation_finished`, `delegation_rejected`, and `delegation_error` metadata events, plus the opt-in child-event family below. Aborting `CreateSupervisorOptions.signal` aborts every running child (session- and task-lifetime) and closes the stream. Hosts routing child events onto a parent session stream pass `childEventSink`; it receives the identical payload the supervisor stream carries — a redacted, capped, rate-coalesced `AgentEvent` tagged with `child: { childId, delegationId, depth }` (contract type `ChildEventOrigin`) — so a parent subscriber can route it with `event.child` and no per-type handling. Hosts may project the lifecycle events through observability `handleDelegation()` using the parent Prism run ID; no OpenTelemetry dependency enters this package.
32
+
33
+ ### Child lifetime, reporting, and budget share
34
+
35
+ Every `SupervisorChild` may carry a `policy` of host ceilings/defaults; a `DelegationRequest` may only narrow them (report is clamped to the ceiling, `everyTurns` can only be raised, budget share takes the lower value, and session lifetime is denied unless the host enabled it). Defaults are exactly the 0.8 behavior: `lifetime: "task"`, `report: "on-complete"`, no milestone, no share.
36
+
37
+ - **Lifetime.** `task` children stay linked to the caller signal. `session` children must be started with `delegateAsync` (`spawn_agent` routes them to it automatically): they detach from the caller and ancestor-child signals, keep running across parent turns, hold an `activeChildren` slot until they end, and stop on `cancel(delegationId)` or when `CreateSupervisorOptions.signal` aborts.
38
+ - **Report.** `on-complete` publishes nothing per turn (the default). `milestones` publishes `child_milestone` (`turn`, redacted `childEvent`) when `milestone.everyTurns` divides the child turn or a host `milestone.predicate` matches; with neither configured it reports the milestone event subset. `stream` publishes every per-turn provider/tool/turn event as `delegation_child_event` — never per-token `message_delta` or full `message_*` payloads; the supervisor-wide `childEvents: true` does the same for children without their own policy.
39
+ - **Budget share.** `budgetShare` (0, 1] scales the inherited `maxSteps`/`maxToolCalls`/`maxTokens`/`timeoutMs` before `narrowSupervisorLimits` clamps them, so a child can never exceed its parent or host limits. It is a fraction of the *inherited supervisor limits*, not of live parent-run usage (the tool boundary carries no parent budget snapshot).
40
+ - **Caps and rate.** Projected child events pass the supervisor `redactor` first, are capped by `limits.maxChildEventsPerDelegation` and `limits.maxChildEventBytes`, and are rate-coalesced to `limits.maxChildEventsPerSecond` (default 10/s per child, floor 1/s window). A capped child publishes one `delegation_child_events_capped`; coalescing publishes `delegation_child_events_coalesced` with the dropped count when a window closes or the pump stops, so gaps are never silent. Size the rate to the host event loop: 10/s per child is trivial for a UI; raise it only for a child whose tool events are the UI.
32
41
 
33
42
  ### Child event passthrough (opt-in)
34
43
 
35
- `createSupervisor({ childEvents: true })` projects a redacted, size-capped **milestone** subset of child `AgentEvent`s onto the same stream as `delegation_child_event` (tagged `childId`, `delegationId`, `depth`). v1 covers run start/finish/`suspended`/`denied` and tool-execution started/finished/error/blocked — not per-token `message_delta`. Default off: the stream is byte-identical to today (no subscribe, no allocation). Caps: `limits.maxChildEventsPerDelegation` (256/4096) and `limits.maxChildEventBytes` (32 KiB/256 KiB); exceeding either drops further child events and emits one `delegation_child_events_capped` marker (never throws). Events pass through the supervisor `redactor` before emission. Children never receive supervisor internals or store/subscription access. Resume-path rebuilds (`resumeNestedRun`) attach the same pump to the rebuilt child session, so a delegation that suspended for approval keeps projecting milestones after the root run resumes; counters restart per pump, so each attempt gets the full cap.
44
+ `createSupervisor({ childEvents: true })` raises the default report ceiling to `stream` for children without their own `policy.report`: it projects redacted, size-capped child `AgentEvent`s onto the same stream as `delegation_child_event` (tagged `childId`, `delegationId`, `depth`). Covered: run start/finish/`suspended`/`denied`, tool-execution started/finished/error/blocked, and turn/provider-turn started/finished — not per-token `message_delta` or full `message_*` payloads. Default off: the stream is byte-identical to today (no subscribe, no allocation). Per-child `policy.report` is authoritative; a model request can only lower it. Caps: `limits.maxChildEventsPerDelegation` (256/4096), `limits.maxChildEventBytes` (32 KiB/256 KiB), and `limits.maxChildEventsPerSecond` (10/1000); exceeding count/bytes drops further child events and emits one `delegation_child_events_capped` marker, exceeding the rate coalesces into `delegation_child_events_coalesced` (never throws). Events pass through the supervisor `redactor` before emission. Children never receive supervisor internals or store/subscription access. Resume-path rebuilds (`resumeNestedRun`) replay the persisted report/every-turns/share policy onto the rebuilt child session, so a delegation that suspended for approval keeps projecting milestones after the root run resumes; counters restart per pump, so each attempt gets the full cap.
45
+
46
+ ### Recovery telemetry
47
+
48
+ `summary()` returns one frozen row per allow-listed child — `{ childId, attempts, retries, failures, failureRadius, outcome }` — maintained incrementally (O(1) per delegation, O(children) to read) and cumulative for the supervisor's lifetime, so a host can diff snapshots per root run or watch a long-lived supervisor without host-side aggregation.
49
+
50
+ - `outcome` is `idle` before the first delegation, `running` while any is live, otherwise the `delegation_finished.status` vocabulary (`succeeded`/`failed`/`aborted`/`suspended`/`denied`) or `rejected` for a hook denial. Resuming a suspended run updates the outcome but is not a new attempt.
51
+ - `attempts` counts started delegations, hook rejections included. `retries` counts attempts started after a `failed`/`aborted` outcome — the recovery re-dispatch metric.
52
+ - `failures` counts delegations that died on an error or a limit; host cancels, denials, and hook rejections are excluded.
53
+ - `failureRadius` is the blast radius of the child's most recent failure: task-lifetime descendant delegations still live at that moment. An unrelated or already-finished child is not counted, and a session-lifetime child is detached from the failed subtree by design.
54
+
55
+ Failure attribution is the same object the `child_failed` event carries, so a host that only keeps the summary and one that only keeps events read the same taxonomy.
36
56
 
37
57
  ## Request/response example
38
58
 
@@ -63,7 +83,7 @@ const result = await supervisor.delegate({ childId: "research", input: "Check so
63
83
 
64
84
  ## Model-facing spawn tool
65
85
 
66
- `createSpawnAgentTool({ supervisor })` turns the same host-owned child allow-list into non-exclusive `spawn_agent` tool calls, so independent calls use the parent session's `toolConcurrency`. The schema has only `childId`, `input`, optional `threadId`, and `mode: "sync" | "async"` (default `sync`); unknown children fail closed as standard tool errors before delegation. Model arguments cannot supply child tools, identity, scopes, or higher limits. Async returns only a local `{ delegationId, status: "running" }` handle. Install `wait_agent` once per handle for wait-all, or `cancel_agent` to abort it; cancellation is terminally reported by `wait_agent`. Parent-run abort propagates to running children. Handles are in-process, ownership-scoped, and bounded — they do not survive host restart.
86
+ `createSpawnAgentTool({ supervisor })` turns the same host-owned child allow-list into non-exclusive `spawn_agent` tool calls, so independent calls use the parent session's `toolConcurrency`. The schema has only `childId`, `input`, optional `threadId`, `mode: "sync" | "async"` (default `sync`), `lifetime: "task" | "session"`, `report`, `milestone.everyTurns`, and `budgetShare`; unknown children and malformed policy args fail closed as standard tool errors before delegation. Model arguments cannot supply child tools, identity, scopes, predicate functions, or higher limits. Async returns only a local `{ delegationId, status: "running" }` handle; a `lifetime: "session"` spawn is always async. Install `wait_agent` once per handle for wait-all, or `cancel_agent` to abort it (also the explicit end for a session-lifetime child); cancellation is terminally reported by `wait_agent`. Parent-run abort propagates to running task-lifetime children only. Handles are in-process, ownership-scoped, and bounded — they do not survive host restart.
67
87
 
68
88
  ```ts
69
89
  import { createAgent } from "@arnilo/prism";
@@ -109,7 +129,7 @@ Supervisors propagate parent `identity` and `effectStore` to every child agent/r
109
129
  - [A2A interoperability](a2a.md): separate remote protocol boundary. `A2ATaskLifecycle` adapts host durable agent/workflow state directly; it does not route A2A execution through local supervisor child planning.
110
130
  - [Workflows](workflows.md): preferred deterministic orchestration.
111
131
  - [Coding workspaces](coding-workspaces.md): opt-in per-child worktree isolation via `createWorktreeChildFactory`.
112
- - [Coding agent tools](coding-agent-tools.md): opt-in `observeSupervisorLifecycle` bridges supervisor `delegation_*` events to coding `subagent_started` / `subagent_stopped` for host timelines.
132
+ - [Coding agent tools](coding-agent-tools.md): opt-in `observeSupervisorLifecycle` bridges supervisor `delegation_*` events to coding `subagent_started` / `subagent_stopped` for host timelines; `supervisor.summary()` covers recovery counters (`retries`, `failures`, `failureRadius`) that the lifecycle bridge does not carry.
113
133
  - Examples: [`examples/autonomous-coding-loop.ts`](../examples/autonomous-coding-loop.ts) — per-child models, factory returns `Agent`; [`examples/spawn-agent-tool.ts`](../examples/spawn-agent-tool.ts) — two model-requested explore children in one tool turn.
114
134
  - [Working and semantic memory](working-and-semantic-memory.md): child scope construction.
115
135
  - [Host security](host-security.md): permission and credential boundaries.
@@ -0,0 +1,157 @@
1
+ # Telegram channel
2
+
3
+ ## What it does
4
+
5
+ `@arnilo/prism-channels/telegram` is framework-free Telegram Bot API transport for text, with bounded attachment handling. It uses native `fetch`; importing it or creating a factory starts no listener, poller, credential lookup, environment lookup, or network request.
6
+
7
+ ## When to use it
8
+
9
+ Use for official Telegram Bot API text and bounded media. Private DMs are the default; group/supergroup text (including forum topics) is opt-in per adapter with `allowGroups: true` and stays subject to host `authorize`. Optional ephemeral streaming previews (`sendDrafts`) are private-chat only. Opt-in host hooks add voice transcription, document extraction and speech synthesis; images reach a model only when it declares image input. Not for Secret Chats, unbounded or stored media, or model-selected destinations.
10
+
11
+ Install peer and channel package:
12
+
13
+ ```bash
14
+ npm install @arnilo/prism @arnilo/prism-channels
15
+ ```
16
+
17
+ ## Inputs / request
18
+
19
+ `TelegramAdapterOptions` (polling): host `connectionId`, `botToken` (`CredentialValueSource`), service-owned `checkpoints`/`leases`/`cursorOwnership`, optional `allowGroups`, `sendDrafts`, `maxAttachmentBytes`, `transcribe`, `synthesize` and `extractDocumentText`. `TelegramWebhookHandlerOptions` add independent `webhookSecret` and the `admit` callback (intake only — outbound sends, drafts and `fetchAttachment` use an adapter instance, which works without `start()`). Importing or constructing starts no network.
20
+
21
+ ## Request/response example
22
+
23
+ Polling is explicit and restart-safe only with a service-owned `CheckpointStore` cursor and `LeaseStore` receiver lease. `start()` verifies the token with `getMe`, refuses to poll if `getWebhookInfo` reports an existing webhook, then runs 30-second `getUpdates` requests. It accepts private `message.text`/`caption` updates from non-bot senders, plus `group`/`supergroup` text and media when `allowGroups: true`. `photo`, `voice` and `document` become bounded attachment *refs* — see [Attachments and voice](#attachments-and-voice). Service, edit and bot-echo updates are acknowledged without agent execution, as are channel/anonymous-admin `sender_chat` messages and forum topics that fail the host grant.
24
+
25
+ A parsed group message keeps the admission keys transport-exact: `externalConversationId` is the chat id (negative for groups), `externalActorId` is `from.id`, and `message_thread_id` becomes `threadId`. Only `claims` carry untrusted context (`chatType`, `isForum`, `forwarded`), and `authorize` receives them so a host can refuse forwarded messages or non-forum groups. Threads and senders are separate sessions and separate grants: a grant for `(chat A, topic 7, user X)` admits neither user Y, chat B, nor topic 8.
26
+
27
+ ```ts
28
+ import { createMessagingRuntime } from "@arnilo/prism-channels";
29
+ import { createTelegramAdapter } from "@arnilo/prism-channels/telegram";
30
+
31
+ const adapter = createTelegramAdapter({
32
+ connectionId: "support-telegram",
33
+ botToken: hostCredentials.telegramBotToken, // string, function, or CredentialValueSource
34
+ checkpoints: serviceCheckpoints,
35
+ leases: serviceLeases,
36
+ cursorOwnership: { tenantId: "service" }, // service scope, not a Telegram claim
37
+ allowGroups: true, // opt-in groups/topics; still deny-by-default at authorize()
38
+ maxAttachmentBytes: 1024 * 1024, // per `getFile` download; hard ceiling 4 MiB
39
+ transcribe: (audio, format) => transcription.transcribe({ model: "whisper-1", audio, format }), // voice -> turn text
40
+ synthesize: (text) => speech.synthesize({ model: "tts-1", input: text }), // text -> voice note, final replies only
41
+ extractDocumentText: (bytes, mimeType) => hostExtractor(bytes, mimeType), // documents are inert without this
42
+ });
43
+
44
+ const runtime = createMessagingRuntime({
45
+ // Exact observed tuple: chat id, optional topic id, sender id. Display names are never keys.
46
+ authorize: ({ externalConversationId, threadId, externalActorId, claims }) =>
47
+ claims?.forwarded === true
48
+ ? false
49
+ : (hostGrants.get(externalConversationId, threadId, externalActorId) ?? false),
50
+ resolveAgent: hostResolveAgent,
51
+ deliver: (reply) => adapter.send(reply),
52
+ onAssistantDelta: (delta) => adapter.sendDraft(delta), // opt-in previews; needs sendDrafts: true
53
+ fetchAttachment: (ref) => adapter.fetchAttachment(ref), // lets a model that declares image input receive the image
54
+ checkpoints: userCheckpoints,
55
+ leases: userLeases,
56
+ });
57
+
58
+ await adapter.start((event) => runtime.admit(event));
59
+ // shutdown: await adapter.stop(); await runtime.stop();
60
+ ```
61
+
62
+ [`examples/telegram-agent.ts`](../examples/telegram-agent.ts) exposes same composition as a host-supplied helper.
63
+
64
+ The adapter awaits `runtime.admit()` before advancing `offset`. Return the admission result from the callback: `capacity`, `unavailable`, and `stopped` keep Telegram's update unacknowledged; all other settled dispositions advance the durable cursor. The operation journal then deduplicates any replay before model or tool work.
65
+
66
+ A connection receiver lease (`prism.channels.v1.telegram.receiver`) is held for the poll loop. A second poller or a webhook worker using the same `connectionId`, service ownership and `LeaseStore` receives no work. This fences stored intake state; it cannot retract an already accepted platform request from a paused process.
67
+
68
+ ## Implementation example
69
+
70
+ Mount `createTelegramWebhookHandler` on a host HTTPS route. It is a Web-standard `(Request) => Promise<Response>` handler; Prism does not create a listener, configure DNS/TLS, or call `setWebhook` for you.
71
+
72
+ ```ts
73
+ import { createTelegramWebhookHandler } from "@arnilo/prism-channels/telegram";
74
+
75
+ const telegramWebhook = createTelegramWebhookHandler({
76
+ connectionId: "support-telegram",
77
+ botToken: hostCredentials.telegramBotToken,
78
+ webhookSecret: hostSecrets.telegramWebhookSecret, // 1–256 URL-safe characters
79
+ admit: (event) => runtime.admit(event),
80
+ leases: serviceLeases,
81
+ cursorOwnership: { tenantId: "service" },
82
+ allowGroups: true, // must match the poller you are replacing
83
+ });
84
+
85
+ // Example host adapter: route only this fixed HTTPS path to telegramWebhook(request).
86
+ ```
87
+
88
+ Configure Telegram deliberately after the route, TLS and independent secret exist. Do not set `drop_pending_updates` and do not remove an active webhook merely to switch modes.
89
+
90
+ ```sh
91
+ curl --fail-with-body \
92
+ -H 'content-type: application/json' \
93
+ -d '{"url":"https://bot.example/telegram","secret_token":"<independent-secret>","allowed_updates":["message","callback_query"]}' \
94
+ "https://api.telegram.org/bot<BOT_TOKEN>/setWebhook"
95
+ ```
96
+
97
+ The handler requires `POST` + JSON, verifies `X-Telegram-Bot-Api-Secret-Token` with a fixed-length hash comparison before body parsing, bounds body bytes, acquires the same receiver lease, and calls `admit` before returning `204`. Bad secret/body returns `401`/`400`/`413`; lease, capacity, stopped runtime, or journal unavailability returns `503` so Telegram retries. Untrusted but final inputs (bot/media/unknown sender/anonymous admin, or group updates when `allowGroups` is off) return `204` without an agent call. `allowGroups` is parsed identically in both modes; set it the same on the webhook worker and the poller you replaced.
98
+
99
+ `callback_query` is promptly acknowledged with `answerCallbackQuery` and only accepts the runtime's `p:a:<opaque-token>` / `p:d:<opaque-token>` callback shape from a non-bot sender in an admitted private, group, or topic conversation (a topic callback keeps its `message_thread_id` as `threadId`). The adapter maps it to an `approval` event; callback data never becomes model input. The runtime validates its short-lived server-side binding before core resume.
100
+
101
+ ## Outputs / response / events
102
+
103
+ `send()` accepts only replies addressed to its own `connectionId`; the runtime fixes `externalConversationId` and the reply `threadId` from the authorized binding, so a topic answer posts to that topic's `message_thread_id` and the model cannot choose either. It sends plain text, chunks at 3,500 UTF-16 code units without splitting surrogate pairs, and retries documented `retry_after` delays up to five attempts. A bounded server-issued control set renders as an inline keyboard on the final chunk; arbitrary model text cannot create buttons. There is no Markdown parse mode.
104
+
105
+ Known API failures return `{ delivered: false, reason }`. A network/timeout outcome, or a later failure after a prior chunk was accepted, rejects with a bounded ambiguous-delivery error. The common durable reply journal records thrown delivery as `delivery_unknown`; do not automatically resend it. Bot token URLs are never included in adapter errors and redirects are forbidden.
106
+
107
+ Telegram's `getUpdates` and webhooks are mutually exclusive. Updates remain available for at most 24 hours; the cursor is an ordering/acknowledgment record, not an exactly-once agent execution guarantee. Bots are not Secret Chats.
108
+
109
+ ### Streaming previews (drafts)
110
+
111
+ With `sendDrafts: true` and `MessagingRuntimeOptions.onAssistantDelta: (delta) => adapter.sendDraft(delta)`, the adapter renders the answer while it is produced through Bot API `sendMessageDraft`: same chat and `message_thread_id` as the bound turn, `draft_id` fixed at one preview slot per chat, text capped to the last 3,500 UTF-16 code units. Previews are coalesced — one request in flight, the latest partial wins — so a token stream never floods the API, and failures are ignored because a draft is only a preview. Cancel, `stop()` and run end stop further previews. Drafts are private-chat only: group and supergroup chats are skipped even when `allowGroups` and `sendDrafts` are both on. No draft is ever a `ChannelReply`: nothing is journaled, no controls or attachments can ride along, and the terminal `sendMessage` remains the only message the user keeps.
112
+
113
+ ### Attachments and voice
114
+
115
+ A `photo`, `voice` or `document` message is parsed into at most eight `ChannelAttachmentRef` values on the event: `kind`, `transportFileId` (Telegram `file_id`), optional `mimeType`, `byteLength` and `fileName`. A photo collapses to its largest size. The event never carries bytes, a URL, a caption-as-identity, or a model-chosen id, and nothing here can be addressed by model output.
116
+
117
+ Before `admit`, the adapter runs the one bounded fetch the host opted into (Bot API `getFile` plus a capped download):
118
+
119
+ - `voice` + host `transcribe` → the transcript becomes ordinary turn text (`caption`, then transcript). The audio is discarded after the call.
120
+ - `document` + host `extractDocumentText` → the extracted text becomes turn text. Without the hook the document stays inert: the message admits with an attachment ref and no text, and the runtime answers with a bounded "cannot be processed" notice instead of a model call.
121
+ - `photo` is never fetched by the adapter. The runtime fetches it (through `fetchAttachment`) only when the resolved agent's model declares `image` input, then sends it as one image content block next to the caption; otherwise the turn is refused with the same notice and no model call. `image` is the only attachment kind that becomes model input — audio/documents are text-only because Prism receives their host-produced text.
122
+
123
+ Oversize fails closed twice: the runtime denies an event whose *declared* sizes exceed `maxAttachmentBytes` (`denied: oversized`, no fetch, no model call), and the adapter refuses to buffer a body whose `Content-Length` or streamed size exceeds the same cap. Failures (network, credential, API, malformed `file_path`) yield no bytes, so the turn is refused rather than run without the attachment.
124
+
125
+ Outbound, `synthesize` adds a Bot API `sendVoice` next to the text for `final` replies: same bound chat and `message_thread_id`, OGG/OPUS, MP3 or M4A formats only, best-effort — a synthesis or upload failure never fails or re-sends the delivered text reply. Notices stay text-only. Nothing hangs a voice note on model-selected destinations.
126
+
127
+ ## Extension and configuration notes
128
+
129
+ | Setting | Default | Range |
130
+ | --- | ---: | ---: |
131
+ | Poll timeout | 30 s | 1–50 s |
132
+ | Poll batch | 100 | 1–100 |
133
+ | Receiver lease | 90 s | poll timeout + 10 s–5 min |
134
+ | Webhook/API body | 128 KiB | 1 KiB–1 MiB |
135
+ | Outbound chunk | 3,500 UTF-16 units | fixed (under Telegram's 4,096 limit) |
136
+ | Known-send attempts | 5 | fixed |
137
+ | Group/topic text (`allowGroups`) | off | boolean |
138
+ | Streaming previews (`sendDrafts`) | off | boolean (private chats only) |
139
+ | Attachment bytes (`maxAttachmentBytes`) | 1 MiB | 1 B–4 MiB |
140
+ | Voice transcription (`transcribe`) | off | host `TranscriptionProvider` wrapper |
141
+ | Speech synthesis (`synthesize`) | off | host `SpeechProvider` wrapper (final replies) |
142
+ | Document text (`extractDocumentText`) | off | host extractor |
143
+
144
+ ## Security and performance notes
145
+
146
+ The webhook secret is independent of the bot token and compared with a fixed-length hash before body parsing. Redirects are forbidden; bot-token URLs never appear in adapter errors. Polling and webhooks are mutually exclusive. Model text cannot create inline buttons.
147
+
148
+ Groups and topics are off by default and never self-authorize: the adapter only parses an observed `(chat id, topic id?, user id)` and the host grant decides, so a group membership claim is not an identity. Chat titles, usernames and display names are never actor ids. Bots, `sender_chat` (channel posts and anonymous admins), and non-text updates are dropped before admission; forwarded messages are admitted but flagged in `claims` so the grant can refuse them. In groups Telegram privacy mode limits what a bot receives (commands and replies to it) unless an operator disables it in BotFather; Prism does not work around that platform behavior. Signal stays DM-only.
149
+
150
+ Drafts are display-only and cannot become input: they carry no controls or attachments, the destination chat/thread always comes from the bound event (never from model output or from the delta), text is redacted with the same redactor as finals, and the 3,500-unit cap keeps the request inside Bot API limits. Since `sendDraft` is fire-and-forget and failures are swallowed, a draft can never fail a turn, mask a delivery failure, or replace the journaled final reply.
151
+
152
+ Media is bounded on every axis. Only the current event's refs are ever fetched (`fetchAttachment` takes a ref, and the runtime only passes refs of the event it is running), the cap is enforced twice (declared size and streamed body), and downloads use the same fixed origin with redirects forbidden. Bytes live only for the fetch call: they are never journaled (`ChannelOperationRecord` and `ChannelReplyRecord` store text only), never logged, and never written to disk. A `file_id` is not a capability — the bot proves nothing to Telegram by holding one, and a caller-supplied or model-invented id yields at most one bounded request inside the cap. Hosts wire `transcribe`/`synthesize`/`extractDocumentText` to their own provider wrappers; `@arnilo/prism-channels` never depends on `@arnilo/prism-providers`.
153
+
154
+ ## Related APIs
155
+
156
+ - [Messaging channels](messaging-channels.md): authorization, durable operations and reply recovery.
157
+ - [Web-standard server handler](server.md): mounting a `Request`/`Response` handler in a Node or framework host.
package/docs/testing.md CHANGED
@@ -21,14 +21,14 @@ Documents how the hermetic suite runs, which stage a new suite belongs to, and t
21
21
  | build race | `scripts/phase23-build-race.test.mjs` |
22
22
  | workspace suites | `npm run test --workspaces --if-present` |
23
23
 
24
- Protected-environment legs (Postgres, PTY, NATS, live credentials) are not part of `npm test`; they fail closed with one canonical `BLOCKED GATE <id> requires=<names> evidence=<surface> hint=<how to unblock>` record and a non-zero exit when their infrastructure is absent (registry and audit: `node scripts/blocked-gate.mjs`). Retired phase freeze/release gates live in `scripts/` for audit but are deliberately kept out of the chain. 0.7.0 host-completeness packed proof is `scripts/fixtures/e2e-070-host-completeness-journey.mjs` (same packed consumer as the full-surface journey) plus `scripts/host-completeness-evidence.test.mjs`; live legs stay skip-not-fail. R16/R17 stay blocked until plans 077/074 ship.
24
+ Protected-environment legs (Postgres, PTY, NATS, live credentials) are not part of `npm test`; they fail closed with one canonical `BLOCKED GATE <id> requires=<names> evidence=<surface> hint=<how to unblock>` record and a non-zero exit when their infrastructure is absent (registry and audit: `node scripts/blocked-gate.mjs`). A successful `PRISM_TEST_POSTGRES_URL=… npm run test:postgres` first removes stale evidence, then writes gitignored `scripts/postgres-evidence.json` with only current `gitHead`, capture time, and TAP counts; release evidence accepts it only at the same `HEAD`. Retired phase freeze/release gates live in `scripts/` for audit but are deliberately kept out of the chain. 0.7.0 host-completeness packed proof is `scripts/fixtures/e2e-070-host-completeness-journey.mjs` (same packed consumer as the full-surface journey) plus `scripts/host-completeness-evidence.test.mjs`; live legs stay skip-not-fail. R16/R17 stay blocked until plans 077/074 ship.
25
25
 
26
26
  ## Isolation rules
27
27
 
28
28
  - **Scratch roots come from the OS.** A suite that writes anything creates its root with `mkdtempSync(join(tmpdir(), "prism-…"))` and removes it in `after()`. Never rely on `process.cwd()` for write targets: the same suite runs with different working directories (workspace stage vs. root stage), so a cwd-relative root silently writes into the repository.
29
29
  - **Pass explicit roots.** Wiki, memory, and store helpers default `workspaceRoot` to `process.cwd()`; suites pass their scratch root (and a `wikiRoot` relative to it) instead of accepting the default.
30
30
  - **Tracked fixtures stay byte-identical.** `packages/memory/.wiki/` is a tracked wiki fixture and `docs/` is a tracked corpus. `scripts/wiki-scratch-isolation.test.mjs` runs the wiki suites from the package and from the repository root and fails if the tracked fixture hashes change, if a new file appears inside the fixture, if `<repo>/.wiki/` is scaffolded, or if the old cwd-relative scratch directories reappear.
31
- - **Gates never write inside the repository.** A gate asserts against tracked content and spawns suites in temporary directories only. A gate that spawns `node --test` must strip `NODE_TEST_CONTEXT`/`NODE_TEST_WORKER_ID` from the child environment (an inherited value makes the nested runner skip every file and still exit 0) and assert the child reported a non-zero pass count.
31
+ - **Gates never write inside the repository.** A gate asserts against tracked content and spawns suites in temporary directories only. A gate that spawns `node --test` must strip `NODE_TEST_CONTEXT`/`NODE_TEST_WORKER_ID` from the child environment (an inherited value makes the nested runner skip every file and still exit 0) and assert the child reported a non-zero pass count. The wiki gate additionally uses `--test-isolation=none`: all nested files run in its one runner process, avoiding process-worker IPC deserialization without retrying failures.
32
32
  - **Wait by polling, not by sleeping.** Async browser state (download quarantine, idle reaping) is not awaitable from the outside — `manager.ts` settles it on a fire-and-forget listener promise — so a fixed sleep is a race that loses under CPU load and fails the assertion for a reason unrelated to the behavior under test. Suites poll observable state through `waitFor(read, ok, label, { timeoutMs, intervalMs })` in `packages/web-tools/src/browser/__tests__/wait-for.ts`, which returns as soon as the state appears and otherwise throws naming the label and the last observed value. Fixed sleeps remain only where real elapsed time is the subject of the test (idle TTLs).
33
33
 
34
34
  ## Related APIs
package/docs/tools.md CHANGED
@@ -159,6 +159,23 @@ await session.run(input, { toolNames: ["web_search"] });
159
159
 
160
160
  Scope the active `ToolRegistry` (or declarative `AgentDefinition.tools`) at agent construction. `PermissionPolicy` / `RunOptions.validate` still fail closed at dispatch; `toolNames` only intersects that host-active set.
161
161
 
162
+ ### Per-turn tool narrowing
163
+
164
+ `toolNarrowing` on `AgentConfig` / `RunOptions` (run wins) is an optional host callback invoked at `loopCtx.assemble` before each provider turn. It receives `{ turn, lastAssistantText?, toolIds }` and must return a subset of the run grant (`toolIds`). Unknown or extra names are dropped (restrictive-only); the runtime emits `tool_narrowing_clamped` with the dropped names and continues with the clamped set. A throw fails the turn — no partial schema is sent.
165
+
166
+ This is not a middleware hook and not `RunOptions.tools`. `filterTools` on the run snapshot preserves run order, so identical consecutive subsets keep schema bytes identical (prompt-cache prefixes stay stable). Changing the subset rewrites tool schemas; pair with `toolsDisclosure: "search"` when the run set is large.
167
+
168
+ Tools hidden this turn are not in the provider schema. They stay callable-by-name only when `allowHiddenToolCalls: true` (default off); otherwise dispatch blocks them with `tool_denied`. Each provider turn records `metadata.tools: { count, idsHash }` (hash of names in request order; no args) — see [Agent events](agent-events.md).
169
+
170
+ ```ts
171
+ const agent = createAgent({
172
+ model, provider, tools,
173
+ toolNarrowing: async ({ turn, lastAssistantText, toolIds }) =>
174
+ plane === "knowledge" ? toolIds.filter((id) => id.startsWith("wiki.")) : toolIds,
175
+ });
176
+ await session.run(input, { toolNarrowing: async ({ toolIds }) => toolIds.slice(0, 4) });
177
+ ```
178
+
162
179
  ### Artifact-loop tools
163
180
 
164
181
  `generate-validate-revise` treats provider tools as inert by default. Set `loop.toolCalls: "bounded"` and `RunOptions.limits.maxToolRounds` only when an artifact needs a host-owned lookup before its next candidate. Each response with one-or-more calls consumes one shared round, dispatches calls sequentially through this exact `dispatchToolCall()` path, persists assistant-call then result transcript rows, and skips artifact parsing/validation for that response. A post-limit call executes nothing; the loop emits `artifact_failed` with `metadata.reason: "tool_round_limit"`. Tools do not consume `maxRevisions`, and tool schemas/context never grant authority.
package/docs/wiki.md CHANGED
@@ -85,7 +85,7 @@ The staging primitive behind `/wiki-ingest`, `wiki_ingest`, and the CLI. Accepts
85
85
  | :--- | :--- |
86
86
  | Text-like files and `text` | Decoded as UTF-8 (RAG text/markdown/html parsers) |
87
87
  | Uncompressed PDF | Parsed by the RAG PDF parser (bounded pages/bytes) |
88
- | Compressed PDF / DOCX | Throws a named error unless the host supplies `options.extractDocument` (e.g. wire `createDocumentReader()` from `@arnilo/prism-coding-tools/document-reader`) |
88
+ | Compressed PDF / DOCX | Throws a named error unless the host supplies `options.extractDocument` (e.g. wire `createDocumentReader()` from `@arnilo/prism-work/document-reader`) |
89
89
  | `url` | `assertSsrfAllowedUrl` runs first (private/link-local hosts rejected before any fetch); then the host `fetchUrl` hook supplies the bytes/text — missing or empty hook output fails closed. Staged filename comes from the hook, the URL extension (`doc.pdf`), or `source.md` |
90
90
  | Images | Staged as-is; stub extract points at the staged `source.*` — no OCR; view the file |
91
91
  | Unknown binary | Fails closed unless `extractDocument` claims it |
@@ -97,7 +97,7 @@ export const handler = createArtifactHandler({ service: artifacts, authorize: ho
97
97
 
98
98
  ## Business action drafts and editable approvals (0.7.0)
99
99
 
100
- Business tools (e.g. mail, calendar, documents in `@arnilo/prism-core/integrations/work`) record mutations through durable `WorkDraftStore` drafts before execution. Human reviewers can approve, deny, or edit draft payloads directly:
100
+ Business tools (e.g. mail, calendar, documents in `@arnilo/prism-work/connectors`) record mutations through durable `WorkDraftStore` drafts before execution. Human reviewers can approve, deny, or edit draft payloads directly:
101
101
  - AG-UI clients advertise and send `approveWithEdits` with revised arguments (`editedArgs`/`modifiedArguments`).
102
102
  - The server resume endpoint accepts `{ decision: "approve", modifiedArguments: { ... } }` under CAS `expectedVersion`.
103
103
  - If arguments are modified, a new draft revision is created with bumped revision number and payload digest. The previous revision's approval is invalidated and the mutation requires approval for the revised content.
@@ -1,33 +1,33 @@
1
1
  # Work connectors
2
2
 
3
- Least-privilege Microsoft 365 and Google Workspace connectors live in `@arnilo/prism-core/integrations/work`.
3
+ Least-privilege Microsoft 365 and Google Workspace connectors live in `@arnilo/prism-work/connectors`.
4
4
 
5
5
  ## Principles
6
6
 
7
- 1. **Host-pinned binary** — Prism never downloads or shells an untrusted CLI path.
8
- 2. **Hard-coded argv templates** — models choose typed tool args; they never supply command strings.
7
+ 1. **Host-pinned binary or HTTP adapter** — Prism never downloads or shells an untrusted CLI path; HTTP adapters use fixed origins and pinned fetch.
8
+ 2. **Hard-coded operation maps** — models choose typed tool args; they never supply command strings or request URLs.
9
9
  3. **Draft-then-approve & durable resumption** — mutations create a draft with tracked revisions and payload digests; side effects run only after host approval binds to that exact revision; durable checkpoint persistence survives process restart.
10
10
  4. **Idempotent retries** — `IdempotencyStore` keyed by identity + operation key.
11
11
  5. **Isolated config** — per-identity `configDir` (CLI `HOME`); no credential argv.
12
- 6. **Shared result shapes** — mail/calendar/file/task list/get tools normalize onto `WorkMailMessage` / `WorkCalendarEvent` / `WorkFileItem` / `WorkTaskItem` without hiding provider-specific ops.
12
+ 6. **Shared result shapes** — mail/calendar/file/task list/get tools normalize onto `WorkMailMessage` / `WorkCalendarEvent` / `WorkFileItem` / `WorkTaskItem` without hiding provider-specific ops. Binary file gets return only untrusted artifact/path metadata plus hash and byte length.
13
13
 
14
14
  ## Microsoft 365
15
15
 
16
- See [Work tools](work-tools.md). Adapter: `createMicrosoft365CliAdapter` / subpath `@arnilo/prism-core/integrations/work/microsoft365`.
16
+ See [Work tools](work-tools.md). Adapters: `createMicrosoft365CliAdapter` or `createMicrosoft365HttpAdapter` from `@arnilo/prism-work/connectors`.
17
17
 
18
- Uses [@pnp/cli-microsoft365](https://pnp.github.io/cli-microsoft365/) commands such as `outlook message list|get`, `outlook mail send`, `outlook event list|add`, `file list|add`, `spo file sharinglink add`. To Do / Planner / Teams remain capability-gated.
18
+ The CLI adapter uses [@pnp/cli-microsoft365](https://pnp.github.io/cli-microsoft365/) commands such as `outlook message list|get`, `outlook mail send`, `outlook event list|add`, `file list|add|copy`, `spo file sharinglink add`. The HTTP adapter maps its fixed operation set to `graph.microsoft.com` through pinned fetch; tokens reach it only in `Authorization`. `m365_file_get` accepts only an item ID and downloads via fixed `/me/drive/items/{id}/content` into a scanned artifact and/or contained sandbox path; it never emits bytes to model context. Upload drafts accept a host path, artifact ref, or contained sandbox path and bind the content hash before approval. It requires direct Graph Drive-item URLs for one-request file list/upload/copy and rejects arbitrary SharePoint links. To Do / Planner / Teams remain capability-gated.
19
19
 
20
20
  ## Google Workspace
21
21
 
22
- See [Work tools](work-tools.md). Adapter: `createGoogleWorkspaceCliAdapter` / subpath `@arnilo/prism-core/integrations/work/google-workspace`.
22
+ See [Work tools](work-tools.md). Adapters: `createGoogleWorkspaceCliAdapter` or `createGoogleWorkspaceHttpAdapter` from `@arnilo/prism-work/connectors`.
23
23
 
24
- Uses [`@googleworkspace/cli` (`gws`)](https://github.com/googleworkspace/cli): `gmail users messages list|get`, `gmail +send`, `calendar events list|insert`, `drive files list|create`, `drive permissions create`, `tasks tasks *`. Docs/Sheets/Slides create remain capability-gated. Discovery `schema` and `auth`/`login`/`setup` are forbidden from Prism argv.
24
+ The CLI adapter uses [`@googleworkspace/cli` (`gws`)](https://github.com/googleworkspace/cli): `gmail users messages list|get`, `gmail +send`, `calendar events list|insert`, `drive files list|create`, `drive permissions create`, `tasks tasks *`, and capability-gated Docs/Sheets/Slides create and fixed update commands. The HTTP adapter maps the same typed operations to Gmail, Calendar, Drive, Tasks, Docs, Sheets, and Slides REST origins with `pinnedFetch`; `gws_file_get` accepts only an item ID and uses fixed `Drive files.get?alt=media`, returning only scanned artifact/path metadata. Its fixed allowlist excludes model-supplied URLs. Docs/Sheets/Slides updates accept only replace/insert text or string-matrix values, never a model-provided batch request array. Discovery `schema` and `auth`/`login`/`setup` are forbidden from Prism argv.
25
25
 
26
26
  Drive **knowledge synchronization** (RAG import of file text + host-mapped ACL via `changes.list`) is not this CLI adapter. Use `createGoogleDriveConnector` / `syncKnowledge` from `@arnilo/prism-memory/rag` — see [Knowledge synchronization](knowledge-sync.md).
27
27
 
28
28
  ## Scoped OAuth establishment (0.0.14)
29
29
 
30
- Hosts establish, refresh, and revoke scoped OAuth credentials for these workloads through the existing `OAuthProvider` / credential-store seams (`@arnilo/prism-core/credentials/node`): `createMicrosoft365OAuthProvider` / `createGoogleWorkspaceOAuthProvider` (PKCE + device code), least-privilege scope bundles per capability (`resolveMicrosoft365Scopes` / `resolveGoogleWorkspaceScopes`, read vs mutation). Connectors consume a per-identity token via a late-bound `tokenProvider` injected as an env var — never argv, never model context; revocation fails closed. See [Credential storage](credential-storage.md) and [Work tools](work-tools.md).
30
+ Hosts establish, refresh, and revoke scoped OAuth credentials for these workloads through the existing `OAuthProvider` / credential-store seams (`@arnilo/prism-core/credentials/node`): `createMicrosoft365OAuthProvider` / `createGoogleWorkspaceOAuthProvider` (PKCE + device code), least-privilege scope bundles per capability (`resolveMicrosoft365Scopes` / `resolveGoogleWorkspaceScopes`, read vs mutation). Connectors consume a per-identity token via a late-bound `tokenProvider`: CLI adapters inject it into env and HTTP adapters send it only as `Authorization` — never argv or model context; revocation fails closed. See [Credential storage](credential-storage.md) and [Work tools](work-tools.md).
31
31
 
32
32
  ## Out of scope
33
33