fix: align Buzz explicit mention semantics

This commit is contained in:
Shakker
2026-08-02 09:50:29 +01:00
parent 1b8c99fa4e
commit c2d6ecee3c
6 changed files with 27 additions and 17 deletions
+1 -1
View File
@@ -23,7 +23,7 @@ Docs: https://docs.openclaw.ai
- **Buzz message fidelity:** preserve Markdown output and accept Buzz normal, rich-content, and structured-diff room messages through the existing authorized inbound path. Thanks @shakkernerd.
- **Buzz typing indicators:** show room- and thread-scoped typing during agent replies and heartbeat deliveries, refresh through the active authenticated connection without waiting for relay acknowledgement, and drop ephemeral updates safely during disconnects or shutdown. Thanks @shakkernerd.
- **Buzz sender directory:** expose current bot, member, room, and room-member directory entries from bounded relay state; use current Buzz profile and room names in inbound context while preserving public keys and UUIDs as stable authorization and routing identities. Thanks @shakkernerd.
- **Buzz native mentions:** resolve unique current room-member names and explicit NIP-27 identities into native `p` tags for replies, proactive sends, and bounded standalone delivery; reject ambiguous, unknown, or out-of-room identities instead of sending inert mention text, and preserve Buzz reply-thread session parsing during maintenance and heartbeat runs. Thanks @shakkernerd.
- **Buzz native mentions:** resolve unique current room-member names and explicit NIP-27 identities into native `p` tags for replies, proactive sends, and bounded standalone delivery; reject out-of-room identities and unresolved labels without an explicit identity, and preserve Buzz reply-thread session parsing during maintenance and heartbeat runs. Thanks @shakkernerd.
- **ClickClack guided setup:** configure ClickClack from `openclaw onboard` or `openclaw channels add clickclack` with URL, token, and workspace prompts, default-account env fallback, nonfatal live connection validation, and gateway-aware next steps that connect automatically when OpenClaw is already running. Thanks @shakkernerd.
- **ClickClack command menus:** publish each bot's native OpenClaw commands to ClickClack composer autocomplete at gateway startup, with per-account opt-out and nonfatal compatibility handling for older tokens and servers. Thanks @shakkernerd.
- **Skill Workshop approvals:** run agent-initiated apply, reject, and quarantine actions without an additional approval prompt by default while preserving `skills.workshop.approvalPolicy: "pending"` as an opt-in approval gate. Thanks @shakkernerd.
+8 -5
View File
@@ -204,12 +204,15 @@ openclaw message send \
--message "Please review this, nostr:npub1..."
```
The referenced public key must be a current member of the target room. Unknown
names, duplicate profile names, and out-of-room public keys fail visibly instead
of sending text that looks like a mention without notifying anyone. Ambiguous
The referenced public key must be a current member of the target room. Without
an explicit identity, unknown names and duplicate profile names fail visibly
instead of sending text that looks like a mention without notifying anyone.
When the message contains an explicit identity, unresolved or ambiguous labels
remain presentation text; include every intended identity explicitly. Ambiguous
errors list candidate public keys so the sender can retry with the intended
`nostr:npub...` identity. Mention-like text inside inline or fenced Markdown
code is ignored, and one message can carry at most 50 native mentions.
`nostr:npub...` identity. Out-of-room public keys always fail. Mention-like text
inside inline or fenced Markdown code is ignored, and one message can carry at
most 50 native mentions.
Connected Gateways resolve names from their existing in-memory directory
snapshot and do not query the relay per message. Profiles beyond the bounded
+1 -1
View File
@@ -9,7 +9,7 @@ describe("Buzz channel guidance", () => {
"- Buzz targets: use a configured room UUID, `buzz:<ROOM_UUID>`, or a unique current room name. Use the UUID when room names are ambiguous.",
);
expect(hints).toContain(
"- Buzz mentions: write a unique current room member as `@Display Name`. For an explicit identity, include `nostr:npub...`; the public key must belong to the target room. Ambiguous, unknown, or out-of-room mentions fail instead of sending untagged mention text.",
"- Buzz mentions: write a unique current room member as `@Display Name`. For an explicit identity, include `nostr:npub...`; the public key must belong to the target room. Any unresolved or ambiguous label needs an explicit identity for every intended member.",
);
expect(buzzPlugin.messaging?.targetResolver?.hint).toBe("<room UUID|configured room name>");
});
+1 -1
View File
@@ -72,7 +72,7 @@ export const buzzPlugin = createChatChannelPlugin<ResolvedBuzzAccount, BuzzProbe
agentPrompt: {
messageToolHints: () => [
"- Buzz targets: use a configured room UUID, `buzz:<ROOM_UUID>`, or a unique current room name. Use the UUID when room names are ambiguous.",
"- Buzz mentions: write a unique current room member as `@Display Name`. For an explicit identity, include `nostr:npub...`; the public key must belong to the target room. Ambiguous, unknown, or out-of-room mentions fail instead of sending untagged mention text.",
"- Buzz mentions: write a unique current room member as `@Display Name`. For an explicit identity, include `nostr:npub...`; the public key must belong to the target room. Any unresolved or ambiguous label needs an explicit identity for every intended member.",
],
},
reload: { configPrefixes: ["channels.buzz"] },
+5 -5
View File
@@ -86,7 +86,7 @@ describe("Buzz outbound mentions", () => {
).toEqual([ALICE_PUBLIC_KEY]);
});
it("requires explicit identities to resolve the named member they accompany", () => {
it("allows unresolved labels as presentation text when an explicit identity is present", () => {
const explicitBob = nip19.npubEncode(BOB_PUBLIC_KEY);
const roomMembers = members(
{ publicKey: ALICE_PUBLIC_KEY, displayName: "Alice" },
@@ -94,21 +94,21 @@ describe("Buzz outbound mentions", () => {
{ publicKey: BOB_PUBLIC_KEY, displayName: "Bob" },
);
expect(() =>
expect(
resolveBuzzMessageMentions({
text: `Hello @Missing (nostr:${explicitBob})`,
members: roomMembers,
senderPublicKey: BOT_PUBLIC_KEY,
}),
).toThrow('Buzz mention "@missing" does not match a current room member');
).toEqual([BOB_PUBLIC_KEY]);
expect(() =>
expect(
resolveBuzzMessageMentions({
text: `Hello @Alice (nostr:${explicitBob})`,
members: roomMembers,
senderPublicKey: BOT_PUBLIC_KEY,
}),
).toThrow('Buzz mention "@alice" is ambiguous');
).toEqual([BOB_PUBLIC_KEY]);
});
it("bounds ambiguous-member guidance", () => {
+11 -4
View File
@@ -20,6 +20,10 @@ function isAsciiWhitespace(character: string | undefined): boolean {
return character !== undefined && /^[\t-\r ]$/u.test(character);
}
function isOnlyAsciiWhitespace(value: string): boolean {
return /^[\t-\r ]*$/u.test(value);
}
function isMentionBoundary(value: string): boolean {
const character = value[0];
return character === undefined || isAsciiWhitespace(character) || ",;.!?:)]}".includes(character);
@@ -32,7 +36,7 @@ function stripCodeRegions(content: string): string {
if (content.startsWith("```", index)) {
const lineStart = content.lastIndexOf("\n", index - 1) + 1;
const beforeFence = content.slice(lineStart, index);
if ([...beforeFence].every(isAsciiWhitespace)) {
if (isOnlyAsciiWhitespace(beforeFence)) {
const openingLineEnd = content.indexOf("\n", index + 3);
let searchFrom = openingLineEnd === -1 ? content.length : openingLineEnd + 1;
let closeEnd = content.length;
@@ -43,7 +47,7 @@ function stripCodeRegions(content: string): string {
}
const closingLineStart = content.lastIndexOf("\n", closingFence - 1) + 1;
const beforeClosingFence = content.slice(closingLineStart, closingFence);
if ([...beforeClosingFence].every(isAsciiWhitespace)) {
if (isOnlyAsciiWhitespace(beforeClosingFence)) {
const closingLineEnd = content.indexOf("\n", closingFence + 3);
closeEnd = closingLineEnd === -1 ? content.length : closingLineEnd + 1;
break;
@@ -237,7 +241,7 @@ export function resolveBuzzMessageMentions(params: {
.map((member) => member.displayName)
.filter((name): name is string => Boolean(name)),
);
if (hasAtMentionCandidate(stripped) && names.length === 0) {
if (hasAtMentionCandidate(stripped) && names.length === 0 && explicitPublicKeys.length === 0) {
throw new Error(
"Buzz mention does not match a current room member; use nostr:npub... for an explicit identity",
);
@@ -245,12 +249,15 @@ export function resolveBuzzMessageMentions(params: {
for (const name of names) {
const matches = namesToPublicKeys.get(name) ?? [];
if (matches.length === 0) {
if (explicitPublicKeys.length > 0) {
continue;
}
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))) {
if (explicitPublicKeys.length > 0) {
continue;
}
const visibleCandidates = matches