@arnilo/prism 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +79 -5
- package/README.md +12 -11
- package/dist/agent-approval.d.ts +4 -0
- package/dist/agent-approval.js +5 -1
- package/dist/agent-definitions.js +1 -0
- package/dist/agent-run-lifecycle.js +39 -4
- package/dist/agent-run-state.d.ts +18 -0
- package/dist/agent-run-state.js +39 -9
- package/dist/agent-session/helpers.js +6 -1
- package/dist/agent-session/session/assemble.js +159 -7
- package/dist/agent-session/session/persist.d.ts +16 -0
- package/dist/agent-session/session/persist.js +64 -4
- package/dist/agent-session/session/provider-round.d.ts +3 -3
- package/dist/agent-session/session/provider-round.js +12 -6
- package/dist/agent-session/session/tool-round.js +5 -1
- package/dist/agent-session/session/types.d.ts +22 -1
- package/dist/agent-session/session.d.ts +16 -0
- package/dist/agent-session/session.js +42 -3
- package/dist/artifacts.d.ts +39 -1
- package/dist/artifacts.js +73 -0
- package/dist/attention-compiler.d.ts +121 -0
- package/dist/attention-compiler.js +479 -0
- package/dist/checkpoints.js +7 -11
- package/dist/cli-init.js +20 -6
- package/dist/context-budget.d.ts +20 -1
- package/dist/context-budget.js +10 -1
- package/dist/contracts-core/agent.d.ts +7 -0
- package/dist/contracts-core/attention.d.ts +66 -0
- package/dist/contracts-core/attention.js +2 -0
- package/dist/contracts-core/compaction.d.ts +59 -0
- package/dist/contracts-core/compaction.js +77 -1
- package/dist/contracts-core/content.d.ts +5 -0
- package/dist/contracts-core/loop.d.ts +42 -0
- package/dist/contracts-core/provider.d.ts +4 -0
- package/dist/contracts-core/run-limits.d.ts +2 -0
- package/dist/contracts-core.d.ts +1 -0
- package/dist/contracts-core.js +1 -0
- package/dist/contracts-protocol.d.ts +44 -3
- package/dist/contracts-run-state.d.ts +32 -5
- package/dist/evidence-grounding.d.ts +29 -0
- package/dist/evidence-grounding.js +162 -0
- package/dist/host-composition.d.ts +91 -0
- package/dist/host-composition.js +279 -0
- package/dist/index.d.ts +13 -6
- package/dist/index.js +7 -4
- package/dist/input.d.ts +13 -1
- package/dist/input.js +40 -1
- package/dist/provider-events.d.ts +3 -1
- package/dist/provider-events.js +2 -2
- package/dist/providers/transport.d.ts +3 -1
- package/dist/providers/transport.js +36 -0
- package/dist/redaction.js +18 -2
- package/dist/run-bundle.d.ts +89 -0
- package/dist/run-bundle.js +149 -0
- package/dist/secure-agent.d.ts +2 -0
- package/dist/secure-agent.js +6 -1
- package/dist/testing/state-concurrency-conformance.js +5 -12
- package/dist/tool-result-fold.d.ts +12 -0
- package/dist/tool-result-fold.js +13 -6
- package/dist/tools.d.ts +10 -0
- package/dist/tools.js +41 -0
- package/docs/acp-agent.md +42 -11
- package/docs/acp.md +2 -1
- package/docs/ag-ui.md +10 -3
- package/docs/agent-definitions.md +9 -1
- package/docs/agent-events.md +4 -1
- package/docs/agent-loops.md +33 -0
- package/docs/agent-session-runtime.md +8 -7
- package/docs/attention-compiler.md +272 -0
- package/docs/cli-rpc.md +4 -2
- package/docs/coding-agent-tools.md +1 -1
- package/docs/coding-security.md +6 -3
- package/docs/coding-tools.md +0 -1
- package/docs/coding-workspaces.md +22 -0
- package/docs/compaction-and-retry.md +36 -4
- package/docs/compaction-observational-memory.md +63 -10
- package/docs/connected-apps.md +116 -0
- package/docs/context-and-skills.md +17 -2
- package/docs/conversations.md +1 -1
- package/docs/core.md +1 -1
- package/docs/dev-inspector.md +4 -0
- package/docs/device-adapters.md +1 -0
- package/docs/diagrams.md +6 -6
- package/docs/document-reader.md +18 -10
- package/docs/documents.md +40 -11
- package/docs/durable-runs.md +87 -0
- package/docs/enterprise-postgres-state.md +6 -2
- package/docs/evaluations.md +168 -4
- package/docs/execution-timeline.md +186 -0
- package/docs/guardrails.md +33 -0
- package/docs/history/0.7.0-primitive-review.md +254 -0
- package/docs/history/079-messaging-primitive-review.md +391 -0
- package/docs/history/080-messaging-followon-primitive-review.md +234 -0
- package/docs/history/081-connected-apps-primitive-review.md +74 -0
- package/docs/history/083-prism-work-primitive-review.md +84 -0
- package/docs/history/084-primitive-review.md +96 -0
- package/docs/history/085-honesty-and-cut-primitive-review.md +91 -0
- package/docs/history/README.md +5 -0
- package/docs/history/migration-0.0.md +2 -2
- package/docs/history/release-handoffs.md +75 -1
- package/docs/host-compositions.md +149 -0
- package/docs/host-security.md +2 -2
- package/docs/hosted-sandboxes.md +94 -0
- package/docs/index.md +82 -45
- package/docs/input-and-prompt-assembly.md +1 -0
- package/docs/knowledge-sync.md +84 -0
- package/docs/language-intelligence.md +1 -1
- package/docs/live-testing.md +8 -3
- package/docs/mcp-tools.md +3 -1
- package/docs/memory-fabric.md +416 -0
- package/docs/messaging-channel-operations.md +166 -0
- package/docs/messaging-channels.md +150 -0
- package/docs/migrate-to-0.5.md +1 -1
- package/docs/migrate-to-0.6.md +1 -0
- package/docs/migrate-to-0.7.md +345 -0
- package/docs/migrate-to-0.8.md +124 -0
- package/docs/migration.md +43 -1
- package/docs/model-registry.md +12 -2
- package/docs/model-routing.md +79 -4
- package/docs/multi-agent-patterns.md +20 -6
- package/docs/observability.md +52 -1
- package/docs/openapi-tools.md +1 -1
- package/docs/operations.md +14 -4
- package/docs/options-index.md +47 -3
- package/docs/peer-dependencies.md +12 -10
- package/docs/postgres-persistence.md +1 -1
- package/docs/process-sessions.md +3 -1
- package/docs/prompt-registry.md +1 -1
- package/docs/provider-caching.md +4 -2
- package/docs/provider-conformance.md +1 -1
- package/docs/provider-layer.md +2 -2
- package/docs/provider-packages.md +22 -22
- package/docs/providers/bedrock.md +71 -7
- package/docs/providers/neuralwatt.md +5 -1
- package/docs/providers/openai.md +1 -1
- package/docs/rag.md +24 -8
- package/docs/realtime-voice.md +87 -0
- package/docs/release-and-install.md +53 -45
- package/docs/run-bundle.md +92 -0
- package/docs/runs-and-usage.md +17 -2
- package/docs/server.md +7 -3
- package/docs/sheets.md +9 -9
- package/docs/signal-channel.md +112 -0
- package/docs/speech.md +7 -1
- package/docs/sqlite-persistence.md +1 -1
- package/docs/supervisors.md +33 -5
- package/docs/telegram-channel.md +157 -0
- package/docs/testing.md +2 -2
- package/docs/thinking-and-reasoning.md +3 -1
- package/docs/tools.md +6 -5
- package/docs/web-tools.md +2 -1
- package/docs/wiki.md +1 -1
- package/docs/work-artifacts-and-review.md +14 -4
- package/docs/work-connectors.md +12 -10
- package/docs/work-sandbox.md +115 -0
- package/docs/work-tools.md +50 -18
- package/docs/workflows.md +69 -1
- package/docs/working-and-semantic-memory.md +25 -14
- package/package.json +5 -3
- package/templates/README.md +2 -0
- package/templates/business-worker/README.md.tmpl +19 -0
- package/templates/business-worker/env.example.tmpl +1 -0
- package/templates/business-worker/gitignore.tmpl +11 -0
- package/templates/business-worker/manifest.json +12 -0
- package/templates/business-worker/package.json.tmpl +23 -0
- package/templates/business-worker/src/agent.ts.tmpl +92 -0
- package/templates/business-worker/src/index.ts.tmpl +13 -0
- package/templates/business-worker/src/tests/agent.test.ts.tmpl +77 -0
- package/templates/business-worker/tsconfig.json.tmpl +15 -0
- package/templates/personal-assistant/README.md.tmpl +18 -0
- package/templates/personal-assistant/env.example.tmpl +1 -0
- package/templates/personal-assistant/gitignore.tmpl +11 -0
- package/templates/personal-assistant/manifest.json +11 -0
- package/templates/personal-assistant/package.json.tmpl +23 -0
- package/templates/personal-assistant/src/agent.ts.tmpl +65 -0
- package/templates/personal-assistant/src/index.ts.tmpl +13 -0
- package/templates/personal-assistant/src/tests/agent.test.ts.tmpl +28 -0
- package/templates/personal-assistant/tsconfig.json.tmpl +15 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# Messaging channel operations
|
|
2
|
+
|
|
3
|
+
## What it does
|
|
4
|
+
|
|
5
|
+
Operational contract for the `@arnilo/prism-channels` durable journal: what is persisted, in what order, what happens across restarts, and how an operator resolves work that cannot be proven. Read this with [Messaging channels](messaging-channels.md) (authorization and execution semantics) and [Agent session runtime](agent-session-runtime.md).
|
|
6
|
+
|
|
7
|
+
Everything here is a consumer of the generic `CheckpointStore`/`LeaseStore` contracts — no channel-specific SQL schema, no background work, no automatic replay.
|
|
8
|
+
|
|
9
|
+
## When to use it
|
|
10
|
+
|
|
11
|
+
Use when a host needs restart-safe bindings, admission dedup, claims and staged replies. Without `checkpoints` the runtime is single-process and restart-unsafe.
|
|
12
|
+
|
|
13
|
+
## Implementation example
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { createMessagingRuntime } from "@arnilo/prism-channels";
|
|
17
|
+
import { createSqlitePersistence } from "@arnilo/prism-core/sessions/sqlite";
|
|
18
|
+
|
|
19
|
+
const persistence = createSqlitePersistence({ filename: "./channels.sqlite" });
|
|
20
|
+
|
|
21
|
+
const runtime = createMessagingRuntime({
|
|
22
|
+
authorize,
|
|
23
|
+
resolveAgent,
|
|
24
|
+
deliver,
|
|
25
|
+
checkpoints: persistence.checkpoints,
|
|
26
|
+
leases: persistence.leases, // optional: fencing when more than one worker can see the same binding
|
|
27
|
+
limits: { retentionDays: 7 },
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
With `checkpoints`, a restart re-reads bindings, generations, dedup records and staged replies from the store; nothing is replayed on startup.
|
|
32
|
+
|
|
33
|
+
## Inputs / request
|
|
34
|
+
|
|
35
|
+
Records live in versioned namespaces (`prism.channels.v1.*`) with a `category` for host-side filtering. Keys embed the connection and the ownership scope (`tenant`, `account`, `user`), so a lookup in another scope misses rather than colliding, and the record carries the same scope so the store itself refuses a mismatched write.
|
|
36
|
+
|
|
37
|
+
| Namespace | Record | Contents |
|
|
38
|
+
| --- | --- | --- |
|
|
39
|
+
| `prism.channels.v1.binding` | `ChannelBindingRecord` | session id, branch leaf, alias, `/new` generation, suspended marker and run/operation correlation |
|
|
40
|
+
| `prism.channels.v1.operation` | `ChannelOperationRecord` | one admitted event: kind, external ids, state, run correlation (`runId`), reply flag, attempts |
|
|
41
|
+
| `prism.channels.v1.cursor` | `ChannelCursorRecord` | per-connection admitted count and last event id (ordering evidence only) |
|
|
42
|
+
| `prism.channels.v1.reply` | `ChannelReplyRecord` | the staged reply text (the only record with payload), delivery state, attempts, message id |
|
|
43
|
+
| `prism.channels.v1.control` | pairing or approval record | SHA-256 token hash, bound ids; approval records also bind principal, agent revision, session/run, pending-decision id, checkpoint version, outcome, expiry and consumed marker |
|
|
44
|
+
|
|
45
|
+
Operation keys use the deterministic operation id (`chan-op-<sha256 prefix>` of connection + event id), so admission dedup is a create-only CAS: the second writer loses and is acknowledged as a duplicate.
|
|
46
|
+
|
|
47
|
+
## Outputs / response / events
|
|
48
|
+
|
|
49
|
+
The store exposes single-record compare-and-swap only, so the order is explicit and each step is bounded:
|
|
50
|
+
|
|
51
|
+
1. **Authorize** the event against the host grant (no record yet; denial leaves nothing behind).
|
|
52
|
+
2. **Operation record** created as `accepted` (create-only CAS). This is the durable dedup and the transport acknowledgment point.
|
|
53
|
+
3. **Cursor advance** as ordering evidence, best-effort after the operation is committed.
|
|
54
|
+
4. **Claim**: `accepted` → `executing` by CAS (with the binding lease fence when `leases` is configured) *before* any provider or tool call.
|
|
55
|
+
5. **Run** the bound `AgentSession` turn with the verified identity. An event whose attachments cannot become text or model input settles `failed` with `errorCode: "unsupported_media"` and a bounded notice; declared attachment bytes over the channel cap are denied `oversized` before a record is claimed, and an oversize body is never buffered by the adapter.
|
|
56
|
+
6. **Operation terminal state** (`succeeded`/`failed`/`cancelled`/`suspended`) with run correlation, then the binding (leaf, suspended marker) is updated.
|
|
57
|
+
7. **Reply staged** as `pending`, then `deliver`, then `delivered` / `delivery_failed` / `delivery_unknown`.
|
|
58
|
+
|
|
59
|
+
Failure semantics by state:
|
|
60
|
+
|
|
61
|
+
| State | Meaning | Automatic action |
|
|
62
|
+
| --- | --- | --- |
|
|
63
|
+
| `accepted` | Journaled, never claimed | None. A crash here is a dead-letter; the user resends |
|
|
64
|
+
| `executing` | Claimed; provider work may have happened | None. Never replayed |
|
|
65
|
+
| `succeeded` / `failed` / `cancelled` | Terminal run outcome (current-run text only is ever sent) | None |
|
|
66
|
+
| `suspended` | Awaiting a core pending decision; blocks further ordinary turns on the binding | A short-lived server-issued allow/deny control resumes one decision, or `/cancel` submits core terminal denial; `/new` cannot bypass it |
|
|
67
|
+
| `execution_unknown` | A reconciling host filed unprovable claimed work | None; it is a dead-letter record |
|
|
68
|
+
| `abandoned` | Reconciled with no effect applied | None |
|
|
69
|
+
| reply `pending` / `delivery_unknown` | The send may or may not have landed | None. Only `reconcile` with an explicit acknowledgment |
|
|
70
|
+
| reply `delivery_failed` | Known transport failure (no ambiguity) | None. `reconcile` may resend without the acknowledgment |
|
|
71
|
+
|
|
72
|
+
A host notice (`runtime.notify`) is the same journal with no provider step: authorize `action: "notify"` → resolve the one bound pair → operation record of kind `notify` (create-only CAS on the caller `notifyId`, no cursor move) → stage the notice as `pending` → `deliver` → `delivered` / `delivery_failed` / `delivery_unknown` → `succeeded`. An unbound pair, another actor's binding, a foreign ownership scope, an ungranted/revoked identity, an unresolvable binding and capacity pressure are denied before any record exists; a repeated `notifyId` settles as a duplicate without a second delivery.
|
|
73
|
+
|
|
74
|
+
Two workers that see the same event cannot both run it: the loser's create-only CAS fails and it is acknowledged as a duplicate. A worker that loses the *binding lease* to another worker (different event, same binding) drops its own unclaimed record so the event stays retryable instead of dead-lettering silently.
|
|
75
|
+
|
|
76
|
+
## Request/response example
|
|
77
|
+
|
|
78
|
+
Nothing is replayed automatically — every entry point is an authorized call that derives ownership from a verified `AgentIdentity`:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
// What still needs attention: unresolved states, plus settled operations whose reply was never confirmed.
|
|
82
|
+
await runtime.listUnresolved({ identity, limit: 100 });
|
|
83
|
+
// → [{ operationId, connectionId, state, replyState?, version, updatedAt }]
|
|
84
|
+
|
|
85
|
+
await runtime.reconcile({
|
|
86
|
+
identity,
|
|
87
|
+
connectionId,
|
|
88
|
+
operationId,
|
|
89
|
+
expectedVersion, // from listUnresolved; a stale value is rejected
|
|
90
|
+
acknowledgeDuplicateRisk: true, // required when an effect may already have happened
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`reconcile` outcomes:
|
|
95
|
+
|
|
96
|
+
- `accepted` → `abandoned` (nothing ran; recorded as a dead-letter).
|
|
97
|
+
- `executing` → `execution_unknown`, **only** with `acknowledgeDuplicateRisk`; the model is never re-invoked.
|
|
98
|
+
- `pending` / `delivery_unknown` reply → resends the persisted text (acknowledgment required) and records `delivered` or `delivery_failed`; an ambiguous resend stays `delivery_unknown`.
|
|
99
|
+
- `delivery_failed` reply → resend without the acknowledgment, because the first attempt is known to have failed.
|
|
100
|
+
- Anything already settled → `outcome: "none"` (no second message).
|
|
101
|
+
|
|
102
|
+
Results are `resolved`, `not_found` (wrong scope or unknown id — never a foreign read), `conflict` (version mismatch or missing acknowledgment, with `detail: "duplicate_risk"`), `denied` (inactive identity) or `unavailable` (no store configured or the store failed).
|
|
103
|
+
|
|
104
|
+
There is no exactly-once claim. The journal guarantees that a recorded operation is not executed twice *by this runtime* and that a reply is never lost between the run and the send; it cannot retract a side effect the platform or a dispatched tool already accepted.
|
|
105
|
+
|
|
106
|
+
## Extension and configuration notes
|
|
107
|
+
|
|
108
|
+
### Leases and fencing
|
|
109
|
+
|
|
110
|
+
When `leases` is configured, a turn acquires a lease on its binding before the claim, renews it while work is queued on the route, passes the fencing token to journal writes, and releases it when the route goes idle or the runtime stops. If release fails, the runtime keeps its in-memory lease and retries on the next idle or stop path; it never treats the binding as free. `leaseTtlMs` remains the cross-process backstop.
|
|
111
|
+
|
|
112
|
+
The Telegram and experimental Signal adapters separately hold service-owned receiver leases. Signal uses `prism.channels.v1.signal.receiver` and writes a small readiness probe under that same service scope before `subscribeReceive`; it pauses/unsubscribes on writer or lease loss. It stores no inbound text/cursor because signal-cli manual notifications have no documented application acknowledgment or replay log.
|
|
113
|
+
|
|
114
|
+
- A lease held elsewhere defers the turn: it executes nothing and drops its unclaimed record (visible as `leaseLosses` in diagnostics).
|
|
115
|
+
- A lost or unrenewable lease fails closed: the claim is never written and the turn does not run.
|
|
116
|
+
- Fences only protect stored transitions — an already-issued network call is not retracted.
|
|
117
|
+
|
|
118
|
+
### Retention and cleanup
|
|
119
|
+
|
|
120
|
+
Retention is operator-driven; there is no background timer and no idle scan.
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
await runtime.prune({ identity }); // or { identity, now: "<iso>" } for a deterministic sweep
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`prune` walks one bounded page (`maxJournalPage`) per namespace in the caller's ownership scope and deletes records whose own last-transition stamp is older than `retentionDays`, except: unresolved operations (`accepted`/`executing`), unsettled replies (`pending`/`delivery_unknown`), and bindings marked suspended. Cursor and expired/consumed control records are pruned with the rest. Repeated calls make progress; the delete count is returned as `{ scanned, deleted, retained }`.
|
|
127
|
+
|
|
128
|
+
Staged reply text is the only payload kept, bounded by `maxJournalRecordBytes` and pruned after retention — do not treat the journal as an archive.
|
|
129
|
+
|
|
130
|
+
### PostgreSQL outbox composition
|
|
131
|
+
|
|
132
|
+
A host that already runs the enterprise PostgreSQL outbox can record the reply in the same transaction as its own business mutation instead of sending from inside a model turn: `deliver` opens a caller-owned `PoolClient`, `BEGIN` → `outbox.append(client, …)` → `COMMIT`, and only then calls the adapter's `send`. The row is the durable record — an external send cannot be rolled back — so a crash between commit and send leaves the host free to re-drive delivery from the outbox (`dispatcher.claim`/`acknowledge`) rather than from the channel journal, and a repeated `deliver` for the same reply appends nothing new because `append` is idempotent on `(tenant_id, message_id)`.
|
|
133
|
+
|
|
134
|
+
The payload holds correlation ids only (`connectionId`, `eventId`, `kind`), never prompt or reply text, and the tenant comes from the resolved identity's ownership scope. Nothing in `@arnilo/prism-channels` knows about PostgreSQL: the composition lives in the host. See [the example](../examples/messaging-outbox.ts) and [Enterprise PostgreSQL state](enterprise-postgres-state.md).
|
|
135
|
+
|
|
136
|
+
### Operating a deployment
|
|
137
|
+
|
|
138
|
+
- **Startup**: construct the persistence, construct the runtime, then `listUnresolved` + `reconcile` what deserves a decision. Nothing self-heals in the background.
|
|
139
|
+
- **Shutdown**: `await runtime.stop()` aborts active runs, marks queued work cancelled (durably) and releases leases within `stopDeadlineMs`; then close the persistence.
|
|
140
|
+
- **Rolling restart**: use `leases` whenever two workers can see the same binding (webhook replicas, polling + webhook overlap), otherwise serialization is per process only.
|
|
141
|
+
- **Revocation**: a revoked identity fails before provider work and before delivery; already-accepted platform effects are not retractable.
|
|
142
|
+
- **Observability**: `runtime.diagnostics()` reports bounded counters only — including `storageFailures` (journal refused or unavailable) and `leaseLosses` (deferred or fenced out). No message text, external ids, tokens or key material is ever logged.
|
|
143
|
+
|
|
144
|
+
## Security and performance notes
|
|
145
|
+
|
|
146
|
+
All journal paths are bounded by the channel limits in [Messaging channels](messaging-channels.md): `maxJournalPage` (list page, ≤ 500), `maxJournalRecordBytes` (per-record value, checked before save), `retentionDays`, `leaseTtlMs` and the input/response caps that decide what may enter or leave the journal. Durable admission is one create and (after claim) one update plus the reply records; the plan's p95 target for durable admission is < 100 ms on a reference SSD excluding network and model time. The sqlite host suite times duplicate admission (journal CAS only) and keeps a generous CI bound of 500 ms so slow runners do not flake; treat 100 ms as the operator target, not the CI gate.
|
|
147
|
+
|
|
148
|
+
Process crash injection uses `scripts/fixtures/messaging-restart-worker.mjs`: SIGKILL leaves an `executing` record for authorized `reconcile` (never a second model/tool run). `reconcile` reuses the stored checkpoint fencing token so a crashed lease holder can still be filed as `execution_unknown` without waiting out `leaseTtlMs`. Graceful shutdown is `await runtime.stop()` (host SIGTERM handler); two sqlite connections on one file defer to the binding-lease holder.
|
|
149
|
+
|
|
150
|
+
Graceful shutdown is verified end to end, not just asserted: `scripts/fixtures/messaging-restart-worker.mjs` claims a real operation on sqlite, installs a `SIGTERM` handler that awaits `runtime.stop()`, and the claimed record lands as `cancelled` — never left `executing` — when the host's own shutdown path runs. The same worker under `SIGKILL` is the contrasting case (record stays `executing` for `reconcile`), and both are asserted in the package suite.
|
|
151
|
+
|
|
152
|
+
Soaks stay an operator decision, but the runner ships: `scripts/fixtures/messaging-soak.mjs` loops admit (including repeated event ids that must dedup) → drain → `listUnresolved`/`reconcile` → `prune` against a throwaway sqlite file and a mock provider. No network, no chat accounts, no credentials.
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
node scripts/fixtures/messaging-soak.mjs --durationMs 3000 # CI smoke (bounded, seconds)
|
|
156
|
+
node scripts/fixtures/messaging-soak.mjs --durationMs 259200000 # 72h operator soak; SIGINT/SIGTERM stops it cleanly
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
It prints one JSON summary (iterations, admissions, duplicate dedup count, deliveries, prune counts, `diagnostics()`, `unresolvedAtEnd`) and exits non-zero if a turn was denied, left unresolved or unsettled, failed, or delivered twice. The loop yields to the event loop periodically, so Ctrl-C/`SIGTERM` ends a long soak promptly instead of after the deadline, and the final sweep prunes with an aged cutoff so even a three-second run exercises deletion. For a soak against real state, keep the loop and swap sqlite for the production store; do not create contacts or accounts to soak.
|
|
160
|
+
|
|
161
|
+
## Related APIs
|
|
162
|
+
|
|
163
|
+
- [Messaging channels](messaging-channels.md) — contracts, authorization, execution semantics and limits.
|
|
164
|
+
- [Signal channel (experimental)](signal-channel.md) — manual receive, policy gate and receive-to-commit loss window.
|
|
165
|
+
- [Database persistence](database-persistence.md) and [Session stores](session-stores.md) — the checkpoint/lease contracts and the SQLite/PostgreSQL adapters.
|
|
166
|
+
- [Package README](../packages/prism-channels/README.md) — package boundary and peer-only install.
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Messaging channels
|
|
2
|
+
|
|
3
|
+
## What it does
|
|
4
|
+
|
|
5
|
+
`@arnilo/prism-channels` provides transport-neutral contracts and the Prism execution adapter for chat transports. A host authorizes an observed sender, binds them to an owned Prism session, and the runtime runs ordinary turns through the existing `AgentSession` API and projects only the current run's final text back to the bound destination.
|
|
6
|
+
|
|
7
|
+
The root export ships the runtime, contracts, limits and durable journal. The official Telegram adapter ships as `@arnilo/prism-channels/telegram`; experimental Signal support ships as the isolated Node-only `@arnilo/prism-channels/signal` subpath. Durable single-decision approval controls ship now.
|
|
8
|
+
|
|
9
|
+
Pass `checkpoints` (and optionally `leases`) to make bindings, admission dedup, the claim before provider work and outgoing replies durable; that is the restart-safe deployment. Without a store the runtime keeps bounded in-process state only — still the correct execution/authorization seam, but restart-unsafe. Recovery mechanics, record layout and the operator runbook: [Messaging channel operations](messaging-channel-operations.md).
|
|
10
|
+
|
|
11
|
+
## When to use it
|
|
12
|
+
|
|
13
|
+
Use when a host already operates a chat bot (or plans to) and wants selected Prism agents reachable by chat message without building another agent runtime or conversation database. Telegram admits private DMs by default and group/topic text only when the adapter opts in with `allowGroups` and the host grants the exact chat/thread/sender; Signal (experimental) stays direct-message only. Not for media, voice or proactive broadcasting in the current scope.
|
|
14
|
+
|
|
15
|
+
## Inputs / request
|
|
16
|
+
|
|
17
|
+
Install the required `@arnilo/prism` peer plus this package. `createMessagingRuntime` takes `MessagingRuntimeOptions`: `authorize`, `resolveAgent`, `deliver`, optional `limits` (`ChannelLimits`), `redactor`, `checkpoints`, `leases`, and `resolveBinding`. Durable writers are also constructed with `ChannelStateStoreOptions`, `ChannelDeliveryJournalOptions`, and `ChannelPairingStoreOptions`.
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @arnilo/prism @arnilo/prism-channels
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Request/response example
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { createMessagingRuntime } from "@arnilo/prism-channels";
|
|
27
|
+
|
|
28
|
+
const runtime = createMessagingRuntime({
|
|
29
|
+
// Observed sender + action -> host-verified grant, or `false`.
|
|
30
|
+
authorize: ({ connectionId, externalActorId, action }) => hostGrants.lookup(connectionId, externalActorId, action),
|
|
31
|
+
// Authorized alias -> the host-configured agent for the current identity/ownership.
|
|
32
|
+
resolveAgent: ({ agentAlias, identity, ownership }) => hostAgents.resolve(agentAlias, identity, ownership),
|
|
33
|
+
// Delivery seam: normally the transport adapter's `send`, or a durable outbox.
|
|
34
|
+
deliver: (reply) => transport.send(reply),
|
|
35
|
+
// Optional preview seam: cumulative redacted assistant text while a turn runs (Telegram drafts).
|
|
36
|
+
onAssistantDelta: (delta) => transport.sendDraft?.(delta),
|
|
37
|
+
// Optional media seam: normally the adapter's own bounded `fetchAttachment`.
|
|
38
|
+
// An image becomes model input only when the resolved model declares image input.
|
|
39
|
+
fetchAttachment: (ref) => transport.fetchAttachment(ref),
|
|
40
|
+
limits: { maxPendingPerBinding: 4 },
|
|
41
|
+
redactor: hostRedactor,
|
|
42
|
+
// Durable journal (optional but required for restart safety): any CheckpointStore/LeaseStore,
|
|
43
|
+
// for example createMemoryCheckpointStore(), SQLite or PostgreSQL persistence.
|
|
44
|
+
checkpoints: hostCheckpoints,
|
|
45
|
+
leases: hostLeases,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// The adapter calls this before acknowledging transport receipt (Telegram offset / webhook 2xx).
|
|
49
|
+
const admission = await runtime.admit(event);
|
|
50
|
+
await runtime.drain({ deadlineMs: 5_000 }); // host shutdown / test settle
|
|
51
|
+
runtime.diagnostics(); // bounded counters, no text or external ids
|
|
52
|
+
await runtime.stop();
|
|
53
|
+
|
|
54
|
+
// Durable recovery, host-driven and never automatic:
|
|
55
|
+
await runtime.listUnresolved({ identity }); // bounded page: operations still needing attention
|
|
56
|
+
await runtime.reconcile({ identity, connectionId, operationId, expectedVersion, acknowledgeDuplicateRisk: true });
|
|
57
|
+
await runtime.prune({ identity }); // retention sweep; unresolved work is never deleted
|
|
58
|
+
|
|
59
|
+
// Opt-in host notice to one already-bound pair (no agent run, no tools, no queue):
|
|
60
|
+
// the grant must carry `notifications: true`, `notifyId` is the idempotency key.
|
|
61
|
+
await runtime.notify({
|
|
62
|
+
identity,
|
|
63
|
+
connectionId: "telegram-main",
|
|
64
|
+
externalConversationId: "8241",
|
|
65
|
+
notifyId: "job-123-done",
|
|
66
|
+
text: "The nightly job finished.",
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Outputs / response / events
|
|
71
|
+
|
|
72
|
+
`admit` authorizes first and returns `{ status: "accepted" | "denied" | "unsupported", reason?, operationId?, duplicate? }`. It resolves before any provider call and before the transport acknowledgment; accepted turns then run in the background.
|
|
73
|
+
|
|
74
|
+
`ChannelInboundEvent` carries only transport-observed fields: `connectionId`, `externalConversationId`, `externalActorId` (stable platform id, never a display name), `eventId` (the transport acknowledgment unit), `text`, optional `threadId`, `attachments`, `claims`, and an adapter-parsed opaque `approval` control. `claims` and transport ids are untrusted context for the host callback only — never authority.
|
|
75
|
+
|
|
76
|
+
`attachments` are bounded `ChannelAttachmentRef` values (`kind: "image" | "document" | "voice"`, transport file id, optional mime/size/name) — identifiers, never bytes and never model-selected. Voice and document become turn text in the adapter through host hooks (`transcribe`, `extractDocumentText`); only an `image` can become model input, and only when the resolved agent's model declares `image` input *and* the host wired `MessagingRuntimeOptions.fetchAttachment` (normally the adapter's `fetchAttachment`). Then the runtime fetches the current event's refs once, bounded by the channel cap, builds one image content block per ref next to the text, and stores nothing: an undeclared modality, a missing seam or a failed fetch produces `unsupported_media` with a bounded notice and no provider call. Declared attachment bytes above `maxAttachmentBytes` are denied (`reason: "oversized"`) before any fetch.
|
|
77
|
+
|
|
78
|
+
`MessagingRuntimeOptions.onAssistantDelta` is the optional outbound preview seam: while a turn runs, the runtime forwards the cumulative assistant text of the current message, already redacted with the reply `redactor`, as `ChannelAssistantDelta` (`connectionId`, `externalConversationId`, optional `threadId`, `text`). It fires once per streamed text chunk, only for turns that actually reach the provider, stops at cancel or `stop()`, and resets per assistant message so a tool-call preamble never leaks into the following answer. It is not a reply: nothing is journaled, delivery is not tracked, a throwing callback is ignored, and the terminal `ChannelReply` is unchanged. An adapter that has no preview capability simply ignores it.
|
|
79
|
+
|
|
80
|
+
`MessagingRuntime.notify` is the opt-in reverse direction: the host sends a `notice` (never a `final`) to one already-bound conversation/thread pair, without a model run, a tool call or a queue slot. It takes `identity`, the bound destination and a caller `notifyId`; the grant must be re-checked with `action: "notify"` and carry `notifications: true`, and `notifyId` is the idempotency key (`duplicate: true` on repeat). The notice is redacted, bounded by the reply cap, journaled as an operation of kind `notify` and staged through the same reply journal as an answer — so a crash between the answer and the send never turns into a silent notice and never re-runs anything. There is no broadcast or fan-out input: an unbound pair, another actor's binding, a foreign ownership scope, an ungranted or revoked identity and an unresolvable binding all fail closed with a bounded `reason`.
|
|
81
|
+
|
|
82
|
+
## Implementation example
|
|
83
|
+
|
|
84
|
+
Network-free host composition: [`examples/messaging-agent.ts`](../examples/messaging-agent.ts). Telegram: [`examples/telegram-agent.ts`](../examples/telegram-agent.ts). Experimental Signal: [`examples/signal-agent.ts`](../examples/signal-agent.ts).
|
|
85
|
+
|
|
86
|
+
## Security and performance notes
|
|
87
|
+
|
|
88
|
+
### Authorization
|
|
89
|
+
|
|
90
|
+
- `authorize` receives the bounded text and action (`"message"`, `"command"`, or `"approval"`) and must return a host-verified `AgentIdentity` (`verified: true`, non-empty scopes, unexpired, unrevoked) plus permitted `agentAliases` and a `grantRevision`. Anything else fails closed with a bounded `reason` and no provider call.
|
|
91
|
+
- Ownership is always derived with `ownershipFromIdentity`; the event, message text and `claims` cannot set or widen it. The run receives the verified `identity` and the runtime never passes a `per-run ownership` override, so `createSecureAgent` defaults cannot be replaced by a channel adapter.
|
|
92
|
+
- Every turn re-checks the grant and `assertIdentityActive` before provider work and before delivery: a revoked identity neither runs nor sends, and no reply is sent after revocation.
|
|
93
|
+
- Unknown aliases, malformed ids, oversized text, ungranted senders and capacity pressure are denied without provider or tool calls. Cross-tenant errors stay bounded and never reveal whether a foreign session exists.
|
|
94
|
+
- **Media boundary.** Attachment refs are validated (count, ids, metadata, safe-integer sizes) and malformed refs are denied `malformed`; sizes above `maxAttachmentBytes` are denied `oversized` before a fetch. The runtime fetches only refs of the event it is running and passes bytes straight into one run call — the same address the model cannot influence. Nothing media-shaped is written to the journal or the delivery seam: operation and reply records stay text-only, and an attachment the host did not opt into produces a bounded notice rather than a prompt-injection surface.
|
|
95
|
+
|
|
96
|
+
### Execution semantics
|
|
97
|
+
|
|
98
|
+
- **Binding.** Connection + conversation + thread + actor + alias maps to a deterministic owned session id (`chan-<sha256 prefix>`); the host may supply `resolveBinding` to persist its own mapping. Same display name on another connection or tenant is a different session; `/new` starts a fresh session for the current binding. With `checkpoints`, the binding — including the `/new` generation, the branch leaf and a suspended marker — is journaled and reused after a restart.
|
|
99
|
+
- **Serialization.** Turns are serialized per logical session id and bounded per binding, per process and by active-session concurrency. Core sessions do not serialize two runtime objects that share an id, so a channel-only mutex is not shared-session arbitration: if another API can drive the same session, use a shared host admission/lease mechanism instead.
|
|
100
|
+
- **Host notices.** `notify` never allocates a binding or a route and never touches the turn queue; it requires an existing binding and re-checks identity, ownership, the `notifications: true` grant flag and the bound alias on every call. When the host has more than one binding for the same pair (the conversation moved alias), the current selection is addressed and anything ambiguous fails closed. Notices count against `maxPendingPerProcess` and reuse the staged-reply path, so a delivery failure is recorded rather than retried.
|
|
101
|
+
- **Output.** Only the current run's terminal `succeeded` text is delivered. Failed, aborted or suspended runs send a bounded notice (redacted), never a previous turn's text and never partial deltas. Responses are truncated to the channel response cap.
|
|
102
|
+
- **Cancellation.** `/cancel` aborts an active run via its `AbortSignal` and marks queued turns cancelled. For a suspended durable run it submits core's CAS-checked terminal denial; an already dispatched tool is never rolled back. Transport disconnection is not user cancellation; `stop()` marks queued turns cancelled and aborts active runs on host shutdown.
|
|
103
|
+
- **Suspension and approval.** A suspended tool decision blocks ordinary turns (`reason: "awaiting_decision"`) and `/new` cannot bypass it. The runtime persists two short-lived opaque controls for the first pending tool decision: `allow_once` and `reject_once`. Each is bound to exact principal, connection, conversation/thread, agent alias/revision, session, run, approval id and checkpoint version. It is consumed atomically before resume; stale, expired, forwarded, replayed or revoked controls fail closed. Resume reauthorizes the sender and resolves current agent policy before core dispatches the decision. Subsequent pending decisions re-suspend and issue fresh controls. Unsupported elicitation stays blocked for authenticated host handling.
|
|
104
|
+
- **Controls.** `/help`, `/status`, `/new`, `/agent <alias>` and `/cancel` are the only ordinary commands. `/status` reports only the bound operation state; controls bypass the prompt queue. The experimental Signal adapter maps `/approve <token>` and `/deny <token>` to the same opaque records. Any other slash input (including registry/config mutation) never reaches a model.
|
|
105
|
+
- **Durable intake.** With `checkpoints`, every authorized event is recorded before it is acknowledged, deduplicated by connection + event id across processes, and moved `accepted` → `executing` by a compare-and-swap claim *before* the provider is called. A duplicate event is acknowledged with `duplicate: true` and never runs twice; a denial leaves no record behind, so a legitimate retry still works.
|
|
106
|
+
- **Durable replies.** A reply is persisted before `deliver` runs, so a crash between the answer and the send never loses it and never reruns the model. An ambiguous send (`deliver` threw) is recorded as `delivery_unknown` and is never resent automatically; only an authorized `reconcile` call with an explicit `acknowledgeDuplicateRisk` resends the persisted text.
|
|
107
|
+
- **Crash truth.** A claimed operation whose process died stays unresolved and is never replayed; `reconcile` files it as `execution_unknown`, which is a dead-letter for host review, not a retry. There is no exactly-once claim: side effects already accepted by the platform (or already dispatched tools) are not retractable.
|
|
108
|
+
|
|
109
|
+
## Extension and configuration notes
|
|
110
|
+
|
|
111
|
+
`resolveChannelLimits` rejects `ChannelLimits` values outside `[1, hard cap]`.
|
|
112
|
+
|
|
113
|
+
| Limit | Default | Hard cap |
|
|
114
|
+
| --- | --- | --- |
|
|
115
|
+
| `maxInputBytes` | 32 KiB | 64 KiB |
|
|
116
|
+
| `maxResponseBytes` | 64 KiB | 128 KiB |
|
|
117
|
+
| `maxPendingPerBinding` | 8 | 32 |
|
|
118
|
+
| `maxPendingPerProcess` | 100 | 500 |
|
|
119
|
+
| `maxActiveSessions` | 4 | 16 |
|
|
120
|
+
| `maxRoutes` | 1000 | 10000 |
|
|
121
|
+
| `maxSeenEventsPerConnection` | 1000 | 10000 |
|
|
122
|
+
| `stopDeadlineMs` | 30 s | 5 min |
|
|
123
|
+
| `retentionDays` | 7 | 90 |
|
|
124
|
+
| `maxJournalPage` | 100 | 500 |
|
|
125
|
+
| `maxJournalRecordBytes` | 128 KiB | 512 KiB |
|
|
126
|
+
| `leaseTtlMs` | 30 s | 5 min |
|
|
127
|
+
| `approvalTtlMs` | 5 min | 5 min |
|
|
128
|
+
| `maxAttachmentBytes` | 1 MiB | 4 MiB |
|
|
129
|
+
|
|
130
|
+
Capacity pressure fails admission (`reason: "capacity"`) instead of evicting accepted work, and an unusable journal fails admission with `reason: "unavailable"` rather than running unclaimed work. Without `checkpoints` the in-process seen-event set is bounded and is not a durable deduplication guarantee.
|
|
131
|
+
|
|
132
|
+
## Telegram
|
|
133
|
+
|
|
134
|
+
Use [`@arnilo/prism-channels/telegram`](telegram-channel.md) for explicit long polling or a separately mounted webhook handler. Both require a service-owned receiver lease; polling also persists its offset after `admit` settles. The adapter uses only native `fetch`, resolves the bot token at each transport request, never follows redirects, sends plain text in Unicode-safe chunks, and records timeout/network sends as ambiguous so the durable reply journal never blindly resends them. Private DMs are the default; `allowGroups: true` adds `group`/`supergroup` text (forum topics keep their topic id as `threadId`) and only ever hands observed `(chat, thread?, sender)` tuples to the host `authorize` — membership is never an identity.
|
|
135
|
+
|
|
136
|
+
## Signal (experimental)
|
|
137
|
+
|
|
138
|
+
Use [`@arnilo/prism-channels/signal`](signal-channel.md) only with an externally supervised, pinned signal-cli v0.14.8 private Unix socket and an explicit operator acceptable-use/GPL policy attestation. It admits only direct UUID text envelopes after durable writer readiness, pauses on account/storage/lease trouble, sends only to the authorized bound UUID, and does not claim lossless receive or end-to-end encryption through Prism/model providers. Group envelopes are always dropped (no Signal group or topic support).
|
|
139
|
+
|
|
140
|
+
A network-free host composition is `examples/messaging-agent.ts` (sqlite journal, mock agent, `drain`/`stop`). Telegram polling/webhook helpers live in `examples/telegram-agent.ts`; the experimental Signal helper is `examples/signal-agent.ts`. Opt-in live probes skip without credentials — see [Live and end-to-end testing](live-testing.md).
|
|
141
|
+
|
|
142
|
+
## Related APIs
|
|
143
|
+
|
|
144
|
+
- [Telegram channel](telegram-channel.md) — polling, webhook mounting, credentials, limits and failure behavior.
|
|
145
|
+
- [Signal channel (experimental)](signal-channel.md) — daemon prerequisites, policy gate, manual subscription and reliability limits.
|
|
146
|
+
- [Messaging channel operations](messaging-channel-operations.md) — durable records, recovery, reconciliation and the operator runbook.
|
|
147
|
+
- [Agent session runtime](agent-session-runtime.md) — `AgentSession.run`, results, interruption and resume.
|
|
148
|
+
- [Agent identity](agent-identity.md) — `AgentIdentity`, `IdentityVerifier`, ownership projection.
|
|
149
|
+
- [Host security](host-security.md) — host-owned authority and side-effect policy.
|
|
150
|
+
- [Package README](../packages/prism-channels/README.md) — package boundary and peer-only install.
|
package/docs/migrate-to-0.5.md
CHANGED
|
@@ -120,7 +120,7 @@ Additive surface:
|
|
|
120
120
|
|
|
121
121
|
What to do:
|
|
122
122
|
|
|
123
|
-
1. You can drop `createSessionCachePolicy()` from
|
|
123
|
+
1. You can drop `createSessionCachePolicy()` from a host agent config if it existed only to inject session correlation.
|
|
124
124
|
2. Prefer `createAgent({ thinkingLevel: "low" })` over hand-merging `providerOptions.compat`.
|
|
125
125
|
3. Raw `provider.generate()` to OpenCode Go without `options.sessionId` now fails closed with `ProviderRequirementError` instead of HTTP 400.
|
|
126
126
|
4. Custom generate sites should call `applyDefaultProviderRequestOptions(request, { sessionId, thinkingLevel })`.
|
package/docs/migrate-to-0.6.md
CHANGED
|
@@ -83,6 +83,7 @@ Pin the previous published line: `@arnilo/prism@0.5.6` (exact pins per package).
|
|
|
83
83
|
## Related APIs
|
|
84
84
|
|
|
85
85
|
- [Migration guide](migration.md): the era index of migration cuts with replacement tables and rollback notes.
|
|
86
|
+
- [Migrate Prism 0.6 to 0.7](migrate-to-0.7.md): the next line's guide (extended line: plans 072–075, 077–079).
|
|
86
87
|
- [Migrate Prism 0.4 to 0.5](migrate-to-0.5.md): the previous line's guide (plans 055–067).
|
|
87
88
|
- [Release and install](release-and-install.md): packed surfaces, install rules, support matrix, and the offline test budget.
|
|
88
89
|
- [Peer dependencies](peer-dependencies.md): every third-party peer declaration with range, optionality, subpath, and install line.
|