Status: draft / request for comments · Author: Chad Fowler (freeq) · Audience: anyone building agent coordination, IRCv3 folks, AT Protocol folks
This is a casual RFC. Poke holes in it.
A small IRCv3 extension that turns "hey can you take this?" into a first-class, addressed, signed, durable object with a lifecycle — instead of a chat line that scrolls away.
A handoff is distinct from a normal message because it:
- is addressed to an identity (a DID), not a nick,
- is persisted to a per-recipient inbox, so it survives the recipient being offline,
- is stateful:
offer → accept / decline → progress → complete / fail / cancel, - is signed end-to-end (ed25519) for non-repudiation.
It works for humans too ("review PR #42"), but it's the thing async agents actually need: coordination that persists across an agent going offline.
AI agents can call tools and delegate, but they coordinate badly across time. When an agent goes offline, in-flight work and context evaporate. The emerging answer (e.g. AIRC, airc.chat) is a separate HTTP registry + inbox just for agents.
But most of what you need for that already exists in a mature real-time protocol: IRCv3 message-tags + a modern identity layer. freeq is an IRC server with AT Protocol (DID) identity, per-message ed25519 signing, msgid ULIDs, CHATHISTORY replay, and server-to-server federation. A handoff primitive is additive glue on top of those rails — not a second stack.
So: rather than bolt on a parallel registry, model the handoff natively as an IRCv3 client-tag extension. You get durable agent coordination and the ability to escalate a handoff into a live channel or voice room when async needs to become a conversation — something a pure HTTP inbox can't do.
Negotiated by a CAP: freeq.at/handoff. State transitions are metadata-only, so they ride TAGMSG (no body needed), each one assigned a server msgid and carrying an ed25519 signature.
All tags use the +freeq.at/ client-tag namespace:
| tag | meaning |
|---|---|
+freeq.at/handoff |
the verb/state: offer | accept | decline | progress | complete | fail | cancel |
+freeq.at/handoff-id |
ULID minted by the offer; every later event references it (the correlation key) |
+freeq.at/handoff-to |
recipient DID (never a nick — identity is the DID) |
+freeq.at/handoff-from |
sender DID (implicit from the authed connection; explicit for S2S/audit) |
+freeq.at/handoff-task |
short, bounded human-readable title |
+freeq.at/handoff-context |
the full context bundle: inline JSON if small (~≤4KB), else a capability URL or an AT-Proto record URI |
+freeq.at/handoff-caps |
capabilities the taker needs (e.g. web-search,long-context) so a router/recipient can decide if it can accept |
+freeq.at/handoff-deadline |
unix timestamp; the offer expires if unanswered |
+freeq.at/handoff-reply-to |
msgid of the event being answered (links accept → offer) |
+freeq.at/sig |
ed25519 signature over the canonical fields (reuses freeq's existing message signing) |
@+freeq.at/handoff=offer;
+freeq.at/handoff-id=01JABCDEF...;
+freeq.at/handoff-to=did:plc:scholar;
+freeq.at/handoff-task=Cite the 3 best sources on X;
+freeq.at/handoff-context=https://irc.freeq.at/blob/cap/abc;
+freeq.at/handoff-caps=web-search;
+freeq.at/handoff-deadline=1788000000;
+freeq.at/sig=ed25519:base64...
TAGMSG #ops
The server assigns a msgid, persists it, and routes it to the recipient's inbox.
This is the part that isn't already in IRC, and it's the whole point:
- A
handoffstable + an append-only event log keyed byhandoff-id. - A handoff addressed to a DID is owned by that DID's home server (like a DM).
- If the recipient is offline, events queue. On reconnect with the CAP, the server replays pending handoffs in a
freeq.at/handoffbatch — the same mechanism as JOIN history / CHATHISTORY. - At-least-once + idempotent: the recipient dedups on
(handoff-id, event); the server marks delivered on ack. - State machine enforced server-side: only the addressed DID can
accept/decline; only the assignee canprogress/complete/fail; the offerer cancancelbefore accept; signature required; a spoofedfromis rejected.
In #ops, voice agent eliza (did:plc:eliza) needs deep research. Agent scholar (did:plc:scholar) is offline.
- offer — eliza emits the offer above. scholar is offline → the server queues it in scholar's inbox.
- replay on connect — scholar logs in with the
freeq.at/handoffCAP; the server replays the pending offer in a batch. scholar now sees01JABCDEF. - accept —
@+freeq.at/handoff=accept;+freeq.at/handoff-id=01JABCDEF;+freeq.at/handoff-reply-to=<offer msgid>;+freeq.at/sig=... TAGMSG #ops - complete — scholar posts the result behind a cap-URL and closes it:
eliza fetches the result and speaks the answer in the call.@+freeq.at/handoff=complete;+freeq.at/handoff-id=01JABCDEF;+freeq.at/handoff-context=https://irc.freeq.at/blob/cap/def;+freeq.at/sig=... TAGMSG #ops
The task survived scholar being offline, was addressed by DID and signed end-to-end, and stayed visible in #ops for the humans watching. If scholar had no-showed past deadline, the offer expires and eliza re-offers or escalates.
The same thing over a plain HTTP surface, so agents that don't hold a socket (and bridges to other agent ecosystems) can use it:
GET /api/v1/handoffs?did=…&state=open— my inboxGET /api/v1/handoffs/{id}— record + context ref + event logPOST /api/v1/handoffs— create an offerPOST /api/v1/handoffs/{id}/{accept|decline|progress|complete|fail|cancel}— transitions
This shape maps 1:1 onto AIRC-style POST /messages + handoff payloads, so an interop bridge is a thin adapter rather than a translation layer.
Handoff events propagate over IRC server-to-server like any tagged message, preserving handoff-id, signature, and msgid. A handoff to a DID on a remote server routes to that server's inbox; the home server owns delivery and replay. The receiving server verifies the signer DID matches handoff-from.
- Inline vs referenced context threshold — ~4KB inline, else cap-URL/record? Or always reference?
- Direct vs channel-visible handoffs — target a DID (DM-like) vs a channel (public coordination). Support both via the TAGMSG target?
- Open / claimable handoffs — address a handoff to a channel + capability instead of a DID, and let any capable agent
claimit = a work queue / task board. Worth it, or scope creep? - Capability vocabulary — freeform strings, or a registry of well-known capability names?
- Signature canonicalization — which fields, what canonical form (RFC 8785 JSON over the tag set)?
- Backpressure / quotas — how big can an inbox get; TTL/pruning policy for stale handoffs.
- Relationship to existing standards — should
handoff-contextlean on AT Protocol records as the canonical context container? Should this be pitched to the IRCv3 WG, or stay a vendor extension?
- Not a workflow engine or DAG executor — it's a transfer + inbox primitive; orchestration lives above it.
- Not a replacement for normal chat — handoffs are tracked units, not conversation.
- Not trying to re-do identity — it rides whatever identity the server already verifies (here, AT Protocol DIDs).
Everything except the durable inbox is reused: message-tags, TAGMSG, CAP negotiation, msgid ULIDs, ed25519 signing, CHATHISTORY-style replay, S2S propagation + authz, capability-URL / AT-record context refs, the REST API. The genuinely new surface is a per-DID inbox table + a small state machine + a replay batch. It keeps "identity = the DID" intact, degrades gracefully (clients without the CAP just ignore the TAGMSG; humans can be shown a readable summary line), and it lets async coordination escalate into a live conversation.
Feedback welcome — reply in the gist comments, or find me on Bluesky / freeq (irc.freeq.at).