From a25102a79fff6ac4e19eb3f094d2732b92b00be2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Guti=C3=A9rrez?= <35082514+alezmad@users.noreply.github.com> Date: Mon, 4 May 2026 17:33:18 +0100 Subject: [PATCH] =?UTF-8?q?fix(cli):=201.32.1=20=E2=80=94=20DMs=20to=20ses?= =?UTF-8?q?sion=20pubkeys=20finally=20land=20in=20inbox?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SessionBrokerClient (daemon-side, since 1.30.0) was constructed without a push handler and silently dropped every inbound `push` / `inbound` frame. Header docstring claimed it handled "inbound DM delivery for messages targeted at the session pubkey" but the callback was never wired. Net effect: any DM sent to a peer's session pubkey (everything `peer list` returns now) was queued, broker-acked, marked delivered_at on the broker, and thrown away by the recipient daemon. inbox.db stayed at zero rows; `claudemesh inbox` reported "no messages" no matter what arrived. Two-session smoke surfaced this — sender outbox status=done with broker_message_id, recipient inbox empty. Fix: wire SessionBrokerClient to forward push/inbound frames to the same handleBrokerPush the member-keyed broker already uses. Pass the per-session secret key as sessionSecretKeyHex so decryptOrFallback tries it first; member key remains the fallback for legacy member-targeted traffic. Verified end-to-end with two registered sessions sending in both directions — inbox.db row count went 0 → 2. Files: apps/cli/src/daemon/session-broker.ts, apps/cli/src/daemon/run.ts. No broker change required. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/cli/CHANGELOG.md | 34 +++++++++++++++++++++++++++ apps/cli/package.json | 2 +- apps/cli/src/daemon/run.ts | 16 +++++++++++++ apps/cli/src/daemon/session-broker.ts | 24 +++++++++++++++---- 4 files changed, 71 insertions(+), 5 deletions(-) diff --git a/apps/cli/CHANGELOG.md b/apps/cli/CHANGELOG.md index 5635875..8f2e70b 100644 --- a/apps/cli/CHANGELOG.md +++ b/apps/cli/CHANGELOG.md @@ -1,5 +1,39 @@ # Changelog +## 1.32.1 (2026-05-04) — DMs to session pubkeys actually deliver now + +Critical fix. Sessions launched via `claudemesh launch` (1.30.0+) hold a +per-launch session WebSocket on the broker, separate from the daemon's +member-keyed WS. The broker correctly fans direct messages targeted at a +session pubkey out over THAT session WS — but the daemon's +`SessionBrokerClient` was constructed without a push handler and silently +dropped every inbound `push` / `inbound` frame. The header docstring +even claimed it handled "inbound DM delivery for messages targeted at +the session pubkey"; the code never wired the callback. + +Net effect since 1.30.0: any DM sent to a peer's session pubkey +(everything `peer list` returns these days, since session pubkey is the +canonical routing key) was queued, broker-acked, marked `delivered_at` +on the broker side, and then thrown away by the recipient daemon. The +local `inbox.db` stayed at zero rows forever and `claudemesh inbox` +reported "no messages" no matter what arrived. + +Two-session smoke test that surfaced this: peer A sent "hola" to peer +B's session pubkey — sender outbox showed `status=done` with a +`broker_message_id`, recipient inbox stayed empty, both sides confused. + +The fix wires `SessionBrokerClient` to forward `push` / `inbound` frames +to the same `handleBrokerPush` the member-keyed broker already uses. The +session's secret key (registered via `/v1/sessions/register`) is passed +as `sessionSecretKeyHex` so `decryptOrFallback` tries it first; the +parent member key remains the fallback for legacy member-targeted +traffic that happens to fan out here. + +Files: `apps/cli/src/daemon/session-broker.ts`, +`apps/cli/src/daemon/run.ts`. No broker change required — the broker +half (queue + fan-out + sendToPeer on the session WS) was already +correct; only the daemon-side intake was missing. + ## 1.32.0 (2026-05-04) — multi-session UX bundle Nine UX bugs surfaced from a real two-session interconnect smoke test diff --git a/apps/cli/package.json b/apps/cli/package.json index 4240143..387c92f 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,6 +1,6 @@ { "name": "claudemesh-cli", - "version": "1.32.0", + "version": "1.32.1", "description": "Peer mesh for Claude Code sessions — CLI + MCP server.", "keywords": [ "claude-code", diff --git a/apps/cli/src/daemon/run.ts b/apps/cli/src/daemon/run.ts index 352cb28..150bab8 100644 --- a/apps/cli/src/daemon/run.ts +++ b/apps/cli/src/daemon/run.ts @@ -177,6 +177,13 @@ export async function runDaemon(opts: RunDaemonOptions = {}): Promise { sessionBrokers.delete(info.token); prior.close().catch(() => { /* ignore */ }); } + // 1.32.1 — wire push delivery. Messages targeted at the launched + // session's pubkey land on THIS WS, not on the member-keyed one, + // so without this forward they'd silently disappear (the bug that + // kept inbox.db at zero rows since 1.30.0). Decrypt prefers the + // session secret key; member key remains the fallback for legacy + // member-targeted traffic that happens to fan out here. + const sessionSecretKeyHex = info.presence.sessionSecretKey; const client = new SessionBrokerClient({ mesh: meshConfig, sessionPubkey: info.presence.sessionPubkey, @@ -187,6 +194,15 @@ export async function runDaemon(opts: RunDaemonOptions = {}): Promise { ...(info.role ? { role: info.role } : {}), ...(info.cwd ? { cwd: info.cwd } : {}), pid: info.pid, + onPush: (m) => { + void handleBrokerPush(m, { + db: inboxDb, + bus, + meshSlug: meshConfig.slug, + recipientSecretKeyHex: meshConfig.secretKey, + sessionSecretKeyHex, + }); + }, }); sessionBrokers.set(info.token, client); client.connect().catch((err) => diff --git a/apps/cli/src/daemon/session-broker.ts b/apps/cli/src/daemon/session-broker.ts index 5c03e17..0cac77c 100644 --- a/apps/cli/src/daemon/session-broker.ts +++ b/apps/cli/src/daemon/session-broker.ts @@ -15,8 +15,10 @@ * DaemonBrokerClient's job. Keeps the responsibility split clean * and avoids two clients fighting over the same outbox row. * - Does NOT carry list_peers / state / memory RPCs. This client is - * presence-only (and inbound DM delivery for messages targeted at - * the session pubkey). + * presence-only PLUS inbound DM delivery for messages targeted at + * the session pubkey — pushes are forwarded via the `onPush` + * callback to the daemon's shared handleBrokerPush, decrypted with + * this session's secret key. * * Old brokers reply with `unknown_message_type` on session_hello — we * surface that as a one-shot `error` event and the daemon decides @@ -62,6 +64,14 @@ export interface SessionBrokerOptions { /** Pid of the launched session (NOT the daemon). */ pid: number; onStatusChange?: (s: SessionBrokerStatus) => void; + /** + * Inbound push/inbound dispatch. The broker fans messages targeted at + * a session pubkey out over the corresponding session WS — without + * this callback they hit the floor and the daemon's inbox.db never + * sees them. Wired in run.ts to a handleBrokerPush call that decrypts + * with this session's secret key (member key as fallback). + */ + onPush?: (msg: Record) => void; log?: (level: "info" | "warn" | "error", msg: string, meta?: Record) => void; } @@ -167,8 +177,14 @@ export class SessionBrokerClient { } return; } - // push / inbound — presence-only client ignores them; the daemon's - // member-keyed client handles all DM decryption. + + // 1.32.1 — DMs targeted at the launched session's pubkey arrive + // here, NOT on the daemon's member-keyed WS. Forward to the + // daemon-level push handler so they land in inbox.db. + if (msg.type === "push" || msg.type === "inbound") { + this.opts.onPush?.(msg); + return; + } }); ws.on("close", (code, reason) => {