Bridge Routing
How bridge routing policies map platform messages to Compozy sessions and delivery targets.
- Audience
- Operators running durable agent work
- Focus
- Bridges guidance shaped for scanability, day-two clarity, and operator context.
Bridge routing is the boundary between platform identity and Compozy session identity. An adapter sends Compozy a normalized inbound envelope with platform dimensions such as peer, group, and thread. Compozy then uses the bridge instance routing policy to decide which dimensions belong to the canonical route key.
The result is a durable bridge route: one platform conversation identity mapped to one Compozy session. See Bridge conversations for the conceptual model behind platform conversations, routes, and session history. This page defines the routing and delivery contracts.
Routing policy
Every bridge instance carries this policy:
{
"include_peer": false,
"include_thread": true,
"include_group": true
}| Field | Meaning | Common use |
|---|---|---|
include_peer | Include the platform peer identity in the route key. | Direct conversations such as Slack DMs, Discord DMs, and Telegram private chats. |
include_thread | Include the platform thread identity in the route key. | Slack thread timestamp, Discord thread channel ID, Telegram forum topic ID. |
include_group | Include the platform group identity in the route key. | Shared containers such as Slack channels, Discord channels, Telegram groups, or Telegram supergroups. |
include_thread cannot be the only enabled dimension. A thread must be anchored by either a peer or
a group, otherwise Compozy rejects the instance configuration with bridges: routing policy cannot include thread without peer or group.
Every enabled routing dimension is required on inbound events. That means one bridge instance usually
targets one conversation shape: direct messages use peer_id, shared channels use group_id, and
threaded channel or forum flows use group_id plus thread_id. If you need both DMs and channel
traffic, create separate bridge instances with different policies.
Route key construction
When a provider calls bridges/messages/ingest, Compozy builds a key from:
scopeworkspace_idbridge_instance_idpeer_id, ifinclude_peeris truethread_id, ifinclude_threadis truegroup_id, ifinclude_groupis true
The key is serialized in a stable JSON shape and hashed with SHA-256. The hash is stored with the route and is what lets Compozy reuse the same session for the next message in the same routed conversation.
| Policy | Message dimensions | Session behavior |
|---|---|---|
| Peer only | Telegram direct chat ID | One session per direct conversation. |
| Group only | Slack channel ID or Discord channel ID | One session per shared channel. |
| Group and thread | Slack channel ID plus thread timestamp | One session per thread in a shared channel. |
| Group and thread | Telegram group ID plus forum topic ID | One session per forum topic. |
Choose dimensions that the provider emits for the events you care about. For example, a policy that
requires thread_id will reject inbound events that do not provide a thread identity, even when the
same bridge is otherwise healthy.
Platform dimension mapping
The adapters normalize platform-specific IDs into the same three fields:
| Platform event | peer_id | group_id | thread_id |
|---|---|---|---|
| Slack direct message | Slack conversation ID | — | Slack thread_ts when present. |
| Slack channel message | — | Slack channel ID | thread_ts, or the message ts for a top-level channel message. |
| Discord direct message | Discord DM channel ID | — | — |
| Discord server channel message | — | Discord channel ID | — |
| Discord thread message | — | Parent/channel ID | Discord thread channel ID. |
| Telegram private chat | Telegram chat ID | — | Telegram message thread ID when present. |
| Telegram group or supergroup | — | Telegram chat ID | Forum topic ID; forum general topic normalizes to 1. |
| WhatsApp direct message | Sender wa_id | — | — |
| Teams personal conversation | Base Bot Framework conversation ID | — | Encoded conversation and service URL. |
| Teams channel or group chat | — | Base Bot Framework conversation ID | Encoded conversation and service URL. |
| Google Chat direct message | Space resource name, spaces/... | — | Encoded Chat target; always present for a DM. |
| Google Chat shared space | — | Space resource name, spaces/... | Encoded native thread or derived message target when present. |
| GitHub issue or review comment | — | Repository owner/name | Encoded issue or review reference. |
| Linear comment or Agent Session | — | Linear issue ID | Encoded root-comment or Agent Session reference. |
There is no explicit channel-to-agent mapping field on a bridge instance. New route sessions use the workspace default agent, and route separation comes from these dimensions.
Session resolution
After the route key is built, Compozy follows one of three paths:
- If an active route already exists, Compozy submits the inbound content to that route's session.
- If the route exists but the session is missing or inactive, Compozy creates a replacement session and updates the route.
- If no route exists, Compozy creates a new workspace session, stores the route, and submits the inbound content.
An active route keeps reusing its session. A missing or inactive session does not make the route disappear: the next accepted inbound event creates a replacement session and rebinds the same route key. The previous session and its events remain historical runtime records. To replace one routed session intentionally, follow Start a fresh routed session.
New bridge sessions use the workspace default agent. Set the workspace default before enabling a bridge if the bridge should talk to a specific agent:
compozy workspace add /Users/you/src/support-bot \
--name support-bot \
--default-agent supportThe inbound session path requires a workspace ID on the bridge instance. A global bridge instance can exist, but it cannot currently create a new Compozy session from inbound messages because there is no workspace to resolve.
When the routed session is busy
Bridge ingress makes up to three local submission attempts when an active routed session reports that a prompt is already in progress. Between attempts, Compozy watches that session for an availability window of about five seconds. If the session remains busy, the Host returns the prompt-in-progress error to the adapter.
This path does not create a replacement session, queue the bridge message, interrupt the active turn,
or steer it into the current prompt. The queue, interrupt, and steer modes available to explicit
session prompt operations are not selected automatically for bridge ingress.
The callback result is adapter-specific. A synchronous adapter can expose the Host failure to the platform. Discord interactions instead return a deferred acknowledgement before asynchronous Host ingestion. An adapter with positive inbound batching acknowledges after placing the event in an in-memory batch; a later flush failure cannot reach the platform, and the pending batch is not durable across adapter restarts. Compozy has no shared durable queue or automatic readmission path behind either early acknowledgement.
Inbound envelope
The provider sends Compozy the normalized envelope, not the raw platform webhook payload.
{
"bridge_instance_id": "brg_123",
"scope": "workspace",
"workspace_id": "ws_8f33a913d23c4fd1",
"group_id": "C0123456789",
"thread_id": "1713200000.000100",
"platform_message_id": "1713200000.000100",
"received_at": "2026-04-16T14:30:00Z",
"sender": {
"id": "U0123456789",
"username": "maya",
"display_name": "Maya"
},
"content": {
"text": "Check the failing deployment."
},
"event_family": "message",
"idempotency_key": "slack:brg_123:1713200000.000100"
}Supported event families are message, edit, command, action, and reaction. Each family has
its own typed payload when the platform event is not just text. An edit identifies the affected
platform message, carries an updated or deleted operation, and includes the replacement text and
original timestamp when the provider supplies them. Attachments are normalized as metadata with ID,
name, MIME type, and URL.
Slack and Telegram normalize supported message edits into this family. Discord's bundled HTTP
interactions adapter does not consume Gateway MESSAGE_UPDATE events, so ordinary Discord message
edits are not inbound events for that adapter.
When a provider has already observed the parent of a reply, it can add the parent's text and author to the envelope. Slack, Telegram, and Google Chat keep that context in bounded, instance- and conversation-scoped caches. An embedded parent snapshot takes precedence when the platform includes one. A cache miss leaves the optional reply fields empty; ingest never performs an on-demand provider fetch.
Compozy renders the envelope into a prompt that includes the event family, platform message ID, sender, peer, thread, group, body text, and attachment list. Edits and quoted reply context use distinct blocks so the current replacement text cannot be mistaken for historical content. That prompt is persisted in the session event store before it is handed to the agent.
Deduplication
Providers must send an idempotency_key. The daemon writes its durable deduplication record only
after the inbound prompt has been submitted successfully. The record remains active for 24 hours
after the later of received_at and daemon time. If the adapter submits the same event during that
window and the route still exists, Compozy suppresses duplicate prompt submission and returns the
existing session and routing key.
Adapters also have a separate, in-process deduplication cache. Its shared default TTL is five minutes, and several bundled adapters mark it before Host ingestion. That early mark has no per-event rollback: an immediate provider retry can be acknowledged and suppressed locally after a busy or failed Host submission. Restarting the adapter clears this local cache, but it also discards any pending in-memory inbound batches.
Deduplication is scoped to the bridge instance and idempotency key. The durable daemon record protects accepted prompts from repeated delivery; the adapter-local cache suppresses near-term callbacks. The combination is not a platform-wide exactly-once guarantee, and a platform acknowledgement is not proof of durable prompt admission.
Delivery targets
Inbound prompts register a delivery target with mode: "reply" so response events go back to the
platform conversation that produced the prompt.
For explicit outbound delivery checks, Compozy can also resolve a target from bridge defaults plus request overrides:
{
"delivery_defaults": {
"group_id": "C0123456789",
"thread_id": "1713200000.000100",
"mode": "direct-send"
}
}Allowed target fields are:
| Field | Notes |
|---|---|
peer_id | Platform direct conversation identity. |
thread_id | Optional thread identity; requires peer_id or group_id. |
group_id | Platform group, guild, or forum container identity. |
mode | direct-send or reply. Empty mode normalizes to direct-send for explicit target resolution. |
Use compozy bridge test-delivery to verify that a target resolves without sending a live message:
compozy bridge test-delivery brg_123 \
--message "routing smoke test" \
--group-id C0123456789 \
--thread-id 1713200000.000100 \
--mode replyThe HTTP endpoint for the same dry run is POST /api/bridges/:id/test-delivery.
Friendly target resolver
The bridge target directory lets operators resolve bridge targets by stable provider identity or by the names adapters report. Resolution is read-only; it never sends a message.
Adapters can report target snapshots from the platform. When an adapter does not implement native target enumeration, Compozy falls back to the operator-declared delivery defaults for that bridge instance. The daemon owns validation, persistence, and freshness either way. Rows are keyed by canonical provider route, refresh on bridge connect and periodically while the adapter is connected, and remain in the directory when a later snapshot omits them so operators can distinguish stale cache from deletion.
For v1, this fallback is the contract for bundled Slack, Discord, and Telegram target directories; platform API enumeration can be added by an adapter later without changing routing keys.
The resolver checks in this order:
- Exact canonical route.
- Exact normalized display name. Normalization lowercases text and strips leading
#or@. - Exact
qualifier/name, where the qualifier is the provider-reported namespace such as a workspace, guild, team, or room family. - Prefix match.
Ambiguous matches return target_ambiguous with candidate targets. Unknown names return
target_unknown. Compozy does not pick a winner for an ambiguous friendly name.
List the current directory and resolve names from the CLI:
compozy bridge targets brg_123 --query launch -o json
compozy bridge resolve brg_123 "northstar/launch room" -o jsonThe API surfaces are:
| Route | Purpose |
|---|---|
GET /api/bridges/:id/targets | List daemon-owned targets, freshness, cache-stale state, and total. |
POST /api/bridges/:id/resolve | Resolve one target name and return match, step, candidates, or diagnostic. |
Delivery guarantees
Compozy preserves order per route. The delivery broker runs workers by route, projects session events into delivery events, and asks the adapter to acknowledge each delivery request.
The event types are:
| Event | Purpose |
|---|---|
start | Begin a response delivery stream. |
delta | Append streamed text. |
final | Mark the response complete. |
error | Mark the response failed. |
resume | Send a snapshot after a pre-commit transport/adapter failure or queue coalescing. |
delete | Remove a previously delivered platform message when the agent output is deleted. |
Before a provider mutation starts, retryable transport or adapter failures can be resumed with a snapshot. Slow adapters get coalesced deltas so the route stays bounded; under pressure, queued deltas may be dropped before a later resume/final snapshot catches the platform up. The default route queue capacity is small by design, and the request timeout is internal to the broker. These are not user-facing bridge configuration knobs.
committed_result_unavailable is a different acknowledgement outcome. It means the mutation may have
crossed the provider commit boundary, but the adapter could not reliably read or validate the response
or recover the required remote message identity. The broker closes that delivery as a terminal error
without a resume or replay, and it never fabricates a remote message ID. This avoids repeating a
mutation that may already be visible at the provider.
When this outcome belongs only to a progress update, Compozy drops that indeterminate progress event under
progress_delivery_indeterminate. It does not close the response's text delivery, so a later final
text event can still continue through the normal delivery path.
Compozy keeps a durable checkpoint ledger for in-flight response delivery. It records route ownership,
sent and acknowledged sequence numbers, remote-message identity, and terminal transitions. Before a
durable start, resume, final, error, or delete mutation reaches the provider, the broker
writes its sequence as sent. A successful provider acknowledgement advances the acknowledged
sequence. A committed_result_unavailable acknowledgement instead terminalizes the delivery while
leaving the acknowledged sequence unchanged. Streamed delta text and tool-progress payloads are not
stored in this ledger. Rows and durable metrics carry direct scope and workspace ownership, so startup
reconciliation never lists or handles another workspace's delivery.
At daemon startup, Compozy reconciles unfinished rows before the broker admits new registrations or
inbound prompt side effects. A row whose sent sequence is ahead of its acknowledged sequence may have
committed remotely before the daemon stopped. Compozy terminalizes that row locally as indeterminate and
does not call the provider again. When no unmatched send intent exists, the checkpoint-only ledger
still has no response text to reconstruct, so reconciliation first writes a new intent and then
fails open as a visible terminal error post with the standard session-stopped message. When a live
session stops normally, Compozy uses the same write-ahead rule for terminal error delivery before the
bridge notifier finishes the stop path.
Error handling
| Failure | Runtime behavior |
|---|---|
| Instance disabled | Ingest is rejected as unavailable. |
Status starting, auth_required, or error | Ingest is rejected until the provider reports a usable state. |
Status ready or degraded | Ingest is accepted. Degradation metadata remains visible in health output. |
| Secret binding is missing, invalid, or points to an empty env var | The provider reports degraded, auth-required, or error state depending on the adapter classification. |
| Webhook signature or token validation fails | The adapter rejects the platform request before calling Compozy ingest. |
| Provider config has duplicate webhook paths for the same adapter process | The adapter marks the affected instance configuration invalid. |
| Routed session remains busy | Host ingest returns prompt-in-progress after bounded local attempts. The daemon creates no queue or durable dedup record; an adapter-local mark may already suppress an immediate platform retry. |
| Pre-commit delivery transport or adapter call fails | The broker records the failure and can retry with a resume snapshot. |
| A received delivery acknowledgement is missing, malformed, or invalid | The manager returns committed_result_unavailable; the broker closes the delivery without replay because the provider commit outcome cannot be proven safe. |
| Provider mutation commits but its result cannot be materialized | The broker records committed_result_unavailable, closes the delivery, and does not replay it. |
| Progress mutation commits but its result cannot be materialized | The progress event is dropped; final text delivery remains eligible to continue. |
Bridge health fields are returned by compozy bridge list and the HTTP bridge responses. compozy bridge get reports the persisted instance and effective status but does not include the health metrics. For
live monitoring, subscribe to GET /api/bridges/health/stream with the required bridge_ids query
parameter. Pass the comma-separated IDs from the current catalog page (up to 200), and reuse that
page's scope and workspace_id query values. The stream emits an initial snapshot and then changes
only for that authorized, page-bounded set; loading another catalog page opens a stream for its IDs
instead of hydrating the full registry.
Related pages
- Bridges Overview explains where routing fits in platform ingest and delivery.
- Bridge CLI Reference lists the generated inspection and test-delivery commands.
- Session Events explains the event stream bridge delivery projects from.
- API Reference summarizes bridge HTTP route families.