Skip to content

fix(composer): the @ picker never offers yourself, and puts the agent first - #2366

Merged
lilyshen0722 merged 2 commits into
mainfrom
fix/mention-picker-agents-first
Oct 21, 2026
Merged

lilyshen0722 merged 2 commits into
mainfrom
fix/mention-picker-agents-first

Conversation

@lilyshen0722

@lilyshen0722 lilyshen0722 commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Why

In the C14 call rehearsal on live, 10-20, typing @eng in the composer listed the person's own account first ("Member") and their agent second. Enter took the first item, so they mentioned themself and the agent never woke. On the call that would be @lar + Enter, giving @laruiss instead of @laruiss-agent.

What

frontend/src/v2/hooks/useV2ThreadMentions.ts:

Cofounder's brief said "agents first when the pod's agents were just active". The hook has no message history, so this ranks agents first among equal matches always. Removing yourself is what fixes the Enter trap.

Tests

useV2ThreadMentions.test.tsx:

  • never offers your own handle;
  • the agent comes first;
  • other people are still offered, after the agent;
  • a prefix match outranks a mid-word one, even a person over an agent;
  • another signed-in user still sees the first user as a member;
  • added after sprint-review's PASS: ranking happens before the 8-item cap (11 candidates, the agent last in pod order, still leads), and the self-skip ignores case (LaRuiss against laruiss). Capping before ranking, and dropping .toLowerCase(), now each fail an arm.

Mutations. Each fails a test:

mutation result
offer self 1 failed
drop agents-first 2 failed
drop prefix rank 1 failed

The composer, thread and hook suites pass, 142 tests. tsc reports no frontend errors and lint reports 0 errors. git merge-tree is clean against #2297, #2196, #2307 and #2360.

Merge

Draft, aimed at Wednesday's second deploy. The cut-off is an exact-head PASS by Wed 13:30 PT.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Wkz5axAzCvoLEFkZ5KtkKG

… first

C14 call rehearsal, 2026-10-20: typing "@eng" listed the person's own account
first ("Member") and their agent second, so Enter mentioned themself and the
agent never woke. On the call that is "@LAR" + Enter → @laruiss.

- The current user is never a candidate (read from AuthContext inside the
  hook; V2Thread, its only caller, already sits under it).
- Among matches, prefix matches come first, then agents before people, then
  the pod's own order. "@LAR" picks laruiss-agent; "@ag" still picks agnes
  over a mid-word agent match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wkz5axAzCvoLEFkZ5KtkKG

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code gate — #2366 at 78b2e3c8d6f1666240ca85eea52b9f220fb011f4

origin/main = cd37b32e0f9288ee24922362d3b86d17c5590368, which is this PR's merge-base. CI at the exact head: 14 pass / 2 skipping; Test & Coverage and Service Tests (Tier 1 — real DBs) SUCCESS in run 39508281935, E2E Tests in 39508281921, both head_sha read back as the head above. BASE: 13 suites / 142 tests — your number exactly.

The fix is right. Three things to know, one of which is a coverage gap worth an arm before this deploys.

The provider question you asked me to check

Clean. The hook has exactly one production caller, V2Thread.tsx:509, and V2Thread renders under V2RequireAuth inside the app-wide AuthProvider, so production always has one. Seven suites import and render the V2Thread component — V2ThreadStarts, V2ThreadDecisionCard, V2ThreadStarter, V2ThreadReveal, V2ThreadDeliveryHint, V2Composer, V2ComposerMentionState — every one of them references AuthContext/AuthProvider, and all seven are inside the 142 that pass. So no harness renders the hook bare; that is measured rather than inferred.

Your fifth arm is the right control, too: flipping the signed-in user to larissa and asserting laruiss reappears proves the skip keys on the viewer and not on a name in the code.

1. No arm reaches the cap, so ranking-before-capping is unpinned

Moving .slice(0, 8) above the .sort(...) leaves 142 / 142 green. It cannot be caught: the fixture has 4 candidates and the cap is 8.

It is not a cosmetic ordering question. Probed with 12 candidates — ten people matching lar, then the agent last in pod order:

first offered = laruiss-agent      agent present = true      list length = 8

So the shipped code is correct, and that is the property nothing holds. Capped first, those same inputs rank only the first eight in pod order and the agent is not among them — it disappears from the picker entirely, which is the C14 failure again in any pod with more than eight matching members. One arm with a >8 fixture covers it.

2. The self-guard fails open when currentUser is unavailable — but the ranking covers for it

selfUsername is (currentUser?.username || '').toLowerCase(), and the skip is if (selfUsername && …), so an absent user disables it. Measured on the small fixture, no cap in play:

auth state picker
signed in as laruiss [laruiss-agent, larissa]
currentUser: null [laruiss-agent, **laruiss**, larissa]
currentUser with no username [laruiss-agent, **laruiss**, larissa]

Reachable without a 401: V2RequireAuth gates on loading and isAuthenticated, which is !!token, and never on currentUser — so a non-401 failure of /api/auth/user (a 500, a network blip) leaves loading: false, the token in place, and the composer rendering with currentUser null.

The mitigation is already in the PR and is worth seeing: in that degraded window the agent is still at index 0, so Enter still picks the agent and the C14 harm does not recur — the picker merely offers self as a second option. The two halves of this fix cover for each other, which is a good argument for both.

3. The case-insensitivity of the skip is unpinned and load-bearing

Dropping .toLowerCase() from selfUsername is 142 / 142 green, because every username in the fixture is already lowercase. It does real work: with the auth username LaRuiss against the member row laruiss, the shipped code still skips self ([laruiss-agent, larissa]), and the case-sensitive mutant would offer it.

Verified as claimed

Needle asserted at one occurrence, site printed, source restored and re-verified after every run:

mutation site result
offer self useV2ThreadMentions.ts:108 1 failed — never offers your own handle
drop agents-first :169 2 failed — the agent comes first, so Enter picks it, other people are still offered, after the agent
drop prefix rank :168 1 failed — a prefix match outranks a match in the middle
startsWith → includes in prefix() (mine) :163 1 failed — same arm, so the prefix semantics are pinned, not just the term
cap before ranking (mine) :165 142 / 142
drop the a.order - b.order tiebreak (mine) :170 142 / 142
case-sensitive self-skip (mine) :58 142 / 142

The tiebreak row is the one green I would not chase: V8's sort is already stable, so the explicit index only makes the guarantee independent of the engine. Its green is correctness, not a gap.

One measured consequence, for the record

A bare @ now lists agents before people, where it used to follow pod order — because ''.startsWith('') is true, every item is a prefix match and the tiebreak falls through to agents-first. Probed: members alice, scout, bob with scout the agent give [scout, alice, bob]. Consistent with "among matches the likeliest agent is first", and worth knowing it reaches the empty query too.

eslint on both changed files: 0 errors, 1 warning (react/jsx-filename-extension), and that warning is the house state — src/v2/__tests__ carries 78 of them and 0 errors. Your "0 lint errors" is accurate.

Written by sprint-review, a Commonly agent — https://commonly.me/v2/pods/6a692a1be833c668acdb84cf

GATE: PASS

… @ picker

sprint-review on #2366 (PASS at 78b2e3c) found two properties the shipped
code holds and no arm pinned:
- Ranking before the 8-item cap. With ten people matching and the agent
  last in pod order, capping first drops the agent from the picker. New arm
  with 11 candidates; moving .slice(0, 8) above the sort now fails it.
- The self-skip ignores case. A signed-in "LaRuiss" against the member row
  "laruiss" must still be skipped; dropping .toLowerCase() now fails it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wkz5axAzCvoLEFkZ5KtkKG

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-gate — #2366 at d2c558f7c3ea16aa5d7de128c9eb02f3e05a2fc7

Baseline: my PASS at 78b2e3c8. One commit since, d2c558f7, authored Lily Shen, and tests-only confirmed by path: frontend/src/v2/__tests__/useV2ThreadMentions.test.tsx is the whole diff. origin/main = cd37b32e0f9288ee24922362d3b86d17c5590368, still the merge-base. CI at the exact head: 14 pass / 2 skipping, Test & Coverage and Service Tests (Tier 1 — real DBs) SUCCESS in run 39513402517, head_sha read back as the head above. BASE: 144 tests, up two arms.

Both findings closed. One new finding, which is not yours.

The two arms discriminate, each alone

● @ picker › ranking happens before the 8-item cap: the agent last in pod order still leads
Tests: 1 failed, 143 passed, 144 total        (.slice(0, 8) moved above the sort)

● @ picker › the self-skip ignores case
Tests: 2 failed, 142 passed, 144 total        (.toLowerCase() dropped from selfUsername)

The second failure in that last row is the new finding below, not a second discrimination.

New: a flaky arm in the suites this gate cites

V2Thread decision cards › chunks loaded source ids so decisions at both ends of a long transcript remain discoverable fails on a clean tree. Measured, three consecutive runs of the unmutated scope:

run1: 1 failed, 143 passed   ● chunks loaded source ids so decisions at both ends…
run2: 144 passed
run3: 144 passed

One in three on a small sample, so I will not claim a rate — but it is real, it is pre-existing (nothing in this PR touches V2Thread), and it sits inside the composer/thread/hook scope this gate and your own summary both quote. It cost me two misreadings before I isolated it: it appeared in both mutation rows above, and in the cap row it arrived alongside four more V2Thread failures that do not reproduce on an idle machine — those were my own contention from three batteries at once. Worth a ticket of its own; a flaky arm in a gate's scope makes every future mutation row here ambiguous.

Written by sprint-review, a Commonly agent — https://commonly.me/v2/pods/6a692a1be833c668acdb84cf

GATE: PASS

@lilyshen0722
lilyshen0722 marked this pull request as ready for review October 21, 2026 18:37
@lilyshen0722
lilyshen0722 added this pull request to the merge queue Oct 21, 2026
Merged via the queue into main with commit ea78e9b Oct 21, 2026
30 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant