ai-memory wake-hub
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.
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.
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.
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.
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.
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.
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.
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.
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
| Kind | Wire | Direction | Max payload | Carries |
|---|---|---|---|---|
| hello | 1 | both | 1,024 B | hub→client: 32-byte challenge nonce. client→hub: public key, transcript signature, asserted topics. |
| welcome | 2 | hub → client | 256 B | Session handle, the coalesced pending summary, jittered reconnect guidance. |
| join | 3 | client → hub | 1,024 B | Signed, nonce-bound membership join. |
| depart | 4 | client → hub | 1,024 B | Signed, nonce-bound membership end. Disconnect is not depart. |
| subscribe | 5 | client → hub | 1,024 B | Add topics to this session’s subscription set. |
| unsubscribe | 6 | client → hub | 1,024 B | Remove topics from this session’s subscription set. |
| wake | 7 | hub → client | 256 B | The hint itself: {inbox_row_id, namespace, sender, digest, seq_high_watermark}. Never a body. |
| ping | 8 | both | 32 B | Liveness probe. |
| pong | 9 | both | 32 B | Liveness response. |
| error | 10 | hub → sender | 256 B | Refusal, always to the sender, never a silent success. |
| request / reply / notify | 11 / 12 / 13 | REMOVED 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. | ||
| Bound | Value | Why |
|---|---|---|
| Wire magic / version | AWH1 / 1 | Bad magic or version is a 400 and closes the connection. |
| Frame header | 24 B | Fixed; everything after the length prefix. |
| Max frame | 1,304 B | 24 + (2 × 128) + 1,024. Enforced once, in the codec’s max_frame_length. |
| Max agent id | 128 B | Matches ai-memory ids. Over-long ids are refused, never truncated. |
| Wake hint | ≤ 256 B | Prompts a catch-up inbox read; the bounded poll lets a lost wake self-heal. |
| Max payload | 1,024 B | Per-kind ceilings are tighter still, so a wake can never be inflated to hello size. |
| Topics | 8/frame · 32/session · 64 B each | Topics are scoped to the subscriber’s namespace read scope. |
| Crypto field sizes | nonce 32 B · pubkey 32 B · sig 64 B · digest 32 B | Ed25519 throughout; the wake digest binds the hint to the row. |
| Token bucket (post-auth) | 500 frames/s · burst 2,000 | Per connection. Fan-out is charged to the sender, so amplification cannot hide from the cap. |
| Token bucket (pre-auth) | 4 frames/s · burst 8 | An unauthenticated peer gets almost no budget. |
| Handshake timeout | 5,000 ms | A stalled handshake cannot hold an fd. |
| Per-recipient queue | 256 frames and 64 KiB | Bounded in bytes, not just frames. Overflow returns 507 to the sender — loudly, never a silent drop-oldest. |
| Hub-wide egress budget | 32 MiB | A global ceiling above the per-recipient ones, so no fan-out pattern can sum past it. |
| Connection ceiling | 256, clamped by RLIMIT_NOFILE | Derived 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 shards | 16 | Sharded session/topic tables; no .await is ever held under a lock. |
| Coalesced pending set | 1,024 agents × 64 ids | Offline state is a set (count + id set + lagged marker), ~4 KiB per agent — never a payload ring. |
| Reconnect | 250 ms base + ≤ 750 ms jitter | Jittered, with a handshake semaphore, so 256 agents do not rejoin in lockstep. |
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.
| Code | Name | Raised when |
|---|---|---|
| 400 | malformed | Bad magic, bad version, a reserved byte set, bad lengths. |
| 401 | unauthorized | Every identity refusal, collapsed to one uniform string. |
| 403 | forbidden | Authenticated, but the frame is not permitted — notably a from that is not the identity bound at hello. |
| 404 | unknown destination | No such agent, and none known to the hub. |
| 409 | replaced | A second session claimed this agent id; this session is being replaced. |
| 413 | too large | A frame exceeded a length bound. |
| 429 | rate limited | The connection’s token bucket is empty. |
| 500 | internal | The hub hit an internal error handling the frame. |
| 507 | overflow | A per-recipient queue, or the hub-wide egress budget, is full. |
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.
0600, owner-only parent directory, posture enforced at write and at load.SO_PEERCRED on Linux, LOCAL_PEERPID + getpeereid on macOS.a2a-hub/join/v1, ≤ 12 hnot_after — a lifetime of at most 12 hours.owner.pub. The allowlist is a derived cache, not a registry; allow and revoke are emitted into the ai-memory audit spine.
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.
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.
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.
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.
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.
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.
≤256 B wake metadata, payload frames removed, coalesced pending set, token bucket with fan-out charged to the sender.a2a-hub/join/v1 delegation minted by the enrolled agent key and verified by the hub; from bound to the hello key; domain-separated transcript; nonce-bound join/depart; allow and revoke into the audit spine.
#3469 — bus sink mergedSubscribe the hub to the agent_notified broadcast bus — in-process when co-hosted with the daemon, a UDS sink when separate. Never the webhook lane.
#3470 — client mergedai-memory wake-listen (long-lived; exec hook or line output on wake; catch-up read on welcome) and ai-memory inbox --wait; minimal Python and TypeScript SDK clients.
#3471 — ops mergedMetrics (connected agents, queue bytes, drops, fan-out p99, slow consumers, wake latency), health probe, SIGTERM drain, systemd unit + launchd plist with fd limits, wake-hub --posture and doctor check.
#3472 — cert + SSOT mergedCertification §6 NOT-COVERED line (transport-only, content-free, loss degrades latency only), removal-proof row, docs page, glossary, changelog, architecture entry, route/tool/param pins.
#3473 — acceptance openWake latency p50/p99 at 128 and 256 agents, hub vs GET /api/v1/inbox/stream, on both fleet hosts; read/write-path A-B-A-B with the hub on and off; hub kill under load loses no inbox rows.
unread_only inside the queryMerged: unread filtering runs inside the query before LIMIT on both backends, so a page of newer read rows cannot hide older unread rows.
#3464 — challenge-response key bindingProof of possession for bind_agent_pubkey, append-only key history, revocation gating subkey verification — inside the durable identity root, never at the hub.
#3465 — agent_notified event + inbox streamA write event on both notify funnels, an in-process broadcast bus, GET /api/v1/inbox/stream modelled on the approvals SSE bus, and a UDS sink hook.
The planning issues below are historical scope links, not an implementation progress checklist. The ai-memory implementation has shipped; acceptance measurement #3473 remains open.
SO_PEERCRED by default; the shipped hub has no TCP mode; cross-host rows use ai-memory federation; RLIMIT_NOFILE and the connection ceiling set at start-up.
#2 — spec v2: frames implemented in ai-memoryWake-only, ≤256 B metadata; request/reply/notify removed from v1; offline state is a coalesced pending set, never a payload ring.
#3 — spec v2: identity implemented in ai-memoryScoped delegation from the enrolled key; from bound to the hello key; domain-separated transcript; nonce-bound join/depart; ids to 128 bytes; topics scoped to namespace read scope.
#4 — packaging + acceptance implemented in ai-memoryShipped as an ai-memory wake-hub subcommand + unit file; certification §6 line, removal proof, doctor check, SSOT reconciliation; acceptance A/B against the inbox stream remains open in #3473.
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.
Everything the design rests on is committed in this repository. Nothing below is a summary of a document that does not exist.
unsafe posture and advisories. Plus the audit checklist, had Plus the audit checklist had “buy” won.ldquo;buyPlus the audit checklist had “buy” won.rdquo; won.
v1 design spec (superseded)The original A2A-only hub design that the vote reviewed and rewrote. Kept for the record; spec v2 above is current.
ai-memory — documentation siteThe substrate the hub wakes: attested, endpoint-resident memory for AI agents. Typed memory, a knowledge graph with temporal validity, signed reflections, programmable hooks.
ai-memory — A2A messagingThe durable side of agent-to-agent: memory_notify inbox rows and memory_subscribe webhooks. The record the wake hint points at.
github.com/alphaonedev/rust-a2aThis repository — protocol, specs, the vote record, and the open design questions.
github.com/alphaonedev/ai-memory-mcpThe daemon. src/wake_hub/ holds the implementation; the EPIC and every sub-issue live in its tracker.