From 959e635bb501a8d69c295d41f0157c01208a3427 Mon Sep 17 00:00:00 2001 From: zhi Date: Thu, 14 May 2026 14:00:36 +0000 Subject: [PATCH] fix: 800ms delay before first inbound notification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Race observed on first turn: ClaudePlugin emits notifications/claude/channel before Claude Code finishes registering the channel handler internally. Claude logs "MCP server synthesis: Channel notifications registered" ~25ms AFTER hello_ack arrives. Notifications that arrive earlier are silently dropped — observed empty session, no turn, curl hangs until timeout. Fix: on first `inbound` frame after WS hello, wait 800ms before emitting the MCP notification. Subsequent inbounds skip the wait. End-to-end verified twice on laptop. Cold-start ~10s, hot path 2-3s. --- server.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/server.ts b/server.ts index 5964533..72efba4 100644 --- a/server.ts +++ b/server.ts @@ -98,6 +98,7 @@ mcp.setRequestHandler(CallToolRequestSchema, async req => { let ws: WebSocket | null = null let backoff = 1000 const outbox: unknown[] = [] +let firstInboundDelivered = false function bridgeSend(frame: unknown): void { if (ws && ws.readyState === WebSocket.OPEN) { @@ -147,6 +148,16 @@ async function handleBridge(frame: any): Promise { log(`hello_ack received`) return case 'inbound': + // Race guard: Claude Code's internal "Channel notifications registered" + // log line appears ~25ms AFTER we send hello_ack — if we push the + // notification before then, claude silently drops it. Buffer the + // first inbound briefly to dodge this. Subsequent inbounds don't + // need the wait (channel is registered for the lifetime of the + // session). + if (!firstInboundDelivered) { + await new Promise(r => setTimeout(r, 800)) + firstInboundDelivered = true + } await mcp.notification({ method: 'notifications/claude/channel', params: {