Skip to content

fix(native): an agent a person @-mentioned never stands down to another agent's claim - #2269

Merged
lilyshen0722 merged 1 commit into
mainfrom
lily/addressed-never-stands-down
Oct 16, 2026
Merged

lilyshen0722 merged 1 commit into
mainfrom
lily/addressed-never-stands-down

Conversation

@lilyshen0722

Copy link
Copy Markdown
Contributor

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:

  • the hosted agent's chat.mention was queued at 47.359;
  • Scout's wake-on-message turn was queued at 47.513;
  • Scout claimed first at 47.756, and the backend logged … stood down — message 78340 claimed by scout;
  • Scout answered NO_REPLY, so the claim went declined;
  • the hand-off re-offers only ambient wakes, so nobody answered.
    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.

  • It applies to chat.mention, thread.mention, dm.message, and a message stamped repliesToYourMessage: a lost claim runs the turn, unclaimed, so there's no renewal and no release.
  • The peer-aware notice is added only when the holder was addressed by the same message too, checked as an addressed AgentEvent for that message within the last hour.
  • Beaten by an ambient listener (Scout), the agent just answers. If the lookup fails, it also answers rather than deferring.
  • An ambient (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:

  • Ambient wakes stay alongside a routed message. The kernel keeps that "ambient companion" on purpose (agentMentionService, the second wake call site).
  • No re-offer to mentioned agents on decline. With this change, an addressed agent never stands down, so there's nothing left for a re-offer to rescue.

Tests

All under Node 22.

  • nativeRuntimeService.claims.test.js:
    • an ambient lost CAS still stands down;
    • each addressed type runs unclaimed on a lost CAS, and nothing is released;
    • an ambient holder gets no notice; an addressed holder gets the peer-aware notice; a failed lookup still answers.
  • 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 passes countRuns.
  • 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.
  • 91 suites, 865/865 (native, mention, claim, first contact, hosted, spend, routes, agents runtime, tool contract).
  • Mutations, each caught and restored from a copy:
    • never addressed: 7 failed;
    • peer notice always: 2;
    • an unknown holder counted as addressed: 1;
    • the meter ignores runs: 1;
    • the route drops the flag: 1;
    • no greeting stop: 1;
    • the stop on every hosted trigger: 1.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Wkz5axAzCvoLEFkZ5KtkKG

…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 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.

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_TYPES is {chat.mention, thread.mention, dm.message} — three members, and neither ambient type (message.posted, chat.message) is in it.
  • CLAIMABLE_RAW_TYPES is 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 at agentMentionService:1387 from the backend's own repliesToYou. 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 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.

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_url is refused with no GET (declared-tools and legacy). Fetch-capable turns still work.
  • Mutation: forcing mayFetch=false while 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

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