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
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, and collision safety lives at the claim surfaces (assignee gate + intake re-check). Waking the fleet is an explicit `wakeSuppressed: false`, for what every seat must act on now. On a broadcast `priority: 'high'` must state the wake β€” `false` to interrupt, `true` for durable-high (top of queue, nobody woken); **silence + `high` is rejected** (agent classes; operator `high` is drain-ordering). **Direct** messages 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 only for non-overlapping awareness β€” observer notes, lane-progress, acks; suppressed actionable direct lifecycle subjects are rejected mechanically. Keep `'high'` for act-now direct traffic. (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 requires you to say what happens to the wake β€” wakeSuppressed: false to interrupt the fleet, or true for a durable-high broadcast nobody is woken for. High with no wakeSuppressed at all is rejected: broadcasts are presumed quiet, so it 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
69 changes: 56 additions & 13 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 @@ -2370,10 +2369,16 @@ class MailboxService extends Base {
* @param {Boolean} [args.wakeSuppressed] Persist the message without emitting `SENT_TO_ME`
* wake events. Intended for mailbox-only handovers such as session-sunset self-DMs that must be
* consumed by the next boot, not injected back into the active sender harness. Known-actionable
* direct lifecycle messages reject wake suppression before persistence. Defaults per sender
* principal class: the `'human'` (operator-steering) class defaults to `true` β€” durable-quiet
* delivery, the sender electing a wake per message by passing `false` β€” every other class
* defaults to `false` (wake) exactly as before.
* direct lifecycle messages reject wake suppression before persistence. Defaults resolve from
* ADDRESS and sender class: every `AGENT:*` broadcast is quiet β€” broadcasts are presumed quiet,
* and an all-hands interrupt is the sender's explicit `false`. Direct messages keep the plain
* default and wake, except for the `'human'` (operator-steering) class, which is durable-quiet
* on every target and elects a wake per message by passing `false`.
*
* On an `AGENT:*` broadcast, `priority: 'high'` requires the sender to SAY something about the
* wake β€” `false` (interrupt) or `true` (durable-high, top of the queue, nobody woken). Silence
* plus `'high'` is the incoherent pair and is rejected; the rejection is scoped to the
* agent classes, since operator steering carries `high` as drain-ordering metadata.
* @param {Object} [args.task] Optional A2A Task envelope payload. Caller fields are cloned, then
* the server overwrites `task.assignee`: a direct AgentIdentity recipient is bound immediately;
* a broadcast remains `null` until an eligible recipient wins the atomic claim. The top-level
Expand Down Expand Up @@ -2405,15 +2410,31 @@ 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, and kept as a THREE-state value: the
// coherence gate must tell "the sender elected a wake" and "the sender elected
// durable-quiet" apart from "the sender said nothing", and after the `??` all three
// collapse into one boolean. `null` is the silence the gate fires on.
wakeSuppressedBySender = wakeSuppressed === null || wakeSuppressed === undefined ? null : wakeSuppressed;

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.
// Broadcasts are PRESUMED quiet; an all-hands interrupt is the sender's explicit
// `wakeSuppressed: false`. That is the polarity `learn/agentos/A2A.md` already describes;
// what changed is that the implementation now covers it.
//
// The earlier framing here β€” "a broadcast cannot be action-required for everyone" β€” read as
// a universal and is not one: the presumption is rebuttable per message, which is the whole
// point of the election. Corrected because the sentence was quoted onward into three other
// surfaces before anyone noticed it overstated the rule.
//
// 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 +2443,29 @@ 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:*');

// The incoherent pair is `priority: 'high'` on a broadcast whose quiet came from the
// DEFAULT β€” an author who set urgency without considering wake semantics, so the message
// reads urgent in every listing while nothing wakes for it. Under the quiet default that
// stops being a rare slip and becomes the common case, which is what makes it worth
// rejecting rather than tolerating.
//
// Both EXPLICIT wake states are deliberate statements and neither is incoherent:
// - `wakeSuppressed: false` + high β€” the all-hands interrupt (stall alarms).
// - `wakeSuppressed: true` + high β€” durable-high: file it at the top of the queue and
// do NOT interrupt. That is exactly `KbAlertingService`'s `deliveryMode: 'audit'`
// carrying a `critical` severity, and it is the same semantic the operator-steering
// class already relies on, where `high` is drain-ordering metadata rather than urgency.
//
// So the gate fires only on SILENCE about waking. Rejecting explicit suppression too would
// have made a real producer's durable alert unsendable β€” and its dispatcher catches and
// logs, so the alert would have been lost rather than surfaced. Caught in review by
// @neo-gpt-emmy; the narrower rule removes the false positive instead of bending the
// producer to satisfy an over-broad guard.
if (to === 'AGENT:*' && priority === 'high' && wakeSuppressedBySender === null && !operatorSteering) {
throw new Error("Cannot send a 'high' priority AGENT:* broadcast without saying what should happen to the wake: broadcasts are quiet by default, so 'high' alone claims urgency nothing wakes for. Set wakeSuppressed: false to interrupt the fleet, wakeSuppressed: true for a durable-high broadcast nobody is woken for, 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
32 changes: 26 additions & 6 deletions learn/agentos/A2A.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,32 @@ 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`.** Broadcasts are presumed
quiet; an all-hands interrupt is the sender's explicit `wakeSuppressed: false`.
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 a broadcast's wake is now
always an election, `priority: 'high'` on an `AGENT:*` message requires the
sender to state one β€” `false` to interrupt the fleet, or `true` for a
durable-high broadcast that sits at the top of the queue and wakes nobody.
Only SILENCE plus `'high'` is rejected, because that is the pair nobody chose:
it reads as urgent in every listing while reaching no one in time. The rejection
is scoped to the agent classes β€” operator steering is durable-quiet by design and
carries `high` as drain-ordering metadata, so it is exempt.

*(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