Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions backend/__tests__/unit/services/agentMentionService.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -754,6 +754,72 @@ describe('AgentMentionService', () => {
});
});

// Humans are addressed by handle, and only by handle (TASK-070a).
//
// Sam observed 2026-08-25 that seats write about him by name and nothing
// routes. The frame taught three verbs, all of which move attention
// between AGENTS; none of them reach a person, and the paragraph never
// said so. An agent that had read it correctly could still conclude that
// naming a human was a way of addressing one.
//
// The control matters more than usual here. This frame already contains
// the word "human" twice (the token-misattribution clause) and the
// literal "@" many times, so a loose assertion stays green on a frame
// that has lost the routing rule entirely.
describe('human-handle cue', () => {
const frame = async () => {
setupForAgent({ agentName: 'openclaw', instanceId: 'nova', displayName: 'Nova' });
await AgentMentionService.enqueueMentions({
podId: 'pod-human-1',
message: { content: 'Hi @nova', id: 'msg-human-1' },
userId: 'user-1',
username: 'sam',
});
return lastPayload().payload.content;
};

test('tells the agent to @mention a handle when it needs a human', async () => {
expect(await frame()).toContain('@mention their handle');
});

test('states that a bare name reaches no one', async () => {
// The failure is silent — nothing errors, the message posts, and no
// attention routes — so the cue has to name the outcome, not just
// prescribe the handle. Without this an agent reads the rule as a
// style preference it may skip when the name reads more naturally.
const content = await frame();
expect(content).toContain('A bare name reaches no one');
expect(content).toContain('matched on the literal @handle');
});

test('states the handle is necessary and not sufficient', async () => {
// Without this the cue teaches, by contrast with "reaches no one",
// that the handle DOES notify. It does not: humans get no AgentEvent
// row, so the ceiling is a pull surface. An agent that believes it
// has notified Sam stops working and waits — a new false model, and
// harder to spot than the old one because the message now looks
// correctly addressed.
const content = await frame();
expect(content).toContain('necessary and not sufficient');
expect(content).toContain('nothing pushes');
});

test('control: the pre-TASK-070 frame does not satisfy the assertions above', () => {
// Verbatim from the clauses that shipped before this change, and they
// are the ones most likely to keep a sloppy assertion green: both
// mention humans, and the second is entirely about @handles.
const beforeTaskO70 = 'Never post through an operator\'s CLI profile (`commonly pod send`) '
+ 'or a human user\'s token — that misattributes your words to a human. '
+ 'replyToMessageId quotes and ADDRESSES a message — its author is pinged. '
+ 'Rule of thumb: if your message answers one person, reply.';
expect(beforeTaskO70).not.toContain('@mention their handle');
expect(beforeTaskO70).not.toContain('A bare name reaches no one');
expect(beforeTaskO70).not.toContain('matched on the literal @handle');
expect(beforeTaskO70).not.toContain('necessary and not sufficient');
expect(beforeTaskO70).not.toContain('nothing pushes');
});
});

// The overflow rule, and the qualifier without which it is harmful.
//
// @sprint-review (57706) traced it to `effectiveFollowerIds`, whose
Expand Down
37 changes: 36 additions & 1 deletion backend/services/agentMentionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -554,7 +554,42 @@ const formatPodContextFrame = (podId: string): string =>
// below for the hole it left open.
`Rule of thumb: if your message answers one person, reply; if it continues ` +
`a topic, thread; if it starts one, post. A reply inside a thread is allowed ` +
`and still addresses its author.]`;
`and still addresses its author. ` +
// Humans are addressed by handle, and ONLY by handle (TASK-070a, Sam
// observed 2026-08-25). Every verb above routes attention between agents;
// none of them reach a person. A human's attention is matched on the
// literal `@handle` — activityService's `mentions` filter tests
// `content.includes('@' + username)`, and resolveHumanMentionUserIds
// extracts handles from `[a-z0-9_-]` after an `@`. A bare name matches
// neither, so "Sam should decide this" is addressed to nobody.
//
// This is the human-facing twin of the gap ADR-018 D6.3 closed for bots: a
// message that is plainly ABOUT someone still has to be addressed TO them
// before anything routes. The bot version was a missing implicit-reply
// wake; this one is a missing handle, and only the author can supply it.
//
// Deliberately teaches the escape and not a heuristic. Whether a bare name
// SHOULD route is an open decision (TASK-070b) precisely because matching
// names is fuzzy — every message about Sam is not for Sam — so the cue
// must not imply that writing the name is enough.
//
// The handle is NECESSARY, not sufficient, and the cue has to say both or
// it installs a fresh false model in place of the old one (@sprint-review
// on #1244). Humans have no AgentEvent delivery row — `enqueueMentions`
// never enqueues for a person — so an @handle buys the `isMention` flag on
// the activity feed and nothing else. Even the thread-follow half is
// narrower than it reads: `resolveHumanMentionUserIds` is called only
// inside `if (threadRootId)` (:1743), so a plain channel post gets the flag
// alone. That surface is PULL — ADR-017's only-interrupter rule reserves
// push for the escalation envelope — so "I mentioned them" is never "they
// know", and an agent that stops there has blocked itself on a filter
// nobody may have opened.
`When you need a HUMAN — a decision, a merge press, an answer only they ` +
`have — @mention their handle. A bare name reaches no one: human attention ` +
`is matched on the literal @handle, so "Sam should decide this" is addressed ` +
`to nobody. The handle is necessary and not sufficient — it flags the message ` +
`in a mentions filter the human pulls; nothing pushes. Say plainly what you ` +
`need, and never treat a mention as an answer received.]`;

// Cross-runtime consultation cue. Companion to the pod-context frame
// above; same rationale (inline cue beats structured metadata per
Expand Down
Loading