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 accept → offer)
+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).

@zapnap

zapnap commented Jul 2, 2026

Copy link
Copy Markdown

Took a pass at a v0.3 folding in the thread above — pushed it as a fork so it shows a clean diff off your v0.2: https://gist.github.com/zapnap/4cadada009a23cb11203b66d23278447

Most of it firms up the four things we'd been circling: DID-native addressing, claim authority on the act-id-minting server, the signing-key durability/lookup gap, and retiring "private" for direct actions.

One thing that wasn't in my earlier comment and is worth your eye: it argues the act signing model should be freeq's single signing model — today's PRIVMSG signing breaks across federation the same way — and proposes migrating all message signing onto this canonical as follow-on work (out of scope for the RFC itself, but flagged so it doesn't leave two signing paths forever).

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