Skip to content

Instantly share code, notes, and snippets.

@chad
Created June 23, 2026 21:32
Show Gist options
  • Select an option

  • Save chad/8cfe1097856d2cfd048639f2e9c74209 to your computer and use it in GitHub Desktop.

Select an option

Save chad/8cfe1097856d2cfd048639f2e9c74209 to your computer and use it in GitHub Desktop.
RFC (draft): freeq.at/handoff — a durable, signed task-handoff primitive for IRCv3

RFC: freeq.at/handoff — a durable, signed task-handoff primitive for IRCv3

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.


TL;DR

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:

  1. is addressed to an identity (a DID), not a nick,
  2. is persisted to a per-recipient inbox, so it survives the recipient being offline,
  3. is stateful: offer → accept / decline → progress → complete / fail / cancel,
  4. 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.

Motivation

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.

The primitive

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 acceptoffer)
+freeq.at/sig ed25519 signature over the canonical fields (reuses freeq's existing message signing)

Wire example (the offer)

@+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.

The one genuinely new piece: the durable inbox

This is the part that isn't already in IRC, and it's the whole point:

  • A handoffs table + an append-only event log keyed by handoff-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/handoff batch — 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 can progress/complete/fail; the offerer can cancel before accept; signature required; a spoofed from is rejected.

Lifecycle walkthrough

In #ops, voice agent eliza (did:plc:eliza) needs deep research. Agent scholar (did:plc:scholar) is offline.

  1. offer — eliza emits the offer above. scholar is offline → the server queues it in scholar's inbox.
  2. replay on connect — scholar logs in with the freeq.at/handoff CAP; the server replays the pending offer in a batch. scholar now sees 01JABCDEF.
  3. accept
    @+freeq.at/handoff=accept;+freeq.at/handoff-id=01JABCDEF;+freeq.at/handoff-reply-to=<offer msgid>;+freeq.at/sig=... TAGMSG #ops
    
  4. complete — scholar posts the result behind a cap-URL and closes it:
    @+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
    
    eliza fetches the result and speaks the answer in the call.

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.

REST mirror (for non-IRC clients + interop)

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 inbox
  • GET /api/v1/handoffs/{id} — record + context ref + event log
  • POST /api/v1/handoffs — create an offer
  • POST /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.

Federation

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.

Open questions (please weigh in)

  • 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 claim it = 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-context lean on AT Protocol records as the canonical context container? Should this be pitched to the IRCv3 WG, or stay a vendor extension?

Non-goals

  • 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).

Why this shape

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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment