fix: tighten Buzz mention disambiguation

This commit is contained in:
Shakker
2026-08-02 06:31:24 +01:00
parent 6d7e371b0e
commit f4e332a72e
2 changed files with 62 additions and 11 deletions
+46
View File
@@ -9,6 +9,7 @@ import {
const BOT_PUBLIC_KEY = "a".repeat(64);
const ALICE_PUBLIC_KEY = "b".repeat(64);
const SECOND_ALICE_PUBLIC_KEY = "c".repeat(64);
const BOB_PUBLIC_KEY = "d".repeat(64);
function members(...values: BuzzMentionMember[]): BuzzMentionMember[] {
return [{ publicKey: BOT_PUBLIC_KEY, displayName: "OpenClaw" }, ...values];
@@ -82,6 +83,51 @@ describe("Buzz outbound mentions", () => {
).toEqual([ALICE_PUBLIC_KEY]);
});
it("requires explicit identities to resolve the named member they accompany", () => {
const explicitBob = nip19.npubEncode(BOB_PUBLIC_KEY);
const roomMembers = members(
{ publicKey: ALICE_PUBLIC_KEY, displayName: "Alice" },
{ publicKey: SECOND_ALICE_PUBLIC_KEY, displayName: "Alice" },
{ publicKey: BOB_PUBLIC_KEY, displayName: "Bob" },
);
expect(() =>
resolveBuzzMessageMentions({
text: `Hello @Missing (nostr:${explicitBob})`,
members: roomMembers,
senderPublicKey: BOT_PUBLIC_KEY,
}),
).toThrow('Buzz mention "@missing" does not match a current room member');
expect(() =>
resolveBuzzMessageMentions({
text: `Hello @Alice (nostr:${explicitBob})`,
members: roomMembers,
senderPublicKey: BOT_PUBLIC_KEY,
}),
).toThrow('Buzz mention "@alice" is ambiguous');
});
it("bounds ambiguous-member guidance", () => {
const duplicateMembers = Array.from({ length: 8 }, (_, index) => ({
publicKey: (index + 1).toString(16).repeat(64),
displayName: "Alice",
}));
expect(() =>
resolveBuzzMessageMentions({
text: "Hello @Alice",
members: members(...duplicateMembers),
senderPublicKey: BOT_PUBLIC_KEY,
}),
).toThrow(
`candidates: ${duplicateMembers
.slice(0, 5)
.map((member) => nip19.npubEncode(member.publicKey))
.join(", ")}, and 3 more.`,
);
});
it("rejects explicit identities outside the room and excludes the bot identity", () => {
const outsider = "d".repeat(64);
expect(() =>
+16 -11
View File
@@ -1,6 +1,7 @@
import { nip19 } from "nostr-tools";
export const BUZZ_MENTION_MAX_COUNT = 50;
const BUZZ_AMBIGUITY_CANDIDATE_LIMIT = 5;
export type BuzzMentionMember = {
publicKey: string;
@@ -243,26 +244,30 @@ export function resolveBuzzMessageMentions(params: {
.map((member) => member.displayName)
.filter((name): name is string => Boolean(name)),
);
const hasExplicitMentions = explicitPublicKeys.length > 0;
if (hasAtMentionCandidate(stripped) && names.length === 0 && !hasExplicitMentions) {
if (hasAtMentionCandidate(stripped) && names.length === 0) {
throw new Error(
"Buzz mention does not match a current room member; use nostr:npub... for an explicit identity",
);
}
for (const name of names) {
const matches = namesToPublicKeys.get(name) ?? [];
if (matches.length !== 1) {
if (hasExplicitMentions) {
if (matches.length === 0) {
throw new Error(
`Buzz mention "@${name}" does not match a current room member; use nostr:npub... for an explicit identity`,
);
}
if (matches.length > 1) {
if (matches.some((publicKey) => explicitPublicKeys.includes(publicKey))) {
continue;
}
if (matches.length === 0) {
throw new Error(
`Buzz mention "@${name}" does not match a current room member; use nostr:npub... for an explicit identity`,
);
}
const candidates = matches.map((publicKey) => nip19.npubEncode(publicKey)).join(", ");
const visibleCandidates = matches
.slice(0, BUZZ_AMBIGUITY_CANDIDATE_LIMIT)
.map((publicKey) => nip19.npubEncode(publicKey))
.join(", ");
const hiddenCandidateCount = matches.length - BUZZ_AMBIGUITY_CANDIDATE_LIMIT;
const candidateSuffix = hiddenCandidateCount > 0 ? `, and ${hiddenCandidateCount} more` : "";
throw new Error(
`Buzz mention "@${name}" is ambiguous; candidates: ${candidates}. Use nostr:npub... for an explicit identity`,
`Buzz mention "@${name}" is ambiguous; candidates: ${visibleCandidates}${candidateSuffix}. Use nostr:npub... for an explicit identity`,
);
}
const publicKey = matches[0];