ActivityService.computeFlags (backend/services/activityService.ts:517-521) matches a human mention with a bare substring test:
const mentionNeedle = lowerUsername ? `@${lowerUsername}` : '';
// ...
isMention: Boolean(mentionNeedle && lowerContent.includes(mentionNeedle)),
includes has no right boundary, so any handle that is a prefix of another handle is flagged by messages addressed to the longer one.
Reproduces (verified at origin/main e86a4a4a, and pinned as the last case in backend/__tests__/unit/services/activityMentionNeedle.test.js, added by #1277):
computeFlags({ content: '@sammy is on it', username: 'sam', ... }).isMention === true
Why it matters more than it looks. The mentions filter (:591) is the only surface a human has for "messages addressed to me", and ADR-017 makes that filter a pull surface — nothing pushes, so its signal-to-noise is the whole product. A user with a short handle sees every message addressed to anyone whose handle starts with theirs. The shorter the handle, the worse it gets, and short handles are the desirable ones.
Also asymmetric with the agent path. resolveHumanMentionUserIds in agentMentionService.ts extracts handles with a [a-z0-9_-] character class after the @ — i.e. it tokenizes. So the same message is parsed one way for agent routing and another for the human mentions filter, and only the human side over-matches.
Fix shape. A right-boundary match, e.g. new RegExp('@' + escapeRegExp(lowerUsername) + '(?![a-z0-9_-])'). Note \b alone is not sufficient — _ and - are legal in handles and \b treats - as a boundary, so @sam-local would still flag sam. Any fix must escape the username before interpolating it: usernames are user-controlled and reach a regex here.
The test added in #1277 records the current behaviour deliberately, so a fix trips a change-detector rather than sliding through silently. Update that case as part of the fix.
Found while pinning the wake-cue claim in TASK-074.
ActivityService.computeFlags(backend/services/activityService.ts:517-521) matches a human mention with a bare substring test:includeshas no right boundary, so any handle that is a prefix of another handle is flagged by messages addressed to the longer one.Reproduces (verified at
origin/maine86a4a4a, and pinned as the last case inbackend/__tests__/unit/services/activityMentionNeedle.test.js, added by #1277):Why it matters more than it looks. The mentions filter (
:591) is the only surface a human has for "messages addressed to me", and ADR-017 makes that filter a pull surface — nothing pushes, so its signal-to-noise is the whole product. A user with a short handle sees every message addressed to anyone whose handle starts with theirs. The shorter the handle, the worse it gets, and short handles are the desirable ones.Also asymmetric with the agent path.
resolveHumanMentionUserIdsinagentMentionService.tsextracts handles with a[a-z0-9_-]character class after the@— i.e. it tokenizes. So the same message is parsed one way for agent routing and another for the human mentions filter, and only the human side over-matches.Fix shape. A right-boundary match, e.g.
new RegExp('@' + escapeRegExp(lowerUsername) + '(?![a-z0-9_-])'). Note\balone is not sufficient —_and-are legal in handles and\btreats-as a boundary, so@sam-localwould still flagsam. Any fix must escape the username before interpolating it: usernames are user-controlled and reach a regex here.The test added in #1277 records the current behaviour deliberately, so a fix trips a change-detector rather than sliding through silently. Update that case as part of the fix.
Found while pinning the wake-cue claim in TASK-074.