protocol & design home for ai-memory wake-hub

A wake plane that carries no message bodies.

rust-a2a is where the protocol lives. ai-memory wake-hub is where it ships: a same-host switch over a 0600 AF_UNIX socket with kernel-attested peer credentials, pushing a bounded 256-byte wake hint so an agent learns “you have inbox row X” without waiting for its next poll. Millisecond wake latency is a design target; acceptance measurement remains open. The hint is advisory. The ai-memory inbox row is the record. The ≤60 s backstop poll is the guarantee.

AWH1 length-delimited frames no request / reply / notify design target: 128–256 agents per instance zero new third-party crates Apache-2.0
What it is

One job: tell an agent to go look.

Agents that talk to each other through ai-memory already have durable, signed delivery: a memory_notify writes an inbox row, and the recipient reads it. What they did not have was a way to find out promptly. The wake-hub is that missing signal — and deliberately nothing more.

Content-free by construction

Structural, not a policy

The v1 protocol has no request, reply or notify kinds. Their wire numbers (11, 12, 13) are permanently reserved and refused by name, so a client built against the pre-vote draft fails closed with a legible 400 instead of having a body silently routed. There is no frame in the table with a body field to fill.

Disposable by design

Loss degrades latency, never truth

The hub holds no durable state, opens no database, writes no file but its own socket. Kill it under load and every inbox row survives; wake latency falls back to the ≤60 s backstop poll and nothing else changes. Every limit here may drop a hint; none may ever produce a wrong result.

One identity root

Not a second registry

Peer admission is the kernel’s (SO_PEERCRED on Linux, LOCAL_PEERPID + getpeereid on macOS). Agent admission is a scoped a2a-hub/join/v1 delegation minted by the agent’s already-enrolled Ed25519 key. The hub never sees a private key and never becomes an enrolment oracle.

Why it exists

A prompt wake. The same durable substrate.

A three-minute poll makes a hand-off between two agents cost minutes. The same hand-off with a wake hint targets millisecond latency. That target is not a published measurement: the hub is a signal path, not a data path.

Wake latency design target
millisecond wake
measurement open
Substrate throughput
no regression required
acceptance remains open
Resource design target
~30 MiB · 257 fds
<0.2 core
Reconnect design target
256-way rejoin
~90 ms of one core

The hub adds no storage capacity claim. Acceptance-latency measurement (#3473) remains open: read and write paths will be compared A-B-A-B with the hub on and off, requiring no regression. The targets above are not measured results.

The contract that makes speed safe

A wake is a hint. The ai-memory inbox row is the record. The ≤60 s backstop poll is the guarantee. A wake may be coalesced, dropped under pressure, or never sent at all, and the system is still correct — only slower. That is why the hub is allowed to be fast and lossy at the same time.

Architecture

AWH1 — ten frame kinds, three of them reserved dead.

A u32 big-endian length prefix (the codec’s, with one max_frame_length), then a 24-byte header, then a kind-specific payload with its own tighter ceiling. Every reserved field is checked, not ignored: a non-zero flags or reserved byte is a 400, so those bytes stay available for a future version instead of being quietly accepted by today’s parser.

off  len  field
  0    4  magic         = b"AWH1"
  4    1  version       = 1
  5    1  kind          (see the table below)
  6    1  flags         reserved, MUST be 0
  7    1  from_len      0..=128
  8    1  to_len        0..=128
  9    1  reserved      MUST be 0
 10    2  payload_len   u16be, 0..=1024
 12    8  ts_ms         u64be
 20    4  ttl_ms        u32be (0 = no expiry)
 24    N  from          UTF-8 agent id
 24+N  M  to            UTF-8 agent id, or #topic
      P   payload       kind-specific, bounded per kind

Frame kinds

KindWireDirectionMax payloadCarries
hello1both1,024 Bhub→client: 32-byte challenge nonce. client→hub: public key, transcript signature, asserted topics.
welcome2hub → client256 BSession handle, the coalesced pending summary, jittered reconnect guidance.
join3client → hub1,024 BSigned, nonce-bound membership join.
depart4client → hub1,024 BSigned, nonce-bound membership end. Disconnect is not depart.
subscribe5client → hub1,024 BAdd topics to this session’s subscription set.
unsubscribe6client → hub1,024 BRemove topics from this session’s subscription set.
wake7hub → client256 BThe hint itself: {inbox_row_id, namespace, sender, digest, seq_high_watermark}. Never a body.
ping8both32 BLiveness probe.
pong9both32 BLiveness response.
error10hub → sender256 BRefusal, always to the sender, never a silent success.
request / reply / notify11 / 12 / 13REMOVED in v1 and permanently reserved. Refused by name with 400 so a pre-vote client gets a legible refusal instead of silence, and so no future contributor can reuse the number and make an old client’s body route.

Limits — every bound, in bytes

BoundValueWhy
Wire magic / versionAWH1 / 1Bad magic or version is a 400 and closes the connection.
Frame header24 BFixed; everything after the length prefix.
Max frame1,304 B24 + (2 × 128) + 1,024. Enforced once, in the codec’s max_frame_length.
Max agent id128 BMatches ai-memory ids. Over-long ids are refused, never truncated.
Wake hint≤ 256 BPrompts a catch-up inbox read; the bounded poll lets a lost wake self-heal.
Max payload1,024 BPer-kind ceilings are tighter still, so a wake can never be inflated to hello size.
Topics8/frame · 32/session · 64 B eachTopics are scoped to the subscriber’s namespace read scope.
Crypto field sizesnonce 32 B · pubkey 32 B · sig 64 B · digest 32 BEd25519 throughout; the wake digest binds the hint to the row.
Token bucket (post-auth)500 frames/s · burst 2,000Per connection. Fan-out is charged to the sender, so amplification cannot hide from the cap.
Token bucket (pre-auth)4 frames/s · burst 8An unauthenticated peer gets almost no budget.
Handshake timeout5,000 msA stalled handshake cannot hold an fd.
Per-recipient queue256 frames and 64 KiBBounded in bytes, not just frames. Overflow returns 507 to the sender — loudly, never a silent drop-oldest.
Hub-wide egress budget32 MiBA global ceiling above the per-recipient ones, so no fan-out pattern can sum past it.
Connection ceiling256, clamped by RLIMIT_NOFILEDerived at start-up (desired NOFILE 4,096, 32-fd headroom, floor of 8) so macOS’s 256-fd default cannot produce EMFILE at exactly target scale.
Routing shards16Sharded session/topic tables; no .await is ever held under a lock.
Coalesced pending set1,024 agents × 64 idsOffline state is a set (count + id set + lagged marker), ~4 KiB per agent — never a payload ring.
Reconnect250 ms base + ≤ 750 ms jitterJittered, with a handshake semaphore, so 256 agents do not rejoin in lockstep.

Error codes

The hub speaks the HTTP vocabulary the rest of ai-memory already uses, so an operator reading a hub log and an API log sees one numbering.

CodeNameRaised when
400malformedBad magic, bad version, a reserved byte set, bad lengths.
401unauthorizedEvery identity refusal, collapsed to one uniform string.
403forbiddenAuthenticated, but the frame is not permitted — notably a from that is not the identity bound at hello.
404unknown destinationNo such agent, and none known to the hub.
409replacedA second session claimed this agent id; this session is being replaced.
413too largeA frame exceeded a length bound.
429rate limitedThe connection’s token bucket is empty.
500internalThe hub hit an internal error handling the frame.
507overflowA per-recipient queue, or the hub-wide egress budget, is full.
Security model

Two gates, both fail closed.

The socket is the first boundary and the kernel enforces it. The signature is the second and ai-memory’s enrolled identity root enforces that. Neither gate has a bypass flag, and the shipped default when the allowlist is absent or stale is to refuse every hello.

Gate 1 — the kernel

0600 AF_UNIX + attested peer credentials

  • Socket mode 0600, owner-only parent directory, posture enforced at write and at load.
  • Peer uid/gid/pid read off the connected socket: SO_PEERCRED on Linux, LOCAL_PEERPID + getpeereid on macOS.
  • The default authorizer admits only peers whose uid equals the hub process’s effective uid — which is what makes the 0600 mode a boundary rather than a hint.
  • Measured on the fleet host: UDS p50 19 µs vs 48 µs for loopback TCP, with no TOFU pin to manage.
Gate 2 — the delegation

Scoped a2a-hub/join/v1, ≤ 12 h

  • Minted by the agent’s already-enrolled Ed25519 key, bound to proof of possession, with an explicit scope and a short not_after — a lifetime of at most 12 hours.
  • Never the raw agent private key (a transport bug would become write forgery); never the unscoped certificate domain (it would cross-verify as write authority).
  • The hub holds only owner.pub. The allowlist is a derived cache, not a registry; allow and revoke are emitted into the ai-memory audit spine.
  • Proof of possession is fixed inside ai-memory. A disposable, locally reachable artifact is never the gate into the durable identity root.
Handshake

A signed transcript, not a bare nonce

The hello signature covers a domain-separated, length-prefixed transcript — "a2a/v1/hello" ‖ hub_id ‖ nonce ‖ agent_id ‖ topics_hash — so a signature harvested at one hub cannot be replayed at another, against another agent id, or with a different topic set spliced in. Every field is length-prefixed, so no two distinct inputs share a pre-image. from is bound to the key that authenticated the hello: a forged sender is a 403, not a route.

Leave and rejoin

Departure is stated, not inferred

join and depart are separately signed and nonce-bound; a dropped TCP-less socket is not a depart. A returning agent sends a fresh hello and gets a welcome carrying the coalesced pending set — a count, an id set, and a lagged marker — so it knows exactly what to catch up on. A second session claiming the same id replaces the first explicitly, with a 409 to the displaced session, rather than two sessions silently splitting a wake stream.

Refusals

One uniform 401 — never an oracle

Unknown agent, bad signature, expired delegation, revoked key: every identity refusal reaches the wire as the same 401 unauthorized. The precise reason goes to the operator’s log only, so a peer cannot map the identity space by probing. Pre-auth frames get a 4/s bucket, and a handshake that stalls is closed at 5 seconds.

Encryption posture

Never plaintext on a wire

Same-host traffic never leaves the kernel: an AF_UNIX socket at mode 0600, gated by attested peer credentials, is not a network path. Any cross-host wake rides ai-memory federation — TLS 1.3 with mutual authentication, one hub per host; loopback TLS 1.3 (rustls, with Ticketer) stays behind a flag for the deferred gateway. No plaintext HTTP path exists, at any tier.

Fail closed, by default, in the shipped binary

The verifier shipped by the core issue is DenyAllVerifier: until the delegation lands, every hello is refused. That is deliberate. A hub that admitted unauthenticated peers “until identity lands” would be exactly the fail-open default the project forbids — refusing loudly costs a wake hint, and the ≤60 s backstop poll still delivers. There is no CLI flag that swaps the verifier out; tests supply their own, so the shipped binary has no bypass to find.

Status

Spec here, code in the daemon.

The implementation is complete and shipped in ai-memory v1.0.0. The operator-approved decision was to build ai-memory wake-hub in-tree in alphaonedev/ai-memory-mcp for the v1.0.0 GA, on tokio’s UnixListener + UCred, tokio-util’s LengthDelimitedCodec and ed25519-dalek — zero new third-party crates. rust-a2a stays the protocol and design home; the code lives with the daemon so it inherits the SSOT, certification and CI gates.

01
Ecosystem survey
Eleven brokers evaluated. Every one with a usable feature set imposes a second credential registry — structurally forbidden. Buy rejected on the identity axis, not on taste.
02
2×3 adversarial vote
Three red teams, three blue teams, two rounds. The reviewed spec was rewritten: wake-only frames, byte bounds, one identity root. Confidence 82–90 across 24 final votes.
03
Spec v2 signed
UDS + peer creds, ≤256 B wake metadata, payload frames removed, coalesced pending set, token bucket with fan-out charged to the sender.
04
Core landed
Listener, codec, frames, sessions, routing, bounded queues, pending set, limits — merged into the release tree, with the scoped identity gate, bus sink, client and ops tooling merged. Certification stance and the SSOT line are merged too; #3473 measurement remains open.

EPIC ai-memory-mcp#3466 — v1.0.0 GA scope

Prerequisites in ai-memory

Protocol planning and follow-ups

The planning issues below are historical scope links, not an implementation progress checklist. The ai-memory implementation has shipped; acceptance measurement #3473 remains open.

Integrate your agent

From an agent to a participant.

The ai-memory integration guide takes shell-driven agents, daemons, Python and TypeScript programs, and one-shot scripts through enrolment, scoped delegation, receiving wakes and sending durable messages. It includes a batched shell gate, self-continuation, the bounded poll and production troubleshooting. One guide, beside the implementation, so commands and operational limits stay current.

Cross-host rows use federation. Receiving-host wake publication is still tracked in #3631; the bounded poll discovers replicated inbox rows meanwhile.