From 8c6b0c0e07f6e57541f1f2ebb02a8b71b7433586 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Guti=C3=A9rrez?= <35082514+alezmad@users.noreply.github.com> Date: Mon, 6 Apr 2026 16:33:26 +0100 Subject: [PATCH] =?UTF-8?q?fix(cli):=20v0.5.2=20=E2=80=94=20poll-based=20p?= =?UTF-8?q?ush=20delivery=20(1s=20interval)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace WS onPush→notification with timer-based buffer drain. The old claude-intercom used 1s polling and worked reliably. WS async callbacks may not flush stdio properly for MCP notifications. Polling on a timer ensures consistent delivery. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/cli/package.json | 2 +- apps/cli/src/mcp/server.ts | 94 +++++++++++++++++++------------------- 2 files changed, 49 insertions(+), 47 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index 7b9f58d..2b8354e 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,6 +1,6 @@ { "name": "claudemesh-cli", - "version": "0.5.1", + "version": "0.5.2", "description": "Claude Code MCP client for claudemesh — peer mesh messaging between Claude sessions.", "keywords": [ "claude-code", diff --git a/apps/cli/src/mcp/server.ts b/apps/cli/src/mcp/server.ts index 1e5690e..6b18643 100644 --- a/apps/cli/src/mcp/server.ts +++ b/apps/cli/src/mcp/server.ts @@ -722,54 +722,56 @@ Your message mode is "${messageMode}". // any mesh's broker connection becomes a // system reminder injected into Claude Code's context. for (const client of allClients()) { - client.onPush(async (msg) => { - // In "off" mode, silently skip notification — messages are still - // buffered in pushBuffer and accessible via check_messages. - if (messageMode === "off") return; + // Poll-based push: drain pushBuffer every 1s and emit channel notifications. + // This is the proven approach from claude-intercom. The WS onPush handler + // fires instantly but server.notification() may not flush stdio reliably + // from an async WS callback. Polling on a timer ensures consistent delivery. + if (messageMode !== "off") { + const pushPollTimer = setInterval(async () => { + const buffered = client.drainPushBuffer(); + for (const msg of buffered) { + const fromPubkey = msg.senderPubkey || ""; + const fromName = fromPubkey + ? await resolvePeerName(client, fromPubkey) + : "unknown"; - const fromPubkey = msg.senderPubkey || ""; - // Resolve sender's display name from the cached peer list. - const fromName = fromPubkey - ? await resolvePeerName(client, fromPubkey) - : "unknown"; + if (messageMode === "inbox") { + try { + await server.notification({ + method: "notifications/claude/channel", + params: { + content: `[inbox] New message from ${fromName}. Use check_messages to read.`, + meta: { kind: "inbox_notification", from_name: fromName }, + }, + }); + } catch { /* best effort */ } + continue; + } - if (messageMode === "inbox") { - // Count-only notification, no content - try { - await server.notification({ - method: "notifications/claude/channel", - params: { - content: `[inbox] New message from ${fromName}. Use check_messages to read.`, - meta: { kind: "inbox_notification", from_name: fromName }, - }, - }); - } catch { /* best effort */ } - return; - } - - // push mode — full content notification - const content = msg.plaintext ?? decryptFailedWarning(fromPubkey); - try { - await server.notification({ - method: "notifications/claude/channel", - params: { - content, - meta: { - from_id: fromPubkey, - from_name: fromName, - mesh_slug: client.meshSlug, - mesh_id: client.meshId, - priority: msg.priority, - sent_at: msg.createdAt, - delivered_at: msg.receivedAt, - kind: msg.kind, - }, - }, - }); - } catch { - /* channel push is best-effort; check_messages is the fallback */ - } - }); + // push mode — full content + const content = msg.plaintext ?? decryptFailedWarning(fromPubkey); + try { + await server.notification({ + method: "notifications/claude/channel", + params: { + content, + meta: { + from_id: fromPubkey, + from_name: fromName, + mesh_slug: client.meshSlug, + mesh_id: client.meshId, + priority: msg.priority, + sent_at: msg.createdAt, + delivered_at: msg.receivedAt, + kind: msg.kind, + }, + }, + }); + } catch { /* best effort */ } + } + }, 1_000); + pushPollTimer.unref(); + } client.onStreamData(async (evt) => { try {