Troubleshooting
Troubleshooting
BridgeClient never fires connect
Most common causes, in order:
- Wrong path — the server expects
/__bridge_socket/by default. If you configured a reverse proxy, ensure it forwards that path unmodified. - Transport mismatch — some proxies strip WebSocket upgrade headers. Try
transports: ['polling']to narrow the cause. - 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 skipsPendingOutbound.register). Every POST to bridge must register a pending token before the request. - Restart replays: duplicates on replay mean the
message_mapstore lost the record. Check thatAdapterStore.messages.insertis being called after every successfulpostMessage.
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++beforepnpm install. - Raspberry Pi / ARM: use Node 20 LTS with official prebuilt binaries.
- Bring-your-own store: implement
AdapterStoreyourself 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:
- The bridge is actually broadcasting an empty title — check server logs.
- You set
guardAgainstEmptyIncomingTitle: false— don't, unless you control both ends. - 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 PlatformAdapter — TurnStreamer will
pick that path automatically and skip the throttled send+edit fallback.