Split from the parser-divergence cluster (#2526, #2686). This is a different failure direction from those: they are silent non-notification, this is a silent false notification, in the same function.
Problem
resolve_mention_pubkeys in crates/buzz-relay/src/workflow_sink.rs never masks code regions. So a workflow-rendered message containing an @name inside an inline code span or a fenced block emits a real p tag, and those p tags gate ACP agent wake (event_mentions_agent, crates/buzz-acp/src/lib.rs).
Workflow text is machine-generated markdown, and agents emit code constantly — a code sample naming an agent is documentation, not an address.
This is the mirror of the CLI issue noted as item 3 of #2526. That one was fixed on the CLI side: resolve_content_mentions now strips code regions before @name extraction (crates/buzz-cli/src/commands/messages.rs:166 feeding :226). The relay path never got the equivalent, exactly as #2526 describes the TS/Rust drift pattern — one surface moves, the other does not.
Measured on pristine main
A probe test appended to the tests module in crates/buzz-relay/src/workflow_sink.rs, one member ("Robby", <pubkey>), calling resolve_mention_pubkeys directly:
let got = resolve_mention_pubkeys("see `ping @Robby` for syntax", &members);
// PROBE inline-code-span result: ["aaaa…aaaa"] <- the code span resolved a real pubkey
The assertion got.is_empty() fails: the mention inside the backtick span wakes Robby.
Verified against crates/buzz-relay/src/workflow_sink.rs as it stands on main today: is_left_boundary is still at line 102 and the file contains zero references to strip_code_regions. git log <earlier-sha>..main -- crates/buzz-relay/src/workflow_sink.rs is empty, so #6953 ("fix(acp): wake agents from workflow messages") did not touch this file.
Note this is independent of the left-boundary widening in #2686 / #2691. Whitespace was always an opener, so `see @Robby` resolved before any boundary change and still will after one. Widening the opener set slightly extends the surface (`(@Robby)` also matches) but is not the cause.
Suggested direction
buzz-relay already depends on buzz-sdk (crates/buzz-relay/Cargo.toml), and buzz_sdk::mentions::strip_code_regions is already pub and already used on the CLI path. Calling it in resolve_mention_pubkeys before scanning is a two-line change.
One implementation note that is easy to get wrong: strip_code_regions is not length-preserving. Measured — "a code @x b" (13 chars) returns "a b" (5 chars). Any caller that maps positions back onto the original string will corrupt them.
That happens to be safe in resolve_mention_pubkeys specifically, because at and consumed are internal to the scanned copy and nothing position-derived escapes the function — it returns pubkeys ordered by first appearance, and masking does not reorder surviving prose. Worth stating explicitly in a comment so a future refactor that does surface offsets does not silently break.
A second, narrower limitation worth a test vector either way: a fence that does not begin at a line start is not masked at all. Measured — "pre ```\nblk @y\n``` post" returns "pre \nblk @y\n ", leaving the mention intact, because is_fence_start` requires the fence to follow a newline or only whitespace. Mid-line fences are unusual in prose but not in machine-rendered text.
Test vectors
Suggested cases for the workflow parser, the first three being the actual fix:
| body |
expected |
see `ping @Robby` for syntax |
no wake |
```\nping @Robby\n``` |
no wake |
`code` then @Robby please |
wake (masking must not swallow surrounding prose) |
heads up @Robby (positive control) |
wake |
alice@Robby (negative control, already passing) |
no wake |
#2526 notes that none of the existing 69 mention tests place an emphasis character next to an @; similarly there is currently no workflow-parser test involving code at all.
Split from the parser-divergence cluster (#2526, #2686). This is a different failure direction from those: they are silent non-notification, this is a silent false notification, in the same function.
Problem
resolve_mention_pubkeysincrates/buzz-relay/src/workflow_sink.rsnever masks code regions. So a workflow-rendered message containing an@nameinside an inline code span or a fenced block emits a realptag, and thoseptags gate ACP agent wake (event_mentions_agent,crates/buzz-acp/src/lib.rs).Workflow text is machine-generated markdown, and agents emit code constantly — a code sample naming an agent is documentation, not an address.
This is the mirror of the CLI issue noted as item 3 of #2526. That one was fixed on the CLI side:
resolve_content_mentionsnow strips code regions before@nameextraction (crates/buzz-cli/src/commands/messages.rs:166feeding:226). The relay path never got the equivalent, exactly as #2526 describes the TS/Rust drift pattern — one surface moves, the other does not.Measured on pristine
mainA probe test appended to the tests module in
crates/buzz-relay/src/workflow_sink.rs, one member("Robby", <pubkey>), callingresolve_mention_pubkeysdirectly:The assertion
got.is_empty()fails: the mention inside the backtick span wakes Robby.Verified against
crates/buzz-relay/src/workflow_sink.rsas it stands onmaintoday:is_left_boundaryis still at line 102 and the file contains zero references tostrip_code_regions.git log <earlier-sha>..main -- crates/buzz-relay/src/workflow_sink.rsis empty, so #6953 ("fix(acp): wake agents from workflow messages") did not touch this file.Note this is independent of the left-boundary widening in #2686 / #2691. Whitespace was always an opener, so
`see @Robby`resolved before any boundary change and still will after one. Widening the opener set slightly extends the surface (`(@Robby)`also matches) but is not the cause.Suggested direction
buzz-relayalready depends onbuzz-sdk(crates/buzz-relay/Cargo.toml), andbuzz_sdk::mentions::strip_code_regionsis alreadypuband already used on the CLI path. Calling it inresolve_mention_pubkeysbefore scanning is a two-line change.One implementation note that is easy to get wrong:
strip_code_regionsis not length-preserving. Measured —"acode @xb"(13 chars) returns"a b"(5 chars). Any caller that maps positions back onto the original string will corrupt them.That happens to be safe in
resolve_mention_pubkeysspecifically, becauseatandconsumedare internal to the scanned copy and nothing position-derived escapes the function — it returns pubkeys ordered by first appearance, and masking does not reorder surviving prose. Worth stating explicitly in a comment so a future refactor that does surface offsets does not silently break.A second, narrower limitation worth a test vector either way: a fence that does not begin at a line start is not masked at all. Measured —
"pre ```\nblk @y\n``` post"returns"pre\nblk @y\n ", leaving the mention intact, becauseis_fence_start` requires the fence to follow a newline or only whitespace. Mid-line fences are unusual in prose but not in machine-rendered text.Test vectors
Suggested cases for the workflow parser, the first three being the actual fix:
see `ping @Robby` for syntax```\nping @Robby\n````code` then @Robby pleaseheads up @Robby(positive control)alice@Robby(negative control, already passing)#2526 notes that none of the existing 69 mention tests place an emphasis character next to an
@; similarly there is currently no workflow-parser test involving code at all.