Compare commits

...

1 Commits

Author SHA1 Message Date
zhi
959e635bb5 fix: 800ms delay before first inbound notification
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.
2026-05-14 14:00:36 +00:00

View File

@@ -98,6 +98,7 @@ mcp.setRequestHandler(CallToolRequestSchema, async req => {
let ws: WebSocket | null = null let ws: WebSocket | null = null
let backoff = 1000 let backoff = 1000
const outbox: unknown[] = [] const outbox: unknown[] = []
let firstInboundDelivered = false
function bridgeSend(frame: unknown): void { function bridgeSend(frame: unknown): void {
if (ws && ws.readyState === WebSocket.OPEN) { if (ws && ws.readyState === WebSocket.OPEN) {
@@ -147,6 +148,16 @@ async function handleBridge(frame: any): Promise<void> {
log(`hello_ack received`) log(`hello_ack received`)
return return
case 'inbound': 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({ await mcp.notification({
method: 'notifications/claude/channel', method: 'notifications/claude/channel',
params: { params: {