fix(cli): 1.31.6 — resolve hex prefix to full pubkey before send so messages actually deliver
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled

claudemesh send <16-hex-prefix> would ack with sent to <prefix> (daemon)
but the recipient never received the message. Broker pre-flight and
the drain query both exact-match on full 64-char pubkey, so a prefix
queued successfully but no recipient drain ever fetched the row.
Sender saw sent, recipient saw nothing — silent drop.

Fix: CLI resolves any hex prefix (4-63 chars, not full 64) to the
full pubkey via the daemon peer list before submitting. Outcomes:

- unique match: canonicalize and continue
- no match: clear error + list of online peer display names
- multiple: clear error + candidate list + hint to lengthen prefix

The 16-hex prefix shown in peer list rows is now safe to paste
straight into claudemesh send.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-05-04 16:45:09 +01:00
parent a852a9df18
commit 25586d298f
3 changed files with 78 additions and 1 deletions

View File

@@ -47,6 +47,59 @@ export async function runSend(flags: SendFlags, to: string, message: string): Pr
flags.mesh ??
(config.meshes.length === 1 ? config.meshes[0]!.slug : null);
// 1.31.6: hex-prefix resolution. If `to` looks like hex but isn't a
// full 64-char pubkey, resolve it against the peer list and replace
// it with the matching full pubkey. The broker stores `targetSpec`
// verbatim and the drain query at apps/broker/src/broker.ts:2408
// matches only on full pubkeys, so a 16-hex prefix would queue
// successfully but never fetch — sender saw "sent", recipient saw
// nothing. Resolving here makes the CLI's prefix UX work end-to-end
// and surfaces ambiguous / unmatched prefixes with a clear error
// instead of a silent drop.
if (
!to.startsWith("@") &&
!to.startsWith("#") &&
to !== "*" &&
/^[0-9a-f]{4,63}$/i.test(to)
) {
try {
const { tryListPeersViaDaemon } = await import("~/services/bridge/daemon-route.js");
const peers = (await tryListPeersViaDaemon()) ?? [];
const lower = to.toLowerCase();
const matches = peers.filter((p) => {
const pk = (p as { pubkey?: string }).pubkey ?? "";
const mpk = (p as { memberPubkey?: string }).memberPubkey ?? "";
return pk.toLowerCase().startsWith(lower) || mpk.toLowerCase().startsWith(lower);
});
if (matches.length === 0) {
render.err(`No peer matches hex prefix "${to}".`);
const names = peers
.map((p) => (p as { displayName?: string }).displayName)
.filter(Boolean)
.join(", ");
if (names) render.hint(`online: ${names}`);
process.exit(1);
}
if (matches.length > 1) {
const candidates = matches
.map((p) => {
const pk = (p as { pubkey?: string }).pubkey ?? "";
const dn = (p as { displayName?: string }).displayName ?? "?";
return `${dn} ${pk.slice(0, 16)}`;
})
.join(", ");
render.err(`Ambiguous hex prefix "${to}" — matches ${matches.length} peers.`);
render.hint(`candidates: ${candidates}`);
render.hint("Use a longer prefix or paste the full 64-char pubkey.");
process.exit(1);
}
to = (matches[0] as { pubkey?: string }).pubkey ?? to;
} catch {
// Daemon unreachable — fall through; cold path will try a name
// lookup and surface its own error if that also fails.
}
}
// Self-DM safety check: if target is a 64-char hex that matches the
// caller's own member pubkey (or any of the caller's session/member
// entries), refuse without --self. Catches the common pasted-from-