@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,391 @@
|
|
|
1
|
+
# Plan 079 Task 1 — Messaging Channels: Primitive, Compatibility, and Threat-Model Review
|
|
2
|
+
|
|
3
|
+
Plan: [079-Prism-Messaging-Channels-Telegram-Signal.md](../../plans/079-Prism-Messaging-Channels-Telegram-Signal.md) Task 1
|
|
4
|
+
Baseline reviewed: working tree at `0.7.0` (2026-09-15). The plan text still names the 0.6.0 planning baseline; every source span below was re-verified against the 0.7.0 revision.
|
|
5
|
+
Target: Release `0.8.0` (channels moved off the 0.7.0 line; 0.7.0 shipped without them).
|
|
6
|
+
Date: 2026-09-15
|
|
7
|
+
Boundary update (2026-09-16, user-directed): Task 4 moved the implementation from `@arnilo/prism-core/integrations/channels` to `@arnilo/prism-channels`. The frozen symbol names, options and semantics recorded below still hold; the package specifier does not. The seven primitive probes stay in `prism-core`, because they examine core primitives rather than channels. Everywhere below read `@arnilo/prism-core/integrations/channels` as the pre-Task-4 location.
|
|
8
|
+
|
|
9
|
+
Executable evidence: [`packages/prism-core/src/__tests__/messaging-primitives.test.ts`](../../packages/prism-core/src/__tests__/messaging-primitives.test.ts) — 7 tests, run with `npm run build --workspace @arnilo/prism-core && node --test "packages/prism-core/dist/__tests__/messaging-primitives.test.js"`.
|
|
10
|
+
|
|
11
|
+
This is a frozen review record (history). Current-contract documentation for the implemented
|
|
12
|
+
subpaths belongs in `docs/` from Task 2 onward.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 1. Executive summary
|
|
17
|
+
|
|
18
|
+
The review proves the messaging-channel runtime can be built entirely on existing Prism primitives:
|
|
19
|
+
`Agent`/`AgentSession` execution, `AgentRunResult`, durable run state, `AgentRunLifecycle`
|
|
20
|
+
resume, `AgentIdentity`/`OwnershipScope`, `CheckpointStore`/`LeaseStore`, and
|
|
21
|
+
`SecretRedactor`. No second agent engine, conversation store, npm package, provider, or
|
|
22
|
+
vendor branch in the core loop is needed.
|
|
23
|
+
|
|
24
|
+
Six compatibility gates were resolved with executable proofs, and four real limitations were
|
|
25
|
+
found that the channel runtime must absorb rather than paper over:
|
|
26
|
+
|
|
27
|
+
1. `ConversationService.continue()` cannot compose with a secure agent — it always forces
|
|
28
|
+
`ownership` + `redactor` run overrides, which `executeRun` rejects for secure agents.
|
|
29
|
+
**Default execution path is therefore a direct `AgentSession`; conversation-service
|
|
30
|
+
composition is out of scope unless a generic fix is separately authorized.**
|
|
31
|
+
2. `RunOptions.idempotencyKey` is append deduplication, not execution deduplication. It
|
|
32
|
+
only collapses an exact retry at the same branch parent; at a new tip the provider runs
|
|
33
|
+
again. **Channel admission must claim the external operation (checkpoint CAS) before
|
|
34
|
+
calling `session.run`.**
|
|
35
|
+
3. `createAgentRunLifecycle.resume()` accepts a `signal` but only pre-checks it; unlike
|
|
36
|
+
`resumeStream()` it never threads cancellation into `resumeAgentRun`. **Channel resume
|
|
37
|
+
uses `resumeStream`**, or Task 2 may add the one-line generic signal pass-through.
|
|
38
|
+
4. `buildRunResult()` projects the last assistant message in *history*, so failed runs can
|
|
39
|
+
carry a previous turn's text and aborted/suspended runs carry empty or partial text.
|
|
40
|
+
**The channel sends only the current operation's terminal output, never `result.text`
|
|
41
|
+
for non-success statuses.**
|
|
42
|
+
|
|
43
|
+
Durable-state fit: the generic checkpoint/lease contracts are sufficient, but store
|
|
44
|
+
semantics impose three hard design rules — keys must embed tenant/user (ownership mismatch
|
|
45
|
+
throws, it does not miss), list pagination is OFFSET-based (unstable under concurrent
|
|
46
|
+
writes), and there is no multi-record transaction. Signal's policy/license gate resolves to
|
|
47
|
+
"experimental, opt-in, no GPL contagion from a socket client, acceptable-use decision
|
|
48
|
+
recorded by the operator before any supported claim".
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## 2. Verified primitive inventory (0.7.0 revision)
|
|
53
|
+
|
|
54
|
+
| Primitive | Verified evidence | Reuse decision | Limitation the channel must absorb |
|
|
55
|
+
| --- | --- | --- | --- |
|
|
56
|
+
| `Agent`, `AgentConfig`, `AgentSessionConfig` | `src/agent-session/create-agent.ts:5-12`; `src/contracts-core/agent.ts:116-155` | Host resolves the agent; channel creates sessions by `{ id, leafId }` only | `AgentSessionConfig` has no ownership/identity field; authority comes from agent config or the run call |
|
|
57
|
+
| `AgentSession.run` / `AgentRunResult` | `src/agent-session/session.ts:190-191` (`run` → `randomId("run")`), `session.ts:318-344` (`buildRunResult`); `src/contracts-run-state.ts:284-320` | Direct execution is the default path | Run IDs are generated per call; `result.text` is history-derived (gate 4) |
|
|
58
|
+
| Run failure/abort shape | `src/agent-session/session/assemble.ts:462-501`; `src/contracts-run-state.ts:311-320` | Channel keys delivery off `AgentRunError.result.status/error/limit` | Failed runs **throw**; partial content may already be appended |
|
|
59
|
+
| Cancellation / abort | `src/agent-session/helpers.ts:155-168` (`bridgeAbort`, `throwIfAborted`); `src/agent-session/session/provider-round.ts:178-186` (`turnRequest.signal`) | Explicit `AbortSignal` propagation is proven end-to-end (probe: cancellation test) | "Cancellation is a request": an already-approved tool dispatch is not rolled back |
|
|
60
|
+
| Secure composition | `src/secure-agent.ts:26-80`; `src/agent-session/session/assemble.ts:348-357` | Reuse `createSecureAgent` unchanged; defaults immutable | Per-run `ownership`/`redactor`/`validate`/`effectStore`/`runState` overrides throw `AgentRunStateError` |
|
|
61
|
+
| Tool-scope narrowing | `src/tools.ts:170-187` (`selectRunTools`); `src/agent-session/session/assemble.ts:109` | `RunOptions.toolNames` narrows; checkpointed grant intersects on resume | A registry change without a revision bump fails fingerprint check on resume |
|
|
62
|
+
| Durable run state | `src/agent-run-state.ts:118-147` (`agentFingerprint`), `171-187` (`loadAgentRunState`), `189-208` (`saveAgentRunState`) | Existing namespace `agent-run-state`, versioned CAS | `AgentRunLifecycleRequest` carries ownership, not caller identity |
|
|
63
|
+
| Resume | `src/agent-run-lifecycle.ts:73-86` (`resume`), `88-101` (`resumeStream`), `174-363` (`prepareAgentRunResume`) | `resumeStream` is the channel resume path | Non-stream `resume` drops `signal` (gate 3, demonstrated gap) |
|
|
64
|
+
| Approvals / decisions | `src/agent-approval.ts` (`assertValidAgentRunResume`, `pendingDecisionsOf`, `resolveRunDecisions`); `src/agent-run-lifecycle.ts:216-300` | `RunDecision` with `expectedVersion` reuses lifecycle CAS | Partial batches resuspend; denied is terminal, not aborted |
|
|
65
|
+
| Ownership / identity | `src/identity.ts` (`assertIdentityActive`, `assertIdentityMatchesOwnership`); `src/contracts-core/persistence.ts:16-21` | `ownershipFromIdentity` projects verified identity to scope | Transport payload fields are never identity; host verifies |
|
|
66
|
+
| Conversation service | `packages/prism-core/src/runtime/server/conversations.ts:180-185` (options), `314-338` (`continue`) | **Not** the default execution seam | Incompatible with secure agents (gate 1, proven) |
|
|
67
|
+
| Checkpoint store | `src/contracts-core/persistence.ts:44-100`; memory `src/checkpoints.ts:26-89`; SQLite `packages/prism-core/src/sessions/sqlite/checkpoints.ts:114-150`; Postgres `packages/prism-core/src/sessions/postgres/checkpoints.ts:131` | Channel journal records; versioned CAS create `expectedVersion: 0` | Single-record CAS; OFFSET pagination; ownership mismatch throws; memory store evicts at 10 000 records / 1 MiB value |
|
|
68
|
+
| Lease store | `src/contracts-core/persistence.ts:105-128` | One receiver per connection; fencing token | Fences protect stored transitions, not a paused process's network side effects |
|
|
69
|
+
| Session-store idempotency | `src/contracts-core/session.ts:194-208`; memory `src/session-stores.ts:255-266`; SQLite `packages/prism-core/src/sessions/sqlite/persistence.ts:382-392`; Postgres `.../postgres/persistence.ts:291-301` | Correlation aid only | Dedup key is `(sessionId, idempotencyKey, expectedParentId)` (gate 2) |
|
|
70
|
+
| Redaction / credentials | `src/redaction.ts:205-223` (`errorToErrorInfo`), `src/credentials.ts` | `SecretRedactor` at error/event/journal/send boundaries | Exact-secret redaction is not DLP |
|
|
71
|
+
| PostgreSQL ERP messaging | `packages/prism-core/src/enterprise/postgres/erp-messaging.ts` | Optional host composition, not a channel requirement | Adds no external-send certainty |
|
|
72
|
+
|
|
73
|
+
Graph-wide searches for `telegram` and `signal-cli` still return no adapter in the repository.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## 3. Compatibility gates — resolution with evidence
|
|
78
|
+
|
|
79
|
+
### Gate 1 — Conversation vs secure agent (resolved: direct session)
|
|
80
|
+
|
|
81
|
+
`ConversationService.continue()` resolves the thread, builds a session from `sessionFactory`,
|
|
82
|
+
then calls `session.run(message, { ...runOptions, ownership, redactor, idempotencyKey, ... })`
|
|
83
|
+
(`conversations.ts:330-338`). `executeRun` rejects exactly those fields for a secure agent
|
|
84
|
+
(`assemble.ts:348-357`). Probe evidence:
|
|
85
|
+
|
|
86
|
+
- `rejects per-run ownership and redactor overrides for a secure agent, including through the
|
|
87
|
+
conversation service` — direct `session.run("go", { ownership })` rejects with
|
|
88
|
+
`AgentRunStateError`; `service.continue()` rejects with
|
|
89
|
+
`"Secure agent defaults cannot be replaced per run"`; provider calls stay at 1.
|
|
90
|
+
|
|
91
|
+
**Decision:** the channel's default and only MVP execution path is
|
|
92
|
+
`agent.createSession({ id, leafId }).run(text, { signal, idempotencyKey })`. The plan's
|
|
93
|
+
`runBoundTurn` snippet compiles and is exercised by the reopen probe. Conversation-service
|
|
94
|
+
threads may be layered later; that requires an explicitly scoped generic fix (e.g. an opt-out
|
|
95
|
+
so the service stops forcing redactor/ownership for an already-secure session) and is not part
|
|
96
|
+
of Task 2's default scope.
|
|
97
|
+
|
|
98
|
+
### Gate 2 — Idempotency is append dedup, not execution dedup (resolved: claim first)
|
|
99
|
+
|
|
100
|
+
Probe `does not treat idempotencyKey as execution deduplication at a new branch tip`:
|
|
101
|
+
|
|
102
|
+
- Run 1 succeeds, one provider call. `result.runId` is generated per call.
|
|
103
|
+
- Exact retry with the same session id, no leaf, same `idempotencyKey` → rejected with
|
|
104
|
+
`AgentRunError` whose `result.error.code === "session_append_conflict"` and
|
|
105
|
+
`idempotencyDuplicate: true`; provider calls stay 1.
|
|
106
|
+
- Same key retried at the advanced tip (`leafId: result.leafId`) → the append is a new
|
|
107
|
+
`(sessionId, key, expectedParentId)` tuple, so the provider runs a second time.
|
|
108
|
+
|
|
109
|
+
**Decision:** channel admission claims the external operation by checkpoint CAS **before**
|
|
110
|
+
`session.run`; `idempotencyKey` is set to the operation ID for correlation and append-level
|
|
111
|
+
retry safety only. No exactly-once execution claim.
|
|
112
|
+
|
|
113
|
+
### Gate 3 — Resume identity, tool scope, limits, cancellation (resolved: lifecycle + stream)
|
|
114
|
+
|
|
115
|
+
Probe `re-resolves ownership and the current agent definition on durable resume, refusing drift` proves:
|
|
116
|
+
|
|
117
|
+
- Wrong ownership → `Checkpoint ownership mismatch` before any work.
|
|
118
|
+
- Stale `expectedVersion` → `AgentRunStateError`.
|
|
119
|
+
- Wrong `agentId` capability → `"Agent run capability mismatch"`.
|
|
120
|
+
- Same `definitionRevision` but a widened tool registry → `"Agent definition revision or
|
|
121
|
+
fingerprint mismatch on resume"` (`agentFingerprint` includes tool name/schema/effect,
|
|
122
|
+
instructions, guardrails, loop revision).
|
|
123
|
+
- A clean resume re-runs `resolveAgent({ agentId, ownership })` and dispatches the approved
|
|
124
|
+
tool exactly once.
|
|
125
|
+
|
|
126
|
+
Tool narrowing survives resume through `selectRunTools(listed, options.toolNames, resumed?.state?.toolNames)`
|
|
127
|
+
(`assemble.ts:109`): the checkpointed grant is the ceiling, a new request can only intersect it.
|
|
128
|
+
|
|
129
|
+
**Demonstrated gap:** `createAgentRunLifecycle.resume()` (`agent-run-lifecycle.ts:73-86`)
|
|
130
|
+
pre-checks `request.signal` but does not pass it to `resumeAgentRun`; `AgentRunResumeOptions`
|
|
131
|
+
(`src/contracts-run-state.ts:229-247`) has no `signal` field at all. Only
|
|
132
|
+
`resumeAgentRunStream`/`resumeStream` accept and thread one (`agent-run-lifecycle.ts:99`), and
|
|
133
|
+
an early consumer return aborts the resumed run (`agent-run-lifecycle.ts:130-145`).
|
|
134
|
+
|
|
135
|
+
**Decision:** the channel resolves approval decisions with `lifecycle.resumeStream` (bounded
|
|
136
|
+
`maxQueuedEvents` + `overflow: "close"`), which also yields the event evidence the delivery
|
|
137
|
+
journal needs. Task 2 may instead add `signal?: AbortSignal` to `AgentRunResumeOptions` and
|
|
138
|
+
thread it — a one-field generic fix — but that is not required for MVP.
|
|
139
|
+
|
|
140
|
+
Probe `cancels resumed work through the resume stream only; non-stream resume drops the signal`:
|
|
141
|
+
|
|
142
|
+
- Pre-aborted signal on `lifecycle.resume` → rejects, the run stays `suspended`, zero tool
|
|
143
|
+
dispatch.
|
|
144
|
+
- Abort during a live resumed provider turn reaches `request.signal` in the in-flight turn,
|
|
145
|
+
ends the run as `aborted`, persists the `aborted` state, and does **not** roll back the
|
|
146
|
+
already-approved dispatch (exactly one call) — matching R4's "cancellation is a request".
|
|
147
|
+
|
|
148
|
+
### Gate 4 — Final-output provenance (resolved: project current-run output)
|
|
149
|
+
|
|
150
|
+
Probe `can expose stale or empty result text on failed and suspended runs`:
|
|
151
|
+
|
|
152
|
+
- Turn 1 succeeds with `answer 1`.
|
|
153
|
+
- Turn 2 fails before producing an assistant message: the thrown `AgentRunError.result` has
|
|
154
|
+
`status: "failed"` and **`text: "answer 1"`** — a previous turn's text.
|
|
155
|
+
- Turn 3 suspends on a tool approval: `status: "suspended"`, `text: ""` (the tool-call
|
|
156
|
+
assistant message has no text blocks).
|
|
157
|
+
- Aborted resume: `text: ""` although the provider yielded a `"partial"` delta.
|
|
158
|
+
|
|
159
|
+
**Decision:** the channel projects final text from the current operation's terminal event
|
|
160
|
+
stream plus persisted run evidence, and sends only for a `succeeded` operation whose output
|
|
161
|
+
is the current run's terminal assistant message. Never send `result.text` for `failed`,
|
|
162
|
+
`aborted`, `suspended`, `denied`, or `execution_unknown`, and never dump history, thinking
|
|
163
|
+
blocks, raw tool arguments, or previous turns.
|
|
164
|
+
|
|
165
|
+
### Gate 5 — Persistence fit (resolved: bounded journal, no fiction)
|
|
166
|
+
|
|
167
|
+
| Question | Verified answer | Consequence |
|
|
168
|
+
| --- | --- | --- |
|
|
169
|
+
| Value size cap | Memory store defaults `maxValueBytes = 1 MiB`, `maxRecords = 10 000` (`src/checkpoints.ts:22-30`); SQLite/Postgres checkpoint tables have no value-size cap | 128 KiB channel record cap is a consumer bound; enforce before save |
|
|
170
|
+
| Page size | SQLite and Postgres clamp `limit` to `[1, 500]`, default 100; memory defaults `maxPageSize = 500` | Journal page default 100, hard max 500 |
|
|
171
|
+
| Pagination stability | Checkpoint cursors are numeric OFFSETs (`decodeCheckpointCursor`, `src/sessions/codecs/checkpoint.ts:30-34`) | A page walk is not a consistent snapshot under concurrent writes; recovery must resolve explicit keys, not rely on scan completeness |
|
|
172
|
+
| Multi-record atomicity | `CheckpointStore` exposes single-record CAS only (`saveCheckpoint`/`loadCheckpoint`/`listCheckpoints`/`deleteCheckpoint`) | Commit order is explicit: operations (with dispositions) before cursor advance; reply before send; replay/dedup on partial batch |
|
|
173
|
+
| Ownership keying | Record identity is `namespace + key` (`src/checkpoints.ts:123-125`); a mismatched scope throws `"Checkpoint ownership mismatch"` (`src/checkpoints.ts:151-155`) | Keys must embed connection/tenant/user so a foreign lookup misses; an unscoped key leaks existence via the mismatch error |
|
|
174
|
+
| Fencing | `LeaseStore.tryAcquireLease`/`renewLease`/`releaseLease` with monotonic `fencingToken`; checkpoint saves accept a fence | Takeover fences stored transitions; a paused process's already-issued network call is not retracted |
|
|
175
|
+
|
|
176
|
+
### Gate 6 — Version truth (resolved)
|
|
177
|
+
|
|
178
|
+
- Root `package.json` is `0.7.0`; `@arnilo/prism-core` is `0.7.0`; root has zero runtime
|
|
179
|
+
dependencies; `@arnilo/prism-core` runtime deps are `@napi-rs/keyring ^2.0.0`, `ajv ^8.17.1`.
|
|
180
|
+
- Optional peers: `better-sqlite3 ^13.0.3`, `pg ^8.23.0`, NATS `^3.4.0`, `@arnilo/prism-memory`.
|
|
181
|
+
- Node `>=22` engines; export surfaces are declared per subpath in the package manifest (no
|
|
182
|
+
barrel imports of optional drivers).
|
|
183
|
+
- The plan's "baseline 0.6.0" sentence is historical; the reviewed revision and the 0.8.0
|
|
184
|
+
target are recorded above. `docs/history/0.7.0-primitive-review.md` remains the prior-line record.
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## 4. Frozen proposed channel surface
|
|
189
|
+
|
|
190
|
+
Subpath names are frozen as proposed; Task 2 may refine option **fields** without renaming
|
|
191
|
+
symbols or moving transport types into the root package.
|
|
192
|
+
|
|
193
|
+
| Subpath | Frozen values | Frozen types |
|
|
194
|
+
| --- | --- | --- |
|
|
195
|
+
| `@arnilo/prism-core/integrations/channels` | `createMessagingRuntime` | `MessagingRuntime`, `MessagingRuntimeOptions`, `MessagingRuntimeDrainResult`, `ChannelAdapter`, `ChannelCapabilities`, `ChannelInboundEvent`, `ChannelReply`, `ChannelSendResult`, `ChannelAuthorizeInput`, `ChannelAuthorization`, `ChannelAgentResolverInput`, `ChannelLimits`, `ChannelAdmission`, `ChannelDiagnostics` |
|
|
196
|
+
| `@arnilo/prism-core/integrations/channels/telegram` | `createTelegramAdapter`, `createTelegramWebhookHandler` | `TelegramAdapterOptions`, `TelegramWebhookHandlerOptions` |
|
|
197
|
+
| `@arnilo/prism-core/integrations/channels/signal` | `createSignalAdapter` | `SignalAdapterOptions` |
|
|
198
|
+
|
|
199
|
+
Minimum lifecycle shape fixed for Task 2 (field names may be refined):
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
interface ChannelAdapter {
|
|
203
|
+
readonly connectionId: string;
|
|
204
|
+
readonly capabilities: ChannelCapabilities; // { acknowledgement: "telegram_offset" | "none"; controls: "callback" | "command"; maxTextCodeUnits: number }
|
|
205
|
+
start(receive: (event: ChannelInboundEvent) => Promise<void>): Promise<void>;
|
|
206
|
+
send(reply: ChannelReply): Promise<ChannelSendResult>;
|
|
207
|
+
stop(): Promise<void>;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
interface MessagingRuntime {
|
|
211
|
+
admit(event: ChannelInboundEvent): Promise<ChannelAdmission>; // durable before the adapter confirms transport receipt
|
|
212
|
+
drain(options?: { deadlineMs?: number }): Promise<MessagingRuntimeDrainResult>;
|
|
213
|
+
diagnostics(): ChannelDiagnostics;
|
|
214
|
+
stop(): Promise<void>;
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Rules frozen with the names:
|
|
219
|
+
|
|
220
|
+
- `admit` resolves authorization and capability binding before any provider call and returns a
|
|
221
|
+
bounded outcome; adapters await it before transport acknowledgment (Telegram offset advance
|
|
222
|
+
or webhook 2xx).
|
|
223
|
+
- `ChannelCapabilities.acknowledgement` is `"none"` for Signal — the shared runtime has no
|
|
224
|
+
acknowledgment step it could pretend to honor.
|
|
225
|
+
- Transport-specific types never enter root `@arnilo/prism` contracts; no Telegram/Signal
|
|
226
|
+
branch is added to the agent loop.
|
|
227
|
+
- Factories perform no I/O; adapters never receive provider or tool registries.
|
|
228
|
+
- New subpaths must be added to `packages/prism-core/package.json` exports and pass
|
|
229
|
+
`scripts/budget-gate.test.mjs` export ceilings plus `scripts/phase54-package-map.test.mjs`,
|
|
230
|
+
`scripts/packaging-current.test.mjs`, and `scripts/import-hygiene.test.mjs` before release.
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## 5. Authority, idempotency, and recovery resolution (R2–R6)
|
|
235
|
+
|
|
236
|
+
| Requirement | Resolution on verified primitives |
|
|
237
|
+
| --- | --- |
|
|
238
|
+
| R2 verified identity / scoped grants | Host `authorize(observed sender + action)` returns a verified `AgentIdentity` + exact `OwnershipScope` + alias grant + revision. Ownership is passed to `session.run`/lifecycle requests; identity is on agent config (secure) or run `identity` with `assertIdentityMatchesOwnership`. Transport fields are never trusted directly. |
|
|
239
|
+
| R2 pairing | Channel-owned one-use token record in `prism.channels.control.*` with hash, binding, grant revision and 5-minute expiry; confirmation stays in the trusted host interface. |
|
|
240
|
+
| R2 key namespacing | Journal/binding keys embed connection + tenant/account/user + external actor so a foreign read misses instead of colliding (`recordKey` is scope-free; mismatch throws). |
|
|
241
|
+
| R2 revocation | Re-check grant revision before dispatch, before approval consumption, and before each send; revoke cancels active runs via the same `AbortSignal` seam and blocks future sends. Already-accepted platform effects are not retractable. |
|
|
242
|
+
| R3 routing / concurrency | Binding = connection + external conversation + actor grant + alias → owned session + optional leaf. Core gives *no* cross-object serialization (probe: two objects on one session ID both run), so the runtime serializes per logical session ID and bounds process concurrency. |
|
|
243
|
+
| R3 shared-session arbitration | If another producer can drive the same session, the host must share the same admission/lease; otherwise the channel refuses shared-session binding. |
|
|
244
|
+
| R4 safe execution | Host resolves `AgentConfig`; text cannot replace model/instructions/tools/policy/limits; slash text maps only to the fixed command allow-list; tool-free default example; `createSecureAgent` example for tool use. |
|
|
245
|
+
| R4 failed/suspended output | Gate 4: send only the current run's terminal text on `succeeded`. Bound the failure notice and redact it. |
|
|
246
|
+
| R4 approval resume | Gate 3: `resumeStream` with bounded subscription and the existing `expectedVersion`/fingerprint checks; approval tokens are opaque server records, `allow_once`/`reject_once` only. |
|
|
247
|
+
| R5 durable intake | Checkpoint records in `prism.channels.*`; operation save precedes cursor advance; operation claim via CAS precedes `session.run`; reply persisted before send; `delivery_unknown` on ambiguous send; no automatic rerun of `executing` after crash. |
|
|
248
|
+
| R5 evidence-based reconciliation | Only exact-owned run-correlated session/ledger/checkpoint evidence; otherwise `execution_unknown` → host review. |
|
|
249
|
+
| R5 ambiguity truth | No exactly-once model/tool execution and no lossless Signal intake claim; operator replay requires authorization + expected version + explicit duplicate-risk acknowledgment. |
|
|
250
|
+
| R6 status/cancel | `/status` reads the bound operation/session only; `/cancel` aborts the active run via `AbortSignal` or cancels queued work; suspended runs use the existing CAS-checked denial path and are documented as `denied`, never hand-edited. |
|
|
251
|
+
| R7 observability | Bounded diagnostics from runtime counters; no message text, phone numbers, pairing links, token URLs, or key material in logs/metrics; redactor applied at error/event/journal/send boundaries. |
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## 6. Durable limits and admission strategy (Performance acceptance)
|
|
256
|
+
|
|
257
|
+
Channel-imposed caps, compatible with the verified store limits (store ceiling in the last
|
|
258
|
+
column; the channel cap is the one enforced):
|
|
259
|
+
|
|
260
|
+
| Resource | Channel default | Hard cap | Store-derived ceiling |
|
|
261
|
+
| --- | --- | --- | --- |
|
|
262
|
+
| Accepted input text | 32 KiB UTF-8 | 64 KiB | run `maxRequestBytes` (process HARD caps stay) |
|
|
263
|
+
| Pending ordinary turns | 8 per binding, 100 per process | 32 / 500 | runtime queue, not store |
|
|
264
|
+
| Active independent sessions | 4 per process; same logical session always serial | host-configurable | runtime |
|
|
265
|
+
| Outgoing logical response | 64 KiB UTF-8, bounded overflow policy | 128 KiB | platform text limit (Telegram 4096 chars/msg) |
|
|
266
|
+
| Channel checkpoint record | 128 KiB encoded JSON after escaping/metadata | 512 KiB | memory store 1 MiB value cap |
|
|
267
|
+
| Journal list page | 100 records | 500 records | checkpoint list clamp `[1, 500]` |
|
|
268
|
+
| Send attempts | 5 known-retryable | 5 | runtime |
|
|
269
|
+
| Pair/control expiry | 5 minutes | host-tightenable | runtime |
|
|
270
|
+
| Processed transport payload retention | 7 days | host policy | runtime tombstones |
|
|
271
|
+
|
|
272
|
+
Admission strategy: in-process serial queue keyed by logical session ID (core provides
|
|
273
|
+
none across runtime objects — proven), process-wide concurrency bound, durable CAS claim
|
|
274
|
+
before provider work, control messages bypass the prompt queue, and capacity/storage
|
|
275
|
+
unavailability fails admission explicitly instead of evicting accepted work. No network or
|
|
276
|
+
background work is introduced by this review.
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
## 7. Threat-model resolution
|
|
281
|
+
|
|
282
|
+
| Threat | Control | Evidence |
|
|
283
|
+
| --- | --- | --- |
|
|
284
|
+
| Forged transport identity (username, phone string, JSON field) | Host-verified `AgentIdentity` keyed on stable platform IDs; ownership mismatch fails closed | identity contracts; checkpoint ownership probe |
|
|
285
|
+
| Cross-tenant session probe | Keyed bindings; ownership mismatch throws; foreign lookups return miss | `src/checkpoints.ts:123-155` |
|
|
286
|
+
| Prompt-driven privilege escalation (slash/config/service text) | Fixed command allow-list; text is untrusted input; agent/tools/policy are host-resolved | R4 rules; toolNames/secure guard |
|
|
287
|
+
| Replay/duplicate external events | Durable operation dedup by connection-scoped external event key before claim | R5 invariants; checkpoint CAS |
|
|
288
|
+
| Crash between claim and provider call | `executing` never auto-rerun; evidence reconciliation or `execution_unknown` | Gate 2 + Gate 5 |
|
|
289
|
+
| Ambiguous send (timeout after accept) | `delivery_unknown`; authorized replay with version + duplicate-risk acknowledgment | R5 invariant 6 |
|
|
290
|
+
| Second receiver / stale worker | Lease + fencing token; no redispatch of old `sending`/`executing` | lease contract |
|
|
291
|
+
| Approval token forwarding/replay | Opaque token bound to principal/connection/chat/session/run/approval/version/grant+expiry; stale/foreign fail closed | R6; lifecycle expectedVersion |
|
|
292
|
+
| Secret leakage in errors/logs/URLs | `SecretRedactor` at boundaries; Telegram token URL redaction; Signal key material excluded | `errorToErrorInfo`, redaction contracts |
|
|
293
|
+
| GPL obligation from signal-cli | Adapter is a JSON-RPC socket client; no linking, copying, bundling or redistribution of GPL code | signal-cli API surface only |
|
|
294
|
+
| Signal acceptable-use violation | DM-only, user-initiated/paired, no bulk or automated registration; operator acceptable-use decision before supported release | Signal terms (below) |
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## 8. Platform fact re-verification (2026-09-15)
|
|
299
|
+
|
|
300
|
+
Telegram Bot API facts confirmed against `https://core.telegram.org/bots/api`:
|
|
301
|
+
|
|
302
|
+
- `getUpdates` and webhooks are mutually exclusive; an outgoing webhook makes `getUpdates`
|
|
303
|
+
fail, and a set webhook blocks `getUpdates`.
|
|
304
|
+
- Updates are stored until received but not longer than 24 hours.
|
|
305
|
+
- `offset` must be one higher than the highest received `update_id`; an offset above an
|
|
306
|
+
`update_id` confirms it; negative offsets forget prior updates.
|
|
307
|
+
- `getUpdates.limit` accepts 1–100 (default 100); `timeout` is the long-poll seconds.
|
|
308
|
+
- `sendMessage.text` is 1–4096 characters after entity parsing.
|
|
309
|
+
- `callback_data` is 1–64 bytes.
|
|
310
|
+
- `setWebhook.secret_token` is 1–256 chars from `A-Za-z0-9_-`, delivered as
|
|
311
|
+
`X-Telegram-Bot-Api-Secret-Token`.
|
|
312
|
+
- `ResponseParameters.retry_after` is the flood-control wait in seconds.
|
|
313
|
+
- All calls use `https://api.telegram.org/bot<token>/METHOD_NAME` — the token is always in
|
|
314
|
+
the URL path, so URL redaction is mandatory.
|
|
315
|
+
- The page no longer supports a blanket "bots never see other bots" assumption (bot-to-bot
|
|
316
|
+
communication exists when both sides enable it), so bot-origin updates are filtered
|
|
317
|
+
explicitly rather than assumed impossible.
|
|
318
|
+
|
|
319
|
+
signal-cli v0.14.8 facts confirmed against the pinned tag:
|
|
320
|
+
|
|
321
|
+
- Repository description: "unofficial commandline, JSON-RPC and dbus interface for the
|
|
322
|
+
Signal messenger"; license GPL-3.0.
|
|
323
|
+
- JVM distribution requires "at least Java Runtime Environment (JRE) 25".
|
|
324
|
+
- "signal-cli releases older than three months may not work correctly" — the pinned version
|
|
325
|
+
is a researched baseline, not a floating download.
|
|
326
|
+
- Registering "will unregister any existing client associated with the same number" —
|
|
327
|
+
registration/relinking is never automated from chat.
|
|
328
|
+
- `--receive-mode=manual` disables automatic receiving; `subscribeReceive` starts receiving
|
|
329
|
+
and returns a subscription id; manual notifications are wrapped as
|
|
330
|
+
`params.result.envelope`; `unsubscribeReceive` stops the subscription.
|
|
331
|
+
- `send` takes `recipient`/`message`; no application-acknowledged receive log or replay
|
|
332
|
+
guarantee is documented, and `GET /api/v1/check` only reports daemon liveness.
|
|
333
|
+
- No single-receiver guarantee: one receiver per account is a host operating rule.
|
|
334
|
+
|
|
335
|
+
Signal legal facts confirmed against `https://signal.org/legal/`:
|
|
336
|
+
|
|
337
|
+
- Users must not use the service in ways that "involve sending illegal or impermissible
|
|
338
|
+
communications such as bulk messaging, auto-messaging, and auto-dialing".
|
|
339
|
+
- Accounts must not be created "through unauthorized or automated means".
|
|
340
|
+
- Signal "may modify, suspend, or terminate your access ... if you violate the letter or
|
|
341
|
+
spirit of our Terms".
|
|
342
|
+
|
|
343
|
+
**Policy/license gate resolution:** Signal support ships only as experimental/opt-in,
|
|
344
|
+
externally supervised, DM-only, with no automated registration, no bulk or proactive
|
|
345
|
+
broadcast, and an operator-recorded acceptable-use decision before any "supported" claim.
|
|
346
|
+
The GPLv3 obligation is not triggered by a JSON-RPC client over a Unix socket; distribution
|
|
347
|
+
must not bundle or redistribute the binary or patched libraries. Prism remains MIT.
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
## 9. New seams justified
|
|
352
|
+
|
|
353
|
+
| New seam | Why the primitives do not already cover it | Explicitly not created |
|
|
354
|
+
| --- | --- | --- |
|
|
355
|
+
| Channel envelope/event/reply types | Transport-neutral mapping for two transports with different receipt/ack semantics | No transport objects in core contracts |
|
|
356
|
+
| `ChannelAdapter` interface | Two real implementations; receive/send/capabilities/start/stop vary | No third abstraction layer or plugin registry |
|
|
357
|
+
| `MessagingRuntime` facade | Admission, per-session serialization, diagnostics, drain are not in core | No second agent runtime; execution stays `AgentSession` |
|
|
358
|
+
| Journal record schemas (`prism.channels.*`) | Core has no operation/delivery state machine | No new SQL schema; no duplicate conversation model |
|
|
359
|
+
| Pairing/token mapping | Transport controls must become existing `RunDecision`s | No channel-specific approval engine |
|
|
360
|
+
| Telegram/Signal wire clients | Vendor protocols | No bot SDK dependency, no provider, no model router |
|
|
361
|
+
|
|
362
|
+
Core changes are **not** proposed by this review. The only demonstrated generic gap
|
|
363
|
+
(`resume` signal pass-through) has a working alternative (`resumeStream`) and stays a Task 2
|
|
364
|
+
conditional item; any core fix must be generic and regression-tested.
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
## 10. Evidence and reproduction
|
|
369
|
+
|
|
370
|
+
Executable evidence (7 passing tests):
|
|
371
|
+
|
|
372
|
+
```sh
|
|
373
|
+
npm run build --workspace @arnilo/prism-core
|
|
374
|
+
node --test "packages/prism-core/dist/integrations/channels/__tests__/primitives.test.js"
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
Covered proofs: store reopen + two turns via the proposed `runBoundTurn`; two runtime
|
|
378
|
+
objects on one logical session ID both execute; secure override conflict direct and through
|
|
379
|
+
`createConversationService`; resume ownership/version/agentId/fingerprint drift refusal plus
|
|
380
|
+
approved-dispatch-once; pre-abort leaves the run suspended and zero side effects; stream
|
|
381
|
+
resume abort reaches the provider turn, persists `aborted`, and does not roll back the
|
|
382
|
+
approved dispatch; `idempotencyKey` dedup exact-retry only and run IDs per call; stale/empty
|
|
383
|
+
result text on failed/suspended/aborted runs.
|
|
384
|
+
|
|
385
|
+
Task 4 moved the test to `packages/prism-core/src/__tests__/messaging-primitives.test.ts` and
|
|
386
|
+
adds its explicit `dist/__tests__/messaging-primitives.test.js` entry to the prism-core package
|
|
387
|
+
test script. The other nested prism-core suites (`runtime/server`, `sessions/*`, `governance/*`)
|
|
388
|
+
still expose the pre-existing shell-glob topology gap: `dist/**/__tests__/*.test.js` collapses
|
|
389
|
+
to one level under the shell. Run this review's evidence with the explicit command above; Node's
|
|
390
|
+
own quoted glob (`node --test "packages/prism-core/dist/**/__tests__/*.test.js"`) also matches
|
|
391
|
+
nested paths.
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# Plan 080 Task 1 — Messaging Channel Follow-ons: Primitive, Compatibility, and Threat-Model Review
|
|
2
|
+
|
|
3
|
+
Plan: [080-Messaging-Channel-Followons-And-0-8-0-Cut.md](../../plans/080-Messaging-Channel-Followons-And-0-8-0-Cut.md) Task 1
|
|
4
|
+
Depends on: [079](../../plans/079-Prism-Messaging-Channels-Telegram-Signal.md) Tasks 1–9 (complete 2026-09-16) and [docs/history/079-messaging-primitive-review.md](079-messaging-primitive-review.md)
|
|
5
|
+
Baseline reviewed: working tree after 079 (version still `0.7.0`; 0.8.0 cut is Task 10).
|
|
6
|
+
Date: 2026-09-16
|
|
7
|
+
|
|
8
|
+
Executable evidence:
|
|
9
|
+
|
|
10
|
+
- [`packages/prism-core/src/__tests__/messaging-primitives.test.ts`](../../packages/prism-core/src/__tests__/messaging-primitives.test.ts) — 079 probes plus two 080 gap probes (foreign checkpoint load; live abort dropped by non-stream `resume`).
|
|
11
|
+
- [`packages/prism-channels/src/__tests__/runtime.test.ts`](../../packages/prism-channels/src/__tests__/runtime.test.ts) — `threadId` participates in session derivation; `ChannelReply.kind` has no `draft`.
|
|
12
|
+
|
|
13
|
+
This is a frozen review record (history). Current-contract docs stay in `docs/` from Task 2 onward. **No new public symbols in this task.**
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 1. Executive summary
|
|
18
|
+
|
|
19
|
+
079 already shipped the transport-neutral runtime, durable journal, Telegram adapter, experimental Signal adapter, approvals, and host examples. Every 079 **Further Action** is either a later 080 task or an explicit out-of-scope row. No second agent engine, no new SQL schema, no `pg` peer on `@arnilo/prism-channels`, no Signal groups/media/drafts/bulk.
|
|
20
|
+
|
|
21
|
+
What already holds (do not rebuild):
|
|
22
|
+
|
|
23
|
+
| Primitive | Span | Follow-on use |
|
|
24
|
+
| --- | --- | --- |
|
|
25
|
+
| `ChannelInboundEvent.threadId` | `packages/prism-channels/src/types.ts:67` | Topic identity is already on the event and in `deriveSessionId` / `actorKey`. Groups/topics are adapter+grant policy, not a new key type. |
|
|
26
|
+
| `ChannelAuthorization` | `types.ts:91-99` | Deny-by-default host grant. Add optional `notifications?: boolean` (missing = false). |
|
|
27
|
+
| `admit` / `deliver` | `runtime.ts:1403-1480`; `MessagingRuntimeOptions.deliver` `types.ts:344` | Unicast to the bound destination. Notify reuses `deliver`; drafts must **not**. |
|
|
28
|
+
| Journal namespaces | `state.ts:11-16` | `prism.channels.v1.{binding,operation,cursor,reply,control}`. Media bytes never become values. |
|
|
29
|
+
| `TranscriptionProvider` / `SpeechProvider` | `src/contracts-core/transcription.ts:42-47`; `speech.ts:32-36` | Host injects `transcribe`/`synthesize` wrappers. Channels does not depend on `@arnilo/prism-providers`. |
|
|
30
|
+
| `createPostgresErpMessaging` | `packages/prism-core/src/enterprise/postgres/erp-messaging.ts:44-69` | Host `BEGIN` + `outbox.append(client, …)` + `COMMIT` then `adapter.send`. Caller-owned `PoolClient`. |
|
|
31
|
+
| `resumeAgentRun` vs `resumeAgentRunStream` | `src/agent-run-lifecycle.ts:73-86,111-118` vs `88-105,121-146` | Stream threads `signal`; non-stream pre-checks then drops it. **Task 3.** |
|
|
32
|
+
| Checkpoint ownership | memory `src/checkpoints.ts:82-89,151-155`; SQLite `sessions/sqlite/checkpoints.ts:106-112`; Postgres `sessions/postgres/checkpoints.ts:97-103` | Foreign load **throws** `"Checkpoint ownership mismatch"` (existence leak). **Task 3** → miss + generic CAS conflict. |
|
|
33
|
+
| Workspace test globs | `packages/prism-core/package.json` `test`; `packages/prism-coding-tools/package.json` `test` | Unquoted `dist/**/__tests__/*.test.js` collapses under the shell. **Task 2.** |
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 2. 079 Further Action → 080 task map
|
|
38
|
+
|
|
39
|
+
| 079 Further Action | 080 task | Notes |
|
|
40
|
+
| --- | --- | --- |
|
|
41
|
+
| Group/topic ownership | 4 | Telegram `allowGroups` default `false`. Signal stays DM-only. |
|
|
42
|
+
| Telegram draft streaming | 5 | Bot API `sendMessageDraft`; not a journaled `ChannelReply.kind`. |
|
|
43
|
+
| Bounded image/document | 6 | Refs on the event; `getFile` + download cap; no bytes in the journal. |
|
|
44
|
+
| Voice transcription/synthesis | 6 | Host `TranscriptionProvider` / `SpeechProvider`. |
|
|
45
|
+
| Opt-in completion notifications | 7 | `runtime.notify` + `ChannelAction` `"notify"` + `notifications: true`. |
|
|
46
|
+
| ERP outbox composition | 8 | Example + docs. No new table. No `pg` on channels. |
|
|
47
|
+
| OS SIGTERM injection | 9 | Restart worker `stop()` on `SIGTERM`. |
|
|
48
|
+
| 72-hour soak runner | 9 | Same script; CI ≤30s; operator 72h. |
|
|
49
|
+
| Nested `**` test globs | 2 | Quote or expand; fix newly executed failures. |
|
|
50
|
+
| `resume()` dropped `signal` | 3 | Add `AgentRunResumeOptions.signal`; thread like stream. |
|
|
51
|
+
| Checkpoint foreign-scope leak | 3 | Load → `null`; save generic conflict. |
|
|
52
|
+
| 0.8.0 cut | 10 | Lockstep + handoff; no registry write. |
|
|
53
|
+
| OFFSET `listCheckpoints` pagination | **out of 080** | Generic store limit; not a channel follow-on. |
|
|
54
|
+
| 079 items later tasks already closed (in-process dedup, durable approvals, `maxRoutes`, SIGKILL reconcile) | **closed** | Do not re-open. |
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## 3. Frozen additive vocabulary
|
|
59
|
+
|
|
60
|
+
Implementation tasks may grow **fields**. They must not rename 079 symbols (`admit`, `deliver`, `ChannelInboundEvent`, `ChannelReply`, `ChannelAuthorization`, journal namespace strings, `createTelegramAdapter`, `createSignalAdapter`).
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
type ChannelAction = "message" | "command" | "approval" | "notify";
|
|
64
|
+
|
|
65
|
+
interface ChannelAuthorization {
|
|
66
|
+
// existing: identity, agentAliases, defaultAgentAlias?, grantRevision
|
|
67
|
+
readonly notifications?: boolean; // missing === false
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface ChannelAttachmentRef {
|
|
71
|
+
readonly kind: "image" | "document" | "voice";
|
|
72
|
+
readonly transportFileId: string; // Telegram file_id; never model-selected
|
|
73
|
+
readonly mimeType?: string;
|
|
74
|
+
readonly byteLength?: number;
|
|
75
|
+
readonly fileName?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface ChannelInboundEvent {
|
|
79
|
+
// existing fields unchanged
|
|
80
|
+
readonly attachments?: readonly ChannelAttachmentRef[]; // ids only — never bytes
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface ChannelLimits {
|
|
84
|
+
// existing fields unchanged
|
|
85
|
+
readonly maxAttachmentBytes?: number; // default 1 MiB, hard 4 MiB
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
interface MessagingRuntime {
|
|
89
|
+
// existing: admit, drain, diagnostics, stop, reconcile, listUnresolved, prune
|
|
90
|
+
notify(input: {
|
|
91
|
+
readonly identity: AgentIdentity;
|
|
92
|
+
readonly connectionId: string;
|
|
93
|
+
readonly externalConversationId: string;
|
|
94
|
+
readonly threadId?: string;
|
|
95
|
+
readonly notifyId: string;
|
|
96
|
+
readonly text: string;
|
|
97
|
+
}): Promise<ChannelAdmission>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
interface MessagingRuntimeOptions {
|
|
101
|
+
// existing authorize/resolveAgent/deliver/…
|
|
102
|
+
readonly onAssistantDelta?: (delta: {
|
|
103
|
+
readonly connectionId: string;
|
|
104
|
+
readonly externalConversationId: string;
|
|
105
|
+
readonly threadId?: string;
|
|
106
|
+
readonly text: string;
|
|
107
|
+
}) => void;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
interface TelegramAdapterOptions {
|
|
111
|
+
readonly allowGroups?: boolean; // default false
|
|
112
|
+
readonly sendDrafts?: boolean; // default false; private chats only
|
|
113
|
+
readonly maxAttachmentBytes?: number;
|
|
114
|
+
readonly transcribe?: (audio: Uint8Array, format: string | undefined, signal?: AbortSignal) => Promise<string>;
|
|
115
|
+
readonly synthesize?: (text: string, signal?: AbortSignal) => Promise<{ audio: Uint8Array; format: string }>;
|
|
116
|
+
readonly extractDocumentText?: (bytes: Uint8Array, mimeType: string | undefined, signal?: AbortSignal) => Promise<string>;
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**Explicitly not added:**
|
|
121
|
+
|
|
122
|
+
- `ChannelReply.kind` `"draft"` — drafts are ephemeral Bot API previews; journal stays `final` \| `notice`.
|
|
123
|
+
- Broadcast/fan-out API.
|
|
124
|
+
- Signal groups, attachments, drafts, or bulk notify.
|
|
125
|
+
- `pg` / ERP types inside `@arnilo/prism-channels`.
|
|
126
|
+
- New checkpoint SQL schema.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## 4. Platform facts (re-verified 2026-09-16)
|
|
131
|
+
|
|
132
|
+
Telegram Bot API (`https://core.telegram.org/bots/api`, fetched 2026-09-16):
|
|
133
|
+
|
|
134
|
+
| Fact | Implication |
|
|
135
|
+
| --- | --- |
|
|
136
|
+
| `sendMessageDraft`: `chat_id` Integer **Yes** — “Unique identifier for the target **private** chat”; `draft_id` Integer **Yes**, non-zero; `text` Optional 0–4096 (empty → “Thinking…”); `message_thread_id` Optional; ephemeral **30-second** preview; **must** follow with `sendMessage` to persist. | Task 5: private chats only. Groups/Signal: no drafts. Not journaled. Coalesce in-flight drafts. |
|
|
137
|
+
| `sendRichMessageDraft` exists | Out of 080 (YAGNI; text drafts only). |
|
|
138
|
+
| `getFile`: bots download ≤ **20 MB**; URL `https://api.telegram.org/file/bot<token>/<file_path>` valid ≥ 1 hour. Token is in the path. | Task 6 channel hard cap **4 MiB** (default 1 MiB) — below getFile. Redact token URLs. Bounded reader; fail closed on oversize. |
|
|
139
|
+
| `sendVoice`: OGG/OPUS, MP3, or M4A; voice notes. `sendDocument`: general files up to 50 MB upload. Sending by URL: photos 5 MB, other 20 MB; `sendVoice` by URL ≤ 1 MB else sent as files. | Outbound speech uses `sendVoice` to the **bound** chat/thread. Never model-selected `file_id` or URL. |
|
|
140
|
+
| `Message.message_thread_id` / forum topics; Bot API 9.3 private-chat topics | Copy into `event.threadId` when present (private and, if `allowGroups`, group/supergroup). Already in `deriveSessionId` (`runtime.ts:359-376`). |
|
|
141
|
+
| `parseMessageUpdate` today (`telegram.ts:235-254`) | Private + non-bot `from` + string `text` only. Drops groups, media, `message_thread_id`. Tasks 4–6 widen this filter. |
|
|
142
|
+
| Group privacy mode | Bots see commands only unless privacy disabled. Document; do not bypass. |
|
|
143
|
+
| Anonymous admins / missing `from.id` / `sender_chat` | No actor id → drop (fail closed). Never use chat title or username as `externalActorId`. |
|
|
144
|
+
| Forwards (`forward_date` / `forward_origin`) | `claims` only; host grant must opt in. Default: do not run a model. |
|
|
145
|
+
|
|
146
|
+
Signal (`signal.ts:225-267` `notificationEvent`): drop when `groupInfo` is set or `attachments` is a non-empty array. DM text only. Terms (079 §8): no bulk/auto-messaging. **Notify is unicast to an existing DM binding**, not a fan-out.
|
|
147
|
+
|
|
148
|
+
Speech/transcription (`docs/speech.md`; contracts above): `transcribe({ model, audio: Uint8Array, format?, signal? })`; `synthesize({ model, input, signal? })`. OpenAI adapter caps (25 MiB transcribe / 4096 chars speech) sit **above** the channel attachment cap. Host wraps providers; channels never imports them.
|
|
149
|
+
|
|
150
|
+
ERP (`erp-messaging.ts:44-69`, `docs/enterprise-postgres-state.md`): `outbox.append` requires a caller-owned `PoolClient` in the host transaction. Payload cap 64 KiB. Dispatcher never invokes business callbacks. Channel `deliver` may append **correlation ids** (`operationId`, `connectionId`), not inbound chat text.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## 5. Compatibility gates still open
|
|
155
|
+
|
|
156
|
+
### Gate A — Nested workspace test globs (Task 2)
|
|
157
|
+
|
|
158
|
+
`packages/prism-core` and `packages/prism-coding-tools` `test` scripts pass unquoted `dist/**/__tests__/*.test.js` through the shell. `**` collapses to one directory, so nested suites (`runtime/server`, `sessions/*`, `governance/*`, coding-tools nested trees) are omitted. `messaging-primitives.test.js` is listed explicitly on prism-core and **must stay**. Quote the glob or reuse `scripts/run-all-tests.mjs` `expandGlob`. Newly executed suites must pass or be named-skipped — not silenced by turning off network-free/secret-scan guards.
|
|
159
|
+
|
|
160
|
+
### Gate B — `resumeAgentRun` drops live abort (Task 3)
|
|
161
|
+
|
|
162
|
+
`createAgentRunLifecycle.resume` (`agent-run-lifecycle.ts:73-86`) pre-checks `request.signal` then calls `resumeAgentRun` **without** `signal`. `AgentRunResumeOptions` (`src/contracts-run-state.ts:229-247`) has no `signal` field; only `AgentRunResumeStreamOptions` (`250-252`) does. `resumeAgentRun` (`111-118`) calls `prepareAgentRunResume(..., options)` with no fifth argument, so `executePreparedAgentRunResume` never sees the host abort.
|
|
163
|
+
|
|
164
|
+
079 probe already covers pre-abort (run stays `suspended`) and stream abort (provider sees `signal`). 080 probe: abort **during** a hanging non-stream resume — provider `request.signal` stays live and the run **succeeds**. Channels still resume via `resumeStream` until Task 3.
|
|
165
|
+
|
|
166
|
+
### Gate C — Checkpoint foreign-scope existence leak (Task 3)
|
|
167
|
+
|
|
168
|
+
`recordKey` is `namespace\0key` (`src/checkpoints.ts:123-125`). `loadCheckpoint` after a hit calls `assertOwnership` / `assertOwnershipScope` and throws `CheckpointConflictError("Checkpoint ownership mismatch")` on memory, SQLite, and Postgres. A tenant can distinguish “other tenant has this key” from “missing”.
|
|
169
|
+
|
|
170
|
+
Channel keys already embed tenant/user (`state.ts:135-157`), so a well-formed foreign lookup **misses** the key. The leak remains for any unscoped or colliding key and for direct store use. Task 3: load mismatch → `null` (same as missing); save mismatch → generic CAS conflict, same wording as a version miss. Do not log the foreign owner.
|
|
171
|
+
|
|
172
|
+
Callers that caught `"Checkpoint ownership mismatch"` as a signal (e.g. `packages/prism-coding-tools/.../workspace-lifecycle.ts:448-465` maps any throw to `ERR_PRISM_WORKSPACE_OWNERSHIP`) must keep failing closed on save conflict; load miss is already treated as absent.
|
|
173
|
+
|
|
174
|
+
### Gate D — OFFSET pagination (out of 080)
|
|
175
|
+
|
|
176
|
+
Checkpoint list cursors are numeric offsets. Concurrent writes make a page walk incomplete. 079 absorbed this (resolve explicit keys; do not trust scan completeness). Not a channel follow-on.
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## 6. Threat-model resolution (follow-ons)
|
|
181
|
+
|
|
182
|
+
| Threat | Control | Task |
|
|
183
|
+
| --- | --- | --- |
|
|
184
|
+
| Group impersonation (display name, username, chat title) | `externalActorId` = numeric `from.id` only; title/username never identity | 4 |
|
|
185
|
+
| “Anyone in this group” grant | Deny-by-default; grant is exact `(connectionId, chat, thread?, actor)` | 4 |
|
|
186
|
+
| Anonymous admin / missing `from` | Drop the update | 4 |
|
|
187
|
+
| Forwarded message as the sender | `claims` only; default no model | 4 |
|
|
188
|
+
| Topic bleed (same chat, different `message_thread_id`) | `threadId` in `deriveSessionId` and `actorKey` (`runtime.ts:175-177,359-376`) | 4 (already keyed) |
|
|
189
|
+
| Model-selected destination / `file_id` / URL | Destination from the bound event only; fetch only the current event’s ref | 5–7 |
|
|
190
|
+
| Draft treated as durable / sent as the final | Drafts out of band; journal `final`\|`notice` only; must `sendMessage` after | 5 |
|
|
191
|
+
| Draft leak of secrets | Same `SecretRedactor` as finals; no controls/attachments on drafts | 5 |
|
|
192
|
+
| Journal media / disk retention of voice | Refs on the event; bytes live only for the fetch+transcribe/extract call | 6 |
|
|
193
|
+
| Oversized getFile (20 MB) | Channel cap 1 MiB / hard 4 MiB; Content-Length + bounded reader | 6 |
|
|
194
|
+
| Token in `api.telegram.org/file/bot<token>/…` | Existing URL redaction; credential provider functions, not static strings | 6 |
|
|
195
|
+
| Notify as broadcast / Signal bulk | Unicast to an existing binding; `notifications: true` re-checked; no fan-out API | 7 |
|
|
196
|
+
| Notify without a binding / revoked identity | Denied | 7 |
|
|
197
|
+
| Outbox dumps chat text into ERP | Payload = correlation ids; tenant from identity ownership | 8 |
|
|
198
|
+
| Signal terms / GPLv3 | DM-only, experimental, socket client, operator gate unchanged | — |
|
|
199
|
+
| Foreign checkpoint existence | Task 3 miss semantics | 3 |
|
|
200
|
+
|
|
201
|
+
Deny-by-default remains the recommended default for **groups** and **notify**.
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## 7. Task 9 budget baseline (no runtime cost this task)
|
|
206
|
+
|
|
207
|
+
Recorded 2026-09-16 from `scripts/budgets.json` (do not bump in Task 1):
|
|
208
|
+
|
|
209
|
+
| Package | Export ceiling | Notes |
|
|
210
|
+
| --- | --- | --- |
|
|
211
|
+
| `@arnilo/prism-channels` | **80** | Root + `/telegram` + `/signal` src-export count |
|
|
212
|
+
| `@arnilo/prism-core` | **1473** | Unchanged by 079 extraction |
|
|
213
|
+
| Root packedBytes / unpackedBytes / fileCount | **1247731** / **4137527** / **489** (5% tolerance) | 079 Task 1 rebaseline |
|
|
214
|
+
|
|
215
|
+
This history page and two probes are excluded from published tarballs (`dist/**/__tests__` deny; history is docs). Task 9 rebaselines only with a recorded reason if measured exports grow (notify, attachment refs, Telegram options).
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## 8. Evidence and reproduction
|
|
220
|
+
|
|
221
|
+
```sh
|
|
222
|
+
npm run build --workspace @arnilo/prism-core --workspace @arnilo/prism-channels
|
|
223
|
+
node --test "packages/prism-core/dist/__tests__/messaging-primitives.test.js"
|
|
224
|
+
node --test "packages/prism-channels/dist/__tests__/runtime.test.js"
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
080 probes (in addition to the seven 079 proofs):
|
|
228
|
+
|
|
229
|
+
1. `threadId` on two otherwise identical events yields two session ids (`deriveSessionId` already hashes `threadId`).
|
|
230
|
+
2. Non-stream `lifecycle.resume` with a live abort during a hanging provider turn does **not** abort the provider; the run succeeds (Task 3 gap).
|
|
231
|
+
3. Memory and SQLite `loadCheckpoint` of a foreign-owned existing key throw `/ownership mismatch/` (Task 3 gap).
|
|
232
|
+
4. `ChannelReply["kind"]` extracts no `"draft"` variant.
|
|
233
|
+
|
|
234
|
+
Core changes proposed by later tasks: glob quoting (Task 2); `signal` on `AgentRunResumeOptions` + checkpoint miss (Task 3). Everything else is `@arnilo/prism-channels` fields, Telegram filter widening, examples, and the 0.8.0 cut.
|