Repository navigation
Description
Describe the bug
An @mention written with markdown emphasis — **@Name**, *@Name*, _@Name_ — is silently dropped by buzz messages send. The message posts normally, the CLI exits 0 with no warning, and the event carries zero p tags, so nobody is notified and no buzz-acp agent wakes.
This affects the CLI/SDK path only. The Desktop composer is not affected — its TypeScript parser already handles emphasis (see "Why Desktop is fine" below). The impact lands on agents, which post exclusively through buzz messages send, and on anyone scripting the CLI.
To Reproduce
buzz messages send --channel <uuid> --content '**@Alice** please take a look'
# exit 0, message appears in the channel, zero p tags on the event
buzz messages send --channel <uuid> --content '@Alice please take a look'
# p tag for Alice presentExpected behavior
**@Alice** should resolve exactly like @Alice. Failing that, the CLI should warn when the content contains @ tokens but zero mentions resolved, rather than exiting 0 silently.
Observed in production
Three consecutive messages from one agent, same channel, same command, same intended recipients. The only difference is the **:
| event | mention as written | p tags on the event |
|---|---|---|
d2c755cc |
… directly. @Atlas that assumption … |
a8a3fd71, 3f770d65 |
9a0c6e2c |
**@Atlas** your fix is aimed … / **@Derek** so item 1 … |
none at all |
f9402f75 |
**@Atlas** … plus one bare @Atlas inside a table |
a8a3fd71 only — the bare one |
Pulled from the relay with buzz messages get --channel <uuid>; 9a0c6e2c has an empty p set. Two agents in that thread went unnotified and the handoff stalled until a human noticed.
Root cause
crates/buzz-sdk/src/mentions.rs:123 — extract_at_mentions_with_known only accepts an @ at start-of-string or preceded by ASCII whitespace:
let preceded = i == 0 || content.as_bytes()[i - 1].is_ascii_whitespace();
if !preceded { continue; }In **@Atlas the preceding byte is *, so the mention is skipped. extract_at_names carries the identical rule at mentions.rs:75.
The call site is resolve_content_mentions at crates/buzz-cli/src/commands/messages.rs:191. It returns whatever the parser found and never reports an empty result — hence exit 0, no warning.
The whitespace rule is deliberate: it stops user@host in an email address from becoming a mention (documented at mentions.rs:57-59). Markdown emphasis is collateral damage.
Why Desktop is fine — and why that is the actual defect
Desktop has a second, independent implementation of the same rule, and it was fixed there over three months ago. desktop/src/features/messages/lib/hasMention.ts:143-150:
const pattern = new RegExp(
`(^|\\s|\\(|[*_]{1,3}|\\|\\|)(@${escaped})(?=\\|\\||[\\s,;.!?:)\\]}*_]|$)`,
"i",
);The leading group explicitly admits */_ ×1–3, (, and ||; the trailing lookahead admits * and _ as well. Added in 0f93ef44 ("fix: mentions survive copy/paste from chat into composer", #328, 2026-04-15) — TipTap's Bold extension wraps pasted mentions in **, which broke them in exactly this way. That commit touched only desktop/. The Rust parser never got the equivalent.
So the defect is parser divergence: two implementations of "is this an @mention" giving different answers, on the two surfaces that have to agree.
Two further divergences in the same pair — worth fixing together
-
Trailing emphasis breaks multi-word display names. Rust's
is_word_boundary(mentions.rs:154-157) accepts whitespace and,;.!?:)]}— not*,_, or|. For a member namedWill Pfleger,@Will Pfleger**fails the known-name match, falls through to the single-word tokenizer, and yieldswill, which matches nobody. The TS lookahead accepts*_and handles this case. -
The Rust path tags mentions inside code blocks; TS does not.
strip_code_regionsis applied only to NIP-27 URI extraction (crates/buzz-cli/src/commands/messages.rs:534), while@nameextraction at line 191 receives the raw content. TS masks fenced blocks, indented blocks and backtick spans before matching (maskMarkdownCodeinhasMention.ts). So an@nameappearing inside a code sample sent from the CLI fires a real notification — a false positive mirroring the false negative above.
Suggested direction (a direction, not a prescription)
Bring mentions.rs to behavioural parity with hasMention.ts rather than patching the one symptom:
- accept
*,_,(,||as leading delimiters atmentions.rs:75andmentions.rs:123; - widen
is_word_boundary(mentions.rs:154) to match, so trailing emphasis stops breaking multi-word names; - apply
strip_code_regionsbefore@nameextraction, not only before URI extraction.
Do not port the TypeScript regex into Rust. That would create a second hand-maintained delimiter list that drifts from the first exactly as the first drifted from the parser — which is this bug. Prefer widening the two existing Rust predicates against one shared "is this a delimiter" definition, and let the tests pin the behaviour. (Direction refined by Prometheus in triage; I originally suggested mirroring the regex and that was the worse call.)
Add Rust unit tests for the emphasis / spoiler / code cases. There are 51 tests in crates/buzz-sdk/src/mentions.rs (module at line 390+) and 18 more in crates/buzz-cli/src/commands/messages.rs (line 877+) — and not one of the 69 contains an emphasis character next to an @ (grep -n '\*\*@\|\*@\|_@' crates/buzz-sdk/src/mentions.rs → no matches). That gap is why the divergence survived a release.
Longer term the two parsers should be one shared implementation, or the divergence recurs.
A cheap independent mitigation, valuable on its own even if the parser is fixed: have buzz messages send warn on stderr when the content contains @ tokens but zero mentions resolved. Silent zero-recipient sends are the part that costs real time.
Related
- [Bug] Handoffs between agents don't always work #2442 — handoffs between agents don't always work. Different root cause (the agent posts nothing at all), but this is a second, independent way an agent handoff fails with no signal to anyone.
- Agent @mentions silently fail when mentioned agent is offline #1743 / Agent @mentions silently fail when mentioned agent is offline #1741 / Agent @mentions silently fail when target agent is offline — no warning, no catch-up #1742 — @mentions silently fail when the target agent is offline. Same failure class: the mention does not deliver and the sender gets no feedback.
- [Bug] Mentions should use NIP-27 #2319 — mentions should use NIP-27. Checked: it does not supersede this. See below — do not re-ask.
Does #2319 / NIP-27 make this moot? No. Checked at origin/main = acfbb1bb:
- fix(desktop): community create handoff, NIP-27 mentions, workflows list #2503 ("fix(desktop): community create handoff, NIP-27 mentions, workflows list") is the open PR carrying the NIP-27 work. All 15 changed files are under
desktop/—nip27Mentions.ts,rewriteMentionsToNip27.ts,collectMentionPubkeys.ts,hasMention.ts,MessageComposer.tsx,useMentionSendFlow.tsand friends. Zero files undercrates/. Same shape as0f93ef44: the TS side moves, the Rust side does not. - The SDK can read NIP-27 —
extract_nostr_urisatmentions.rs:353— but there is no write path: nothing incrates/rewrites an@nameinto anostr:npub1…ref.
So after #2503 lands, buzz messages send still emits bare @name text and still resolves it through extract_at_mentions_with_known. The text parser stays load-bearing on the agent path specifically, which is the exact path this bug breaks. Waiting for the NIP-27 work does not save the work.
(Verified by Prometheus and independently re-checked during triage.)
Environment
block/buzz@acfbb1bb(main). Originally traced atf8fd055b;crates/buzz-sdk/src/mentions.rsandcrates/buzz-cli/src/commands/messages.rsare byte-identical between the two, so every line number above is current.- Linux;
buzz-clias invoked bybuzz-acpmanaged agents
Reported by Prometheus in Buzz #ditto, 2026-07-23. Triaged by Muse.
Activity
fix(cli): resolve @mentions wrapped in markdown emphasis and skip cod…
fix(mentions): resolve emphasis/spoiler-wrapped @mentions in CLI (blo…
cristiansotogarciaxatech commentedon Sep 6, 2026
The fix for this is already open, and it is neither of the two PRs pointing at this issue. #3917 was filed against #3904 for CJK-adjacent mentions, and it closes this one as a side effect, because it replaces the whitespace check at
mentions.rs:123with "anything that is not an ASCII letter or digit opens a mention". Nothing links the two threads in either direction, so it has been sitting there since 31 July with zero reviews while two narrower PRs sit here.Measured rather than read. One probe test appended to
crates/buzz-sdk/src/mentions.rs, one ubuntu-latest runner, five trees fetched by sha, callingextract_at_mentions_with_known(input, &["Scout", "Will Pfleger"]).Run: https://github.com/cristiansotogarciaxatech/buzz/actions/runs/34030702637
3c7f288c0a39d45d3128593d32fcbd0f322611de@Scout hi(positive control)**@Scout** hi(@Scout) hi**@Will Pfleger** hi\u{a0}@Scout hi(NBSP)\u{feff}@Scout hi(BOM)\u{200b}@Scout hi(ZWSP)交给@Scout处理mail [email protected] here(negative control)Three things to add to the issue.
1.
(@Scout)is broken on main and nobody has filed it. Same four lines. Parentheses around a mention are more common in ordinary prose than**is, and the failure is identical, zeroptags and exit 0.2. Your "do not hand-maintain a delimiter list" call was right, and here is the receipt. A body whose first character is U+FEFF, which is what most Windows text editors and PowerShell writers produce, drops its opening mention on main and on all three of the emphasis PRs. U+FEFF is not whitespace under
char::is_whitespace, measured on the same runner asfeff_ws=false, so even the Unicode-aware allowlist in #5932 misses it. Only the denylist in #3917 catches it, and it catches CJK and ZWSP with the same four characters of code. This matters beyond aesthetics, becausecrates/buzz-cli/README.md:34documentsbuzz messages send --content - < message.mdandread_or_stdinatcrates/buzz-cli/src/validate.rs:168is a bareread_to_string, so the documented way to send a long body eats an opening mention on Windows.I hit that one in production on a Windows box running managed agents.
accepted: true, zeroptags, the text renders perfectly at the destination, the addressee is never notified, and the same name one line further down the same body tags correctly. Position 0 is the only position that fails and it is the only one that fails silently, because a name that gets extracted and does not resolve is a hard error atcrates/buzz-cli/src/commands/messages.rs:140, while a name that never gets extracted is not an error at all.3. Item 3 of this issue is stale. At
3c7f288c,resolve_content_mentionsstrips code regions before@nameextraction,crates/buzz-cli/src/commands/messages.rs:166feeding:226, so the CLI no longer tags mentions inside code blocks. That was true when you filed on 23 July and it is not true now. Items 1 and 2 are still live on main, exactly as you described them.I am not opening a sixth PR into this. #3917 already does the work.
vimergent commentedon Sep 15, 2026
Independent corroboration of @cristiansotogarciaxatech's measurement, from a separate implementation, because it reverses the conclusion I had reached on my own.
I had independently written the allowlist fix — widening the leading predicate to
whitespace | ( [ { < * ~ " 'plus the four curly quotes — before reading this thread. It passes the emphasis and bracket cases and it is still the wrong approach, measured:@Scout hi(positive control)["scout"]**@Scout** hi["scout"](@Scout) hi["scout"]\u{a0}@Scout hi(NBSP)[]\u{feff}@Scout hi(BOM)[]\u{200b}@Scout hi(ZWSP)[]交给@Scout处理(CJK)[]mail [email protected] here(negative control)[]Four silent failures that an allowlist cannot fix by growing, which is the point: I had already grown it twice (once for
<and quotes, once for curly quotes after realising phone keyboards substitute them) and it was still missing BOM, ZWSP and CJK. The triage note in the issue body — "do not hand-maintain a delimiter list" — is correct, and adding curly quotes to my own list was me demonstrating the failure mode while trying to fix it.Two things I can add that are not in the thread yet.
The curly-quote case is a fifth member of the same family.
\u{201C}@Scout\u{201D}fails on main and on any ASCII-only allowlist. It matters more than it looks: iOS and macOS substitute curly quotes automatically, and prose editors do too, so"@Scout"typed on a phone is not the character sequence the parser sees. A denylist keyed on "not an ASCII letter or digit" covers it for free.The allowlist also has to be maintained in more than one place, and immediately drifted for me.
crates/buzz-relay/src/workflow_sink.rskeeps its own boundary rule (#2686). When I applied the curly-quote change I had to write it twice, in two files, and the two copies were already inconsistent in a way I nearly shipped: the relay predicate useschar::is_whitespace(Unicode) while the SDK usesis_ascii_whitespace. Deduplicating naively — having the relay call the SDK predicate directly — silently narrows the relay so NBSP and ideographic space stop opening a mention there. That is a regression dressed as a cleanup, and it is only visible if you diff the two whitespace predicates character by character.Which is a concrete argument for #2686's "one shared definition" over three predicates, and for the shared definition being a denylist so the ASCII/Unicode whitespace distinction stops being load-bearing at all.
Not opening a competing PR. #3917 already does the work and is measurably ahead of the four narrower PRs pointing here. It has had zero reviews since 31 July purely because nothing cross-links it to this issue.
One adjacent gap I could not find reported anywhere, filed separately as #7660 rather than widening this thread: the relay's workflow parser never masks code regions, so an
@nameinside a backtick span in workflow-rendered text emits a realptag and wakes the agent. That is the opposite direction from this issue — silent false notification rather than silent non-notification — and it is unaffected by whichever boundary approach lands here. Item 3 of this issue records the same bug being fixed on the CLI path; the relay path never got it.