diff --git a/CHANGELOG.md b/CHANGELOG.md index 52a0eddbf7c6..7f6415911943 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/docs/channels/buzz.md b/docs/channels/buzz.md index d364a46dbefe..00a697470bc2 100644 --- a/docs/channels/buzz.md +++ b/docs/channels/buzz.md @@ -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 diff --git a/extensions/buzz/src/channel.test.ts b/extensions/buzz/src/channel.test.ts index c61e4b835b29..5f34d40a1f5f 100644 --- a/extensions/buzz/src/channel.test.ts +++ b/extensions/buzz/src/channel.test.ts @@ -9,7 +9,7 @@ describe("Buzz channel guidance", () => { "- Buzz targets: use a configured room UUID, `buzz:`, 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(""); }); diff --git a/extensions/buzz/src/channel.ts b/extensions/buzz/src/channel.ts index 276f322b4b3c..0e17f2d0e7aa 100644 --- a/extensions/buzz/src/channel.ts +++ b/extensions/buzz/src/channel.ts @@ -72,7 +72,7 @@ export const buzzPlugin = createChatChannelPlugin [ "- Buzz targets: use a configured room UUID, `buzz:`, 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"] }, diff --git a/extensions/buzz/src/mentions.test.ts b/extensions/buzz/src/mentions.test.ts index 28449783c70a..c16d24df0fbc 100644 --- a/extensions/buzz/src/mentions.test.ts +++ b/extensions/buzz/src/mentions.test.ts @@ -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", () => { diff --git a/extensions/buzz/src/mentions.ts b/extensions/buzz/src/mentions.ts index c18325fe2fce..556b776597d0 100644 --- a/extensions/buzz/src/mentions.ts +++ b/extensions/buzz/src/mentions.ts @@ -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