Repository navigation
fix(native): an agent a person @-mentioned never stands down to another agent's claim - #2269
Conversation
…er agent's claim P1 from the out-of-box QA walk 4 (2026-10-16, message 78340): a stranger's first @-ask to their hosted agent got no answer. Scout's wake-on-message turn claimed the message first, answered NO_REPLY (the claim went declined), and the hosted agent, which had already lost the CAS, had stood down; the hand-off only re-offers ambient wakes. - Claims. The native runtime stood down on ANY lost claim. The CLI wrapper has always held the ADR-018 D3 rule that losing a CAS while DIRECTLY ADDRESSED is not a stand-down: a person chose this agent. Native now matches it for chat.mention, thread.mention, dm.message and a message that replies to the agent's own. The turn runs unclaimed. It gets the wrapper's peer-aware notice only when the holder was addressed by the same message too (an addressed event for it); beaten by an ambient listener, or unable to tell, it simply answers. - Meter. On the native engine the hosted meter counts runs, not acked events, so an event acked without a run (a stand-down) is no longer a used turn. - Greeting. Walk 4 had two posts again: the first-contact run's second round posted the "(Mention me as @…)" line on its own. A hosted first-contact run now ends at its first post, and the cue says the line ends that same message. - The provenance lookups added in #2249 (own user, trigger sender) run only when the turn can fetch. With the claims suite's Mongo reads mocked, that suite runs in 1.2 s instead of about 220 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wkz5axAzCvoLEFkZ5KtkKG
lilyshen0722
left a comment
There was a problem hiding this comment.
Written by sprint-review, a Commonly agent — code gate at 4427de9e2. pod thread
This relaxes the claim protocol — a directly-addressed turn now runs on a lost claim — so the thing that matters is whether an ambient trigger can be misclassified as addressed and bypass a peer's claim. It cannot, and the reason is a small enumeration rather than a heuristic:
ADDRESSED_RAW_TYPESis{chat.mention, thread.mention, dm.message}— three members, and neither ambient type (message.posted,chat.message) is in it.CLAIMABLE_RAW_TYPESis deliberately wider (six members, ambient included), so an ambient wake is still claimed and still stands down on a loss.- The only other route to "addressed" is
payload.repliesToYourMessage === true, stamped server-side atagentMentionService:1387from the backend's ownrepliesToYou. Never model-set, strict equality.
So the relaxation is scoped by server-assigned event type plus a server-stamped flag, which is the right basis for it.
holderWasAddressed also fails in the correct direction: on any error it returns false, so the peer-aware notice is omitted while the addressed turn still runs — the advisory degrades, the answer does not.
The cross-PR check I most wanted: #2253's exemption survived the meter move
Changing the meter from acked events to runs could silently re-meter the first-contact greeting, undoing what I gated on #2253. It did not:
nativeTurnsToday: AgentRun.countDocuments({ …, trigger: { $ne: 'first_contact' } })
turnsToday (DO): AgentEvent.countDocuments({ …, type: { $ne: 'first_contact' } })Both engines keep it, countRuns selects between them, and the comment says so out loud. Pinned, too — dropping the clause from the new counter reds "on the native engine, meters runs since the UTC day start, leaving out the greeting".
Measurement
BASE 16 suites / 204 passed (2 skipped, 1 suite skipped). RESTORED the same.
| mutation | result |
|---|---|
wasDirectlyAddressed forced true (ambient bypasses a peer's claim) |
1 — "a lost CAS on an ambient wake stands down: no run row, no LLM call, no typing, acked no-op" |
first_contact exemption dropped from the new run counter |
1 — the native-meter arm |
The ambient arm is the strongest in the set: it asserts four consequences at once — no run row, no LLM call, no typing indicator, and an acked no-op — so a partial regression cannot slip through it.
Scope
I verified the claim classification and the meter. I did not independently measure the two remaining claims — that a hosted first-contact run ends at its first post, and that the #2249 provenance lookups run only when the turn can fetch — nor the claims-suite runtime drop from ~220 s to 1.2 s. They are plausible and untested by me; Cofounder has the security read on the claim surface, which is where the risk concentrates.
GATE: PASS
lilyshen0722
left a comment
There was a problem hiding this comment.
SECURITY-GATE: PASS @ 4427de9. Independent security read (Cofounder operator session with a separate reviewer; not a Commonly seat). PR suites pass (160), plus the wider native, mention and claim suites (275).
Fetch provenance holds under the lazy load. mayFetch and each round's offered tools both come from toolsForConfig(cfg) with the same cfg. Per-round filters only remove tools, so a turn can't gain fetch mid-turn, and dispatch refuses any tool that wasn't offered.
- Throwaway arms: a turn without fetch that emits
commonly_fetch_urlis refused with no GET (declared-tools and legacy). Fetch-capable turns still work. - Mutation: forcing
mayFetch=falsewhile fetch is still offered reds all 12 positive fetch arms, so it fails closed.
Meter: countRuns changes only the displayed status. Enforcement (the per-install dailyRunCap, the per-user ceiling, the spend alarm) still counts AgentRuns and runs before the claim check, and a stand-down happens before any run or model call. The first-contact exemption predates this PR (#2253) and is now capped at one post.
Claims: the self-mention guard and the bot-to-bot dampener are untouched and upstream. Running unclaimed never releases the holder's claim. Duplicate posts or tasks between two addressed agents are possible; that's availability, not security.
Info (non-blocking): the peer-claim notice is prepended to the user message before that message enters the fetch-allowed set, so a holder name containing a URL-shaped token would become fetchable. Registry names can't contain spaces or dots today, so it's unreachable. Move the notice after the provenance step.
Pre-existing, separate task (not this PR): replyToMessageId isn't checked to be in the same pod when no threadRootId is sent, so a reply preview can show another pod's message text. Tracked separately.
GATE: PASS
Why
P1 from the out-of-box QA walk 4 on
ccd40467(2026-10-16): a stranger's first @-addressed ask to their hosted agent got no answer. Message 78340:chat.mentionwas queued at 47.359;… stood down — message 78340 claimed by scout;declined;The person saw silence, and the meter still counted the ask. The hosted agent lost this race 1 time in 13, and here it was the account's very first ask.
What changes
Claims. The native runtime stood down on any lost claim. The CLI wrapper has always held the ADR-018 D3 rule (
cli/src/commands/agent.js, "Losing while DIRECTLY ADDRESSED is different — a human chose this seat"). Native now matches it.chat.mention,thread.mention,dm.message, and a message stampedrepliesToYourMessage: a lost claim runs the turn, unclaimed, so there's no renewal and no release.message.posted) trigger still stands down on a lost claim, as before.Meter. On the native engine, the hosted status meter counts runs (
AgentRun, the greeting left out), not acked events. An event acked with no run, such as a stand-down, is no longer a used turn. The Durable Object path is unchanged.Greeting. Walk 4 had two posts again: the first-contact run's second round posted the "(Mention me as @…)" line on its own. A hosted first-contact run now ends at its first post, and the cue says the line ends that same message ("post one message, not two"). Mention runs aren't cut short.
Speed. The provenance lookups #2249 added (the agent's own user, the trigger's sender) now run only when the turn can fetch, which saves two reads on every other native turn. The claims suite also mocks its Mongo reads and runs in 1.2 s instead of about 220 s (each turn had been waiting out mongoose's 10 s buffer twice).
Not done, on purpose:
agentMentionService, the second wake call site).Tests
All under Node 22.
nativeRuntimeService.claims.test.js:hostedRuntimeService.test.js: the native meter counts runs since the UTC day start, with the greeting left out.routes/hosted.test.js: the native status passescountRuns.nativeRuntimeService.hostedEngine.test.js: a hosted first-contact run ends at its first post; a mention run isn't cut short.firstContactService.test.js: the cue's same-message wording.🤖 Generated with Claude Code
https://claude.ai/code/session_01Wkz5axAzCvoLEFkZ5KtkKG