Guides

Troubleshooting

Common issues when building and running a bridge adapter — reconnect storms, missing events, duplicate messages, SQLite errors.

Troubleshooting

BridgeClient never fires connect

Most common causes, in order:

  1. Wrong path — the server expects /__bridge_socket/ by default. If you configured a reverse proxy, ensure it forwards that path unmodified.
  2. Transport mismatch — some proxies strip WebSocket upgrade headers. Try transports: ['polling'] to narrow the cause.
  3. CORS / origin — browser environments may be blocked. Server logs will show the rejected origin.

Add a debug logger to see handshake attempts:

const client = new BridgeClient({
  url,
  logger: {
    debug: (m, meta) => console.debug(m, meta),
    info:  (m, meta) => console.info(m, meta),
    warn:  (m, meta) => console.warn(m, meta),
    error: (m, meta) => console.error(m, meta),
  },
})

Events stop arriving mid-session

Check that you called client.join(conversationId) — without joining the room, only sessions:list and server:status are delivered. On every connect, BridgeClient re-emits session:join for every joined room automatically, so manual re-join after reconnect is not needed.

If you see the client stuck in a reconnect loop, it's usually a connect_error — subscribe to that event and log the err.message.

AbortError / BridgeTimeoutError in REST calls

When you configure timeoutMs on BridgeApi (or per-call), exceeded timeouts throw BridgeTimeoutError. Long-poll endpoints like getMessageResult accept their own timeoutMs that is interpreted server-side — set the client BridgeApiOptions.timeoutMs at least as high, otherwise you'll cancel before the server had a chance to respond.

Duplicate messages posted to platform

Symptoms: each user message appears twice (once as echo), or tool cards duplicate on restart.

  • Echo fan-out: ensure you're running the runtime's PlatformToBridge (not a custom implementation that skips PendingOutbound.register). Every POST to bridge must register a pending token before the request.
  • Restart replays: duplicates on replay mean the message_map store lost the record. Check that AdapterStore.messages.insert is being called after every successful postMessage.

better-sqlite3 fails to load

better-sqlite3 is a native module. On environments where a prebuilt binary isn't available, it falls back to compiling from source and needs a C++ toolchain plus python3. Common fixes:

  • Alpine-based Docker: apk add --no-cache python3 make g++ before pnpm install.
  • Raspberry Pi / ARM: use Node 20 LTS with official prebuilt binaries.
  • Bring-your-own store: implement AdapterStore yourself and skip SQLite entirely. The interface is small (4 repos × ~5 methods each).

Titles flicker / revert to raw conversationId

The Reconciler has a guard: empty incoming titles from sessions:list are ignored (guardAgainstEmptyIncomingTitle: true by default). If you see a rename back to the raw ID, one of these is wrong:

  1. The bridge is actually broadcasting an empty title — check server logs.
  2. You set guardAgainstEmptyIncomingTitle: false — don't, unless you control both ends.
  3. The title was mis-set on startup; call runtime.reconciler.repairTitles() manually or wait for the next scheduled repair.

Stream text arrives in one big chunk instead of incrementally

The TurnStreamer throttles edits via maxStreamEditsPerSecond (default 1). Raise it on platforms with generous rate limits (Discord 5/sec is fine), keep it low on Telegram to avoid 429 Too Many Requests. Tune via bridgeToPlatform.maxStreamEditsPerSecond in your AdapterConfig.

For platforms with native streaming, implement supportsNativeStreaming: true and streamDraft(...) on your PlatformAdapterTurnStreamer will pick that path automatically and skip the throttled send+edit fallback.