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:
Alejandro Gutiérrez
2026-05-04 17:33:18 +01:00
parent 7460d34335
commit a25102a79f
4 changed files with 71 additions and 5 deletions

View File

@@ -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) =>

View File

@@ -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) => {