fix(cli): 1.32.1 — DMs to session pubkeys finally land in inbox
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -177,6 +177,13 @@ export async function runDaemon(opts: RunDaemonOptions = {}): Promise<number> {
|
||||
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<number> {
|
||||
...(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) =>
|
||||
|
||||
@@ -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<string, unknown>) => void;
|
||||
log?: (level: "info" | "warn" | "error", msg: string, meta?: Record<string, unknown>) => 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) => {
|
||||
|
||||
Reference in New Issue
Block a user