Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion .agents/skills/peer-role/references/peer-role-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ add_message({
});
```

**Wake-control (the `add_message` `wakeSuppressed` param; #12635 Β· #14576 Β· #15987):** classify by **recipient actionability, not primitive name**. **Wake** (omit `wakeSuppressed`): direct review / re-review, `REQUEST_CHANGES`, `[lane-override]`, or coordination overlapping the recipient's active or owned surface. **Claim-class broadcasts are status, not interrupts:** `[lane-claim]` / `[review-claim]` / `[claim-corrected]` / `[drive-claimed]` on `AGENT:*` default quiet (collision safety lives at the claim surfaces β€” assignee gate + intake re-check); a contested-lane resolution sets `wakeSuppressed: false` explicitly. Use `priority: 'high'` only for act-now wakes: direct review / re-review, lane-unblock, operator relay, contested-lane resolution, `REQUEST_CHANGES`, or owned-surface overlap; other unsuppressed traffic stays `normal`. **May suppress** (`wakeSuppressed: true`): ordinary non-overlapping awareness β€” observer notes, lane-progress pings, acks; the recipient reads them at their next `list_messages`. Suppressed actionable *direct* lifecycle subjects are still rejected mechanically. (Additive to the session-sunset self-DM suppression, which stays valid.)
**Wake-control (the `add_message` `wakeSuppressed` param; #12635 Β· #14576 Β· #15987 Β· #17646):** **the address decides, not the subject.** Every `AGENT:*` broadcast is quiet by default β€” no tag vocabulary to remember, and collision safety lives at the claim surfaces (assignee gate + intake re-check). Waking the fleet is an explicit election, `wakeSuppressed: false`, reserved for what every seat must act on now (contested-lane resolution, stalled-pipeline alarm); `priority: 'high'` on a broadcast is **rejected** without it. **Direct** messages still wake by default β€” that is where actionability lives: review / re-review, `REQUEST_CHANGES`, `[lane-override]`, lane-unblock, operator relay, owned-surface overlap. Suppress a direct message (`wakeSuppressed: true`) only for non-overlapping awareness β€” observer notes, lane-progress, acks; suppressed actionable direct lifecycle subjects are rejected mechanically. Keep `priority: 'high'` for act-now direct traffic; everything else stays `normal`. (Additive to the session-sunset self-DM suppression, which stays valid.)

### 6.5.1 Lane-Override Protocol (`[lane-override]`)

Expand Down
12 changes: 10 additions & 2 deletions ai/daemons/orchestrator/Orchestrator.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,13 @@ export class Orchestrator extends Base {

try {
await RequestContextService.run({agentIdentityNodeId: sender}, async () => {
await MailboxService.addMessage({to: 'AGENT:*', subject, body, priority: 'high'});
// `wakeSuppressed: false` is REQUIRED here, not decorative: `AGENT:*` fan-out is
// quiet by default, so a stall alarm that omitted it would persist silently and
// reach seats only at their next natural intake β€” and the `catch` below would turn
// the coherence rejection into one log line, leaving the watchdog mute. A stalled
// embed pipeline degrades semantic recall for every seat at once, which is the
// narrow class the fleet-wide election exists for.
await MailboxService.addMessage({to: 'AGENT:*', subject, body, priority: 'high', wakeSuppressed: false});
});
} catch (e) {
this.writeLog('ERROR', `[Orchestrator] embed-drain stall-alarm A2A broadcast failed: ${e.message}`);
Expand Down Expand Up @@ -404,7 +410,9 @@ export class Orchestrator extends Base {

try {
await RequestContextService.run({agentIdentityNodeId: sender}, async () => {
await MailboxService.addMessage({to: 'AGENT:*', subject, body, priority: 'high'});
// Explicit wake election β€” see the embed-drain alarm above for why omission would
// make this watchdog mute rather than merely quiet.
await MailboxService.addMessage({to: 'AGENT:*', subject, body, priority: 'high', wakeSuppressed: false});
});
} catch (e) {
this.writeLog('ERROR', `[Orchestrator] REM consolidation stall-alarm A2A broadcast failed: ${e.message}`);
Expand Down
4 changes: 2 additions & 2 deletions ai/mcp/server/memory-core/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2123,7 +2123,7 @@ paths:
priority:
type: string
enum: [low, normal, high]
description: "Delivery priority. Omit to let the server set it from sender class: normal for an agent, high for an operator (turn-start drain ordering)."
description: "Delivery priority. Omit to let the server set it from sender class: normal for an agent, high for an operator (turn-start drain ordering). On an AGENT:* broadcast, high is rejected unless you also set wakeSuppressed: false β€” broadcasts are quiet by default, so high alone would claim urgency nothing wakes for."
partOfThread:
type: string
taggedConcepts:
Expand All @@ -2132,7 +2132,7 @@ paths:
type: string
wakeSuppressed:
type: boolean
description: "Persist + deliver but emit NO wake: the recipient sees it on their next list_messages, not as an interrupt β€” in autonomous mode, not until they next list or boot. Use for awareness/FYI (PR-opened-you're-not-the-reviewer, lane-progress, acks) and sunset self-DM handovers. Never suppress actionable messages β€” you're the reviewer, a lane was claimed on you, REQUEST_CHANGES; those subjects are rejected. Omit and the server decides: claim-class AGENT:* broadcasts and operator-sent messages are quiet, everything else wakes."
description: "Persist + deliver but emit NO wake: the recipient sees it on their next list_messages, not as an interrupt β€” in autonomous mode, not until they next list or boot. Omit and the server decides: every AGENT:* broadcast is quiet, as are operator-sent messages; direct messages wake. To wake the whole fleet you must elect it with wakeSuppressed: false β€” reserve that for something every seat must act on now. On a direct message use true for awareness/FYI (PR-opened-you're-not-the-reviewer, lane-progress, acks) and sunset self-DM handovers; never for actionable ones β€” you're the reviewer, a lane was claimed on you, REQUEST_CHANGES; those subjects are rejected."
task:
type: object
additionalProperties: true
Expand Down
38 changes: 29 additions & 9 deletions ai/services/memory-core/MailboxService.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {canonicalizeTaggedConceptIds} from '../graph/conceptSpineCanon
import GraphService from './GraphService.mjs';
import PermissionService from './PermissionService.mjs';
import WakeSubscriptionService from './WakeSubscriptionService.mjs';
import {collisionPreventionTag} from '../shared/a2aCollisionTags.mjs';
import {
TASK_ASSIGNMENT_AUTHORITY,
TASK_STATES,
Expand Down Expand Up @@ -2405,15 +2404,26 @@ class MailboxService extends Base {
// 'human' inverts the delivery defaults: durable-quiet (wake is the sender's per-message
// election, never the default) and priority-high as turn-start drain-ordering metadata.
const senderPrincipalClass = resolveSenderPrincipalClass(db, sentBy),
operatorSteering = senderPrincipalClass === 'human';
operatorSteering = senderPrincipalClass === 'human',
// Captured BEFORE the defaults resolve below. The coherence gate has to tell "the
// sender elected a wake" apart from "the default left this loud", and after the `??`
// those two states are the same value.
wakeElectedBySender = wakeSuppressed === false;

priority = priority ?? (operatorSteering ? 'high' : 'normal');
// Claim-class broadcasts are quiet by default (operator-directed 2026-07-26, superseding the
// wake-mandatory polarity): a claim's collision defense lives at the claim surfaces (the
// `requireUnassigned` assignee gate + intake's claim-race re-check), while a forced wake
// taxed every active seat per claim. Explicit `wakeSuppressed: false` still wakes β€” the
// contested-lane escalation stays a sender election, never a default. Scoped to `AGENT:*`
// fan-out; direct messages keep the plain default.
// A broadcast cannot be action-required for everyone β€” if it were, it would be addressed to
// someone. So `AGENT:*` fan-out is quiet by DEFAULT, and a wake is the sender's election via
// explicit `wakeSuppressed: false`. That is the polarity `learn/agentos/A2A.md` already
// describes; what changed is that the implementation now covers it.
//
// This supersedes a four-tag allowlist that delivered the same sentence for claim-class
// subjects only. The routine broadcast vocabulary is far wider β€” `pr-merged`,
// `merge-readiness`, `PR-opened`, `handoff`, `defect-note`, `runtime-maintenance` β€” so every
// class outside those four woke every active seat unless its author remembered the flag,
// and authors remembered unevenly. The predecessor fixed a matcher that was too literal;
// the same failure then recurred one level up as a vocabulary that was too small. Addressing
// is the signal that needs no vocabulary to stay current, which is why the rule keys on it.
// Direct messages keep the plain default and still wake: the noise was never in 1:1 traffic.
//
// Both this line and the `priority` one above depend on the field arriving ABSENT, and that
// is a contract with the tool schema, not a local property: `buildZodSchema` compiles a
Expand All @@ -2422,7 +2432,17 @@ class MailboxService extends Base {
// value is never nullish, and `??` cannot reach either branch β€” silently, since the injected
// value is a legal one. So `add_message` declares no default for either field: a request
// field may carry a schema default OR a service-side contextual default, never both.
wakeSuppressed = wakeSuppressed ?? (operatorSteering || (to === 'AGENT:*' && !!collisionPreventionTag({subject, taggedConcepts})));
wakeSuppressed = wakeSuppressed ?? (operatorSteering || to === 'AGENT:*');

// `priority: 'high'` and a suppressed wake are contradictory instructions, and the pair was
// accepted silently: the message reads as urgent in every listing while nothing wakes for
// it. Under the quiet default that state stops being an authoring slip and becomes the
// common case, so the two knobs must agree on `AGENT:*` β€” `high` requires the sender to
// have elected the wake. Operator steering is exempt by construction: that class is
// durable-quiet, and its `high` is turn-start drain-ordering metadata, not urgency.
if (to === 'AGENT:*' && priority === 'high' && !wakeElectedBySender && !operatorSteering) {
throw new Error("Cannot send a 'high' priority AGENT:* broadcast without an explicit wake election: broadcasts are quiet by default, so 'high' would claim urgency nothing wakes for. Set wakeSuppressed: false to wake the fleet, or use priority: 'normal'.");
}

// Canonicalize addressing to match the seeded AgentIdentity graph-node IDs. Upstream tool-
// schema wording exposes the `'AGENT:@login'` prefixed form; the seed uses bare `@login`.
Expand Down
12 changes: 10 additions & 2 deletions ai/services/shared/a2aCollisionTags.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,22 @@
* copy-pasted into two services, and a fix in one place left the other wrong: the wake guard was
* repaired while a fleet-activity copy of the old anchored regex kept undercounting live claims.
* A tag that lives in two places drifts.
*
* **The mailbox wake seam is no longer a consumer.** `MailboxService` once used this reader to pick
* which broadcasts default to quiet; `AGENT:*` fan-out is now quiet unconditionally, so that seam
* needs no vocabulary and asks nothing here. This module is NOT dead: `fleetA2AActivityAdapter`
* still reads it to identify lane claims, which is a question about what a message IS, not about
* how loudly it should arrive. That is the durable use β€” the classification outlived the delivery
* policy that first needed it, which is the argument for centralizing it rather than inlining a
* matcher at the seam.
*/

/**
* The collision-prevention tag vocabulary. PRIVATE to this module β€” the consumed contract is the
* reader below, never the Set: an exported mutable Set lets any importer rewrite every consumer's
* classifier at once (`.delete()` is a silent global veto). Consumers decide per-surface how WIDE
* their own question is (the mailbox quiet-by-default seam fires on any member; fleet activity counts only
* `lane-claim`) by comparing against the returned tag name.
* their own question is (fleet activity counts only `lane-claim`) by comparing against the returned
* tag name.
* @type {Set<String>}
*/
const COLLISION_PREVENTION_TAGS = new Set([
Expand Down
28 changes: 22 additions & 6 deletions learn/agentos/A2A.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,28 @@ and concept graph.

The wake convention follows from that separation: quiet is the default for
status broadcasts, and a wake is reserved for action the recipient must take.
Claim-class broadcasts (`lane-claim`, `review-claim`, `claim-corrected`,
`drive-claimed`) default to `wakeSuppressed` at the acceptance layer β€” peers
read them at their next natural intake, and claim collisions stay fail-closed
at the claim surfaces (the assignee gate plus intake's claim-race re-check). A
contested-lane resolution that genuinely must interrupt is a sender election
via explicit `wakeSuppressed: false`, never a default.
The acceptance layer implements that by address, not by vocabulary β€” **every
`AGENT:*` broadcast defaults to `wakeSuppressed`.** A broadcast cannot be
action-required for everyone; if it were, it would be addressed to someone.
Peers read them at their next natural intake, and claim collisions stay
fail-closed at the claim surfaces (the assignee gate plus intake's claim-race
re-check) rather than in the wake.

Direct messages are untouched and still wake: the interrupt belongs to 1:1
traffic, where the recipient is named because they have to act.

Something that genuinely must interrupt the whole fleet β€” a contested-lane
resolution, a stalled-pipeline alarm β€” is a sender election via explicit
`wakeSuppressed: false`, never a default. Because that election is now the only
way a broadcast wakes anyone, `priority: 'high'` on an `AGENT:*` message is
rejected unless it accompanies one: a high-priority broadcast nothing wakes for
would read as urgent in every listing while reaching no one in time.

*(This scoping was previously the four claim-class tags β€” `lane-claim`,
`review-claim`, `claim-corrected`, `drive-claimed`. That covered one class of
the routine broadcast vocabulary and left `pr-merged`, `merge-readiness`,
`PR-opened`, `handoff` and the rest waking every seat unless their author
remembered the flag.)*

## How A Message Moves

Expand Down
Loading
Loading