> ## Documentation Index
> Fetch the complete documentation index at: https://docs.formation.texturehq.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Application streaming

> Signed snapshots and resumable public conversation events.

Formation's generic connector exposes a durable application feed. The agent
continues working in Temporal when an application disconnects. Postgres owns
both the materialized public conversation and its ordered event journal.

Read [the design rationale](https://docs.formation.texturehq.com/design/application-streaming.md) for the philosophy,
privacy boundaries, and ELI5 explanation.

## Opt in per connection

New and existing connections default to buffered answers. In the operator UI,
open an agent's connection and enable **Stream answers**, or use the existing
operator session/API-token authorization with:

```http theme={null}
PUT /api/agents/{slug}/connectors/{connector_id}/streaming
Content-Type: application/json

{"enabled":true}
```

Slack and Teams connections also accept `progress_enabled` (boolean) and
`scope` (`all`, `chats`, or `direct`) in the same request. Progress is independent
of answer streaming and preserves existing enabled behavior by default. Scope
applies to both features; final replies are unaffected. Omitted options retain
their saved values, so existing clients remain compatible. These advanced options
are not accepted for other connector types.

The response contains `connector.streaming_enabled`, `connector.progress_enabled`,
`connector.streaming_scope`, and `status` (`active` or
`saved_restart_required`). Only operators with access to the owning agent can
change its Slack, Telegram, or generic installation. Settings survive credential
rotation and connection edits. Disabling applies to new turns after reload;
it does not cancel an in-flight turn. A restart is required when reload fails.

Opt-in enables the public-answer handoff and incremental inference. With it off,
existing model requests and whole-message delivery remain buffered. Generic
snapshot/replay still provides canonical messages and authorized progress;
webhook payloads and whole-response delivery do not change.

### Native messaging

Slack uses `chat.startStream`, `chat.appendStream`, and `chat.stopStream` with
that installation's bot credentials (`chat:write`). The validated final first
part updates the same Slack message; further parts use normal delivery. Drafts
are provisional and may be corrected. Public-channel streams include the
requesting user and installation's workspace as recipient context.

Telegram uses `sendMessageDraft` in private chats only. It sends plain-text
previews, then the existing formatted `sendMessage` path persists the final
answer. Group replies stay buffered. No draft stop/cancellation button is exposed.

Both transports coalesce updates to approximately one every three seconds,
with a final flush. If a draft exceeds the provider's limit or its API rejects an update,
further previews stop and the canonical reply path remains authoritative.
Failed model attempts remove known Slack previews or clear Telegram drafts
best-effort; retries do not append a different generation onto the old preview.

Native APIs do not provide the SSE journal's replay guarantees. Formation reserves
creation and append before sending and retains known message IDs across worker
restarts. It never blindly repeats an uncertain Slack creation or append. If
Slack accepts a creation but its response or the local ID commit is lost, a
partial preview can remain alongside the eventual answer; Slack expires the
stream after ten minutes. Telegram drafts expire automatically. Lost ownership
prevents cleanup by the old worker. Operators should test with a dedicated
connection before enabling this on an existing conversation.

The native transport's saved preview text follows the configured run-ledger
body retention window after its run becomes terminal. Active and recent runs
retain that text; a zero window keeps it. Delivery identity and uncertain-send
reservation metadata remain available for reconciliation until the run is deleted.

Official API contracts: [Slack streaming](https://docs.slack.dev/reference/methods/chat.startStream/)
and [Telegram drafts](https://core.telegram.org/bots/api#sendmessagedraft).

## Authentication and commands

Use the generic connector's existing `X-Mesh-Install`, `X-Mesh-Timestamp`, and
`X-Mesh-Signature` headers. Sign the exact POST body with the installation's
secret using the normal `v1:in` construction. The body binds the operation,
conversation, actor, and replay cursor. Keep signing secrets in the application's
backend; never ship them to a browser or mobile app.

The application backend derives `actor_id` from its authenticated user. It must
not accept a client-supplied actor or installation as authority. Formation checks
that this is a private conversation for that actor, in that installation and
agent's perspective. Mixed or unverifiable audiences do not expose a feed.
Current source disclosure is rechecked for snapshots and each replay page,
including previously delivered progress. Disabled agents and rotated or removed
connections stop active subscriptions.

Send messages through `POST /generic/events` with stable `message_id` values for
idempotency. Subscribing does not submit a command, cancel a run, approve a tool,
or start a model call. This protocol does not add cancellation, attachment, or
approval command APIs; applications must not advertise those capabilities based
on streaming support alone.

## Snapshot

`POST /generic/conversations/snapshot`:

```json theme={null}
{"operation":"snapshot_conversation","conversation_key":"support-42","actor_id":"reader-7"}
```

The response is an application snapshot:

```json theme={null}
{
  "conversationId":"support-42",
  "sequence":12,
  "messages":[{
    "id":"reply:question-1",
    "actor":{"id":"agent-id","kind":"agent","name":"Assistant"},
    "status":"complete",
    "parts":[{"id":"text","kind":"text","text":"The result is ready.","format":"markdown","streaming":false}]
  }],
  "runs":[{"id":"run-id","status":"completed"}],
  "inputs":[]
}
```

The sequence and state come from one repeatable database snapshot. Start replay
**after** that sequence. An unknown or inaccessible conversation may return an
empty snapshot; it never exposes another actor's state. A new message becomes
visible when durable intake captures it into the conversation graph.

## Subscription and replay

`POST /generic/conversations/stream`:

```json theme={null}
{"operation":"stream_conversation","conversation_key":"support-42","actor_id":"reader-7","after_sequence":12}
```

The response uses `Content-Type: text/event-stream`, `Cache-Control: private,
no-store`, and `X-Accel-Buffering: no`. Disable buffering in any application proxy.
Use a streaming HTTP client rather than a short JSON-request timeout.

```text theme={null}
event: formation.event
id: 13
data: {"version":1,"conversationId":"support-42","eventId":"event-uuid","sequence":13,"data":{"type":"text.delta","messageId":"reply:question-1","partId":"text","delta":"Hello"}}

```

`id` is the decimal `sequence`, never the event UUID. Numbers stay within the
JavaScript safe-integer range. If sending `Last-Event-ID`, it must exactly match
`after_sequence` in the newly signed request. The server does not trust an
unsigned header to change the read scope or cursor.

The current event types are:

| Event | Reducer behavior |
| - | - |
| `message.upsert` | Replace the entire message by ID, preserving its position. |
| `text.delta` | Append to an existing text part. |
| `run.upsert` | Replace the run's state by ID. |

A streaming message carries `status: streaming`, a text part with
`streaming: true`, its `runId`, and an `attemptId`. A new model attempt replaces
the draft with an empty `message.upsert` and a different attempt ID. Never
concatenate text from different attempts. A validated canonical reply replaces
the draft at the same message ID and marks it complete. An interrupted or failed
terminal run marks any remaining draft failed. Draft text is provisional;
replacing it cannot undo what a person has already seen.

Incoming IDs are `message:<message_id>`, replies are `reply:<parent_message_id>`,
and approved progress has its own `progress:<event_id>`. Messages are ordered by
their first appearance; replacing a draft does not move it to the bottom.

Run states are `queued`, `running`, `waiting_input`, `completed`, `failed`, and
`cancelled`. Socket EOF does **not** mean a run completed. Preserve the last
successfully applied sequence and reconnect with a fresh signature. Replay can
include later messages and runs in the same conversation after a run completes.

Subscriptions last at most two minutes, with comment heartbeats every fifteen
seconds and five-second write deadlines. A process admits at most 128 concurrent
subscriptions, with a default of 16 per installation (configure
`MESH_GENERIC_MAX_STREAMS_PER_INSTALL`). Slots are released when subscriptions
close. Reconnect with bounded jittered backoff after transport failure or
429/503; authentication and authorization errors require fixing access. An
application backend must tie its proxy subscription to the client's connection,
while keeping command execution independent of that connection.

The journal retains roughly 4,096 events per conversation, pruning in batches of
256\. A cursor below the retained floor or ahead of the current watermark receives
HTTP 410 before streaming starts, without an SSE control frame. On HTTP 410, fetch
a new signed snapshot, replace local state, and reconnect with that snapshot's
sequence; do not retry the expired cursor. If retention overtakes an open
subscriber, the server emits `event: resnapshot` with `data: {}` and closes. This is a transport
control event, not an application event: fetch a new snapshot and replace local
state before resuming. Never advance the cursor across a gap or feed this control
frame into a domain-event reducer.

## What streams

The engine offers `begin_answer` on eligible application turns. The agent calls
it alone once work and verification are finished. Its normal tool receipt makes
the handoff recoverable; the next inference has no tools and emits public answer
text. This adds an explicit model decision boundary rather than exposing private
assistant commentary that may precede a tool call. If a model answers directly
without the handoff, the validated answer still arrives as a complete upsert.

OpenRouter, OpenAI-compatible Chat Completions, and the Responses adapter decode
incrementally through the existing preparation, routing, audit, retry, and usage
paths. Native reasoning fields and tool arguments never become answer deltas.
Provider truncation is an error even when the transport returns EOF cleanly.
Final usage remains provider-reported, including usage received before failure.

Control-envelope continuations and unsupported text tool protocols retain final
validation. The incremental reply guard holds text from the first brace, angle
bracket, or backtick until final validation, so a split JSON envelope, think tag,
or code fence cannot leak control syntax. Ordinary prose before that boundary
can stream; code and structured answers settle through the canonical upsert.
Approved `report_progress` notices also appear in the feed. Raw tool execution
logs, reasoning, arguments, and internal activity labels remain private.

## Storage and deployment

The forward migration installs a projection and journal plus transactional hooks
for conversation messages, run status, and authorized progress. It backfills
existing generic conversation messages and run states. Test migration time on a
representative database before upgrading a large installation.

Streaming writes use the existing execution ownership fence and current source
policy in the publication transaction. Public state and event sequence commit
atomically. Concurrent writers serialize on the conversation projection row;
rolled-back writes cannot leave a cursor hole.

Readers poll committed pages at 250 ms intervals. This works across API replicas
and with Formation's transaction-pool database configuration without persistent
LISTEN connections or a new broker. Replay pages contain at most 128 events and normally at most 256 KiB (a larger
single canonical message is returned alone). A slow consumer reconnects from
durable history. The retained snapshot and
provenance remain durable after event pruning. Future notification fan-out can
reduce read traffic without changing the replay contract.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.