Skip to content
CompozyOS RuntimeBridges

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
}
FieldMeaningCommon use
include_peerInclude the platform peer identity in the route key.Direct conversations such as Slack DMs, Discord DMs, and Telegram private chats.
include_threadInclude the platform thread identity in the route key.Slack thread timestamp, Discord thread channel ID, Telegram forum topic ID.
include_groupInclude 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:

  • scope
  • workspace_id
  • bridge_instance_id
  • peer_id, if include_peer is true
  • thread_id, if include_thread is true
  • group_id, if include_group is 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.

PolicyMessage dimensionsSession behavior
Peer onlyTelegram direct chat IDOne session per direct conversation.
Group onlySlack channel ID or Discord channel IDOne session per shared channel.
Group and threadSlack channel ID plus thread timestampOne session per thread in a shared channel.
Group and threadTelegram group ID plus forum topic IDOne 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 eventpeer_idgroup_idthread_id
Slack direct messageSlack conversation IDSlack thread_ts when present.
Slack channel messageSlack channel IDthread_ts, or the message ts for a top-level channel message.
Discord direct messageDiscord DM channel ID
Discord server channel messageDiscord channel ID
Discord thread messageParent/channel IDDiscord thread channel ID.
Telegram private chatTelegram chat IDTelegram message thread ID when present.
Telegram group or supergroupTelegram chat IDForum topic ID; forum general topic normalizes to 1.
WhatsApp direct messageSender wa_id
Teams personal conversationBase Bot Framework conversation IDEncoded conversation and service URL.
Teams channel or group chatBase Bot Framework conversation IDEncoded conversation and service URL.
Google Chat direct messageSpace resource name, spaces/...Encoded Chat target; always present for a DM.
Google Chat shared spaceSpace resource name, spaces/...Encoded native thread or derived message target when present.
GitHub issue or review commentRepository owner/nameEncoded issue or review reference.
Linear comment or Agent SessionLinear issue IDEncoded 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:

  1. If an active route already exists, Compozy submits the inbound content to that route's session.
  2. If the route exists but the session is missing or inactive, Compozy creates a replacement session and updates the route.
  3. 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 support

The 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:

FieldNotes
peer_idPlatform direct conversation identity.
thread_idOptional thread identity; requires peer_id or group_id.
group_idPlatform group, guild, or forum container identity.
modedirect-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 reply

The 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:

  1. Exact canonical route.
  2. Exact normalized display name. Normalization lowercases text and strips leading # or @.
  3. Exact qualifier/name, where the qualifier is the provider-reported namespace such as a workspace, guild, team, or room family.
  4. 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 json

The API surfaces are:

RoutePurpose
GET /api/bridges/:id/targetsList daemon-owned targets, freshness, cache-stale state, and total.
POST /api/bridges/:id/resolveResolve 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:

EventPurpose
startBegin a response delivery stream.
deltaAppend streamed text.
finalMark the response complete.
errorMark the response failed.
resumeSend a snapshot after a pre-commit transport/adapter failure or queue coalescing.
deleteRemove 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

FailureRuntime behavior
Instance disabledIngest is rejected as unavailable.
Status starting, auth_required, or errorIngest is rejected until the provider reports a usable state.
Status ready or degradedIngest is accepted. Degradation metadata remains visible in health output.
Secret binding is missing, invalid, or points to an empty env varThe provider reports degraded, auth-required, or error state depending on the adapter classification.
Webhook signature or token validation failsThe adapter rejects the platform request before calling Compozy ingest.
Provider config has duplicate webhook paths for the same adapter processThe adapter marks the affected instance configuration invalid.
Routed session remains busyHost 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 failsThe broker records the failure and can retry with a resume snapshot.
A received delivery acknowledgement is missing, malformed, or invalidThe 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 materializedThe broker records committed_result_unavailable, closes the delivery, and does not replay it.
Progress mutation commits but its result cannot be materializedThe 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.

On this page