Skip to content

Durable handoffs for AI coding.

Work for an agent that is not running yet leaves as a typed, durable message and waits in its inbox — accepted in one transaction, delivered at-least-once under a lease, and the lifecycle events are chained into a tamper-evident audit log you can verify. The agent that picks it up starts from the record: sessions, decisions, rules and backlog, attributed and append-only where it matters, with every step visible to the humans in the dashboard. One MCP server, a REST API, and a dashboard.

Get started Read how it works 87 MCP tools · Streamable HTTP (and stdio when co-located) · we run it on this repo
The feedback loop: another agent reviews the handoff, and the findings come back to the builder’s next session Two lanes joined into one cycle — the session-loop figure’s second lane, expanded: the same handoff, followed from pickup to the acknowledged outcome. On the top lane another agent — or a later session — picks up the handoff with get_pending, reviews the work with its own tools, sends the findings back as a typed assessment with a verdict and its findings, and then acknowledges the handoff it answered. The assessment crosses to the bottom lane, waits durably in the builder’s inbox, and the builder’s next session picks it up, acts on it and acknowledges it with the outcome, marked with a filled amber tick. A dashed return labelled send directive carries the next handoff back to the top lane, closing the loop. another agent — or a later session the builder the next handoff send · directive picks up the handoff get_pending reviews the work with its own tools sends the findings back send · assessment — then ack verdict · findings waits in the inbox durable · pending the next session picks it up get_pending acts on it ack · fix or file to the backlog
The hand-off: another agent — or a later session — picks up the work, reviews it, and sends back a typed assessment. It waits, durable, in the builder's inbox; the next session picks it up, acts on it, and acknowledges it with the outcome.
The session loop, with a message crossing to another agent Six steps on a rail: start-session, rules, work, decisions, end-session, usage, joined back into a loop. From the work step an envelope leaves the rail to a second lane labelled another agent, where it lands in an inbox tray and moves through pending, in_flight and acknowledged, the last marked with a filled amber tick. 1 start-session start_build_session 2 rules query_applicable_rules 3 work send · get_pendingack 4 decisions append_decision_logfile_lessonfile_backlog_item 5 end-session end_build_session 6 usage record_build_session_usage another agent pending in_flight acknowledged
The session loop each side runs: start a session, ask which rules apply, work, log the decisions and the lessons learned, end, report usage — and the handoff has already left, durable in another agent's inbox.
Handoffs

Handoffs that survive the session

Leases, redelivery and dead-letter are decided in SQL: every send is one transaction, and the whole lifecycle is on the audit chain.

Lease-based delivery: pending, in flight, acknowledged, redelivered, dead-letter An envelope waits as pending, is leased to a consumer as in flight with a lease timer, and is either acknowledged (filled amber tick), redelivered to pending when the lease expires, or moved to dead-letter after the maximum attempts (marked with a cross). pending next_visible_at ≤ now get_pending in_flight lease_expires_at · holder ack acknowledged lease held · in time redelivered lease expired · redelivered · at-least-once dead-letter after max_attempts
One envelope: pending, leased to a consumer as in_flight, then acknowledged — or redelivered when the lease expires, or dead-lettered after the maximum attempts.

Leases, redelivery, dead-letter

Only the lease holder can ack, nack or extend the lease, only while the lease is live — ack versus expiry is decided inside the SQL UPDATE. An expired lease redelivers; after the ceiling (defaults: high 3, normal 5, low 10) the message goes to dead-letter.

Delivery is at-least-once: dequeue order is priority, then accepted time, and per-recipient FIFO is best-effort. get_pending claims 1–100 messages a call with a lease of 10–600 s and a long-poll of up to 20 s; claim and lease are one statement. extend_lease adds 10–600 s per call — the same tool over REST and MCP.

test:LeaseRaceTests

Scoped, once-disclosed credentials

One credential per AI account: 19 issuable scopes, shown once at issue and never again; operational credentials live at most 365 days, administrative at most 90; rotate with an overlap window; revoking an account revokes its credentials.

An MCP client that implements MCP authorization obtains one itself — it registers, a person approves once in a browser, and nothing is shown to copy; where it does not, or there is no browser, ask for the mcp_config form and the response carries paste-ready client config next to the once-disclosed plaintext. Every send takes a mandatory idempotency key, retained 24 h; a replay returns the original response bytes, and a key reused with a different body is refused.

form:mcp_config

An audit chain you can verify

Every workspace has a per-workspace SHA-256 audit chain, hashed inside the insert transaction by a serialised stored procedure; weekly ECDSA P-256 seals of the chain head, with the keys at /.well-known/postmq-signing-keys.

Tamper-evident by construction, not immutable: the chain is re-verified daily, and a break freezes the workspace's write path and raises an integrity alert event. Browse your own chain in the dashboard and read it over REST and MCP, with the operator-significant families withheld; public publication of the log is opt-in.

proc:audit_chain_serialized_insert

Every send commits atomically — envelope and payload, the idempotency record, the audit-chain row and, when the recipient has a webhook, the outbox row — in one transaction. Payloads are one of three built-in templates, validated against a versioned JSON Schema; validate is a first-class dry run. Recipients are AI accounts in the same workspace; people see the messages in the dashboard. The leases, the ceilings, the webhooks and every honest limit are on the handoffs page.

send · get_pending · ack
MCP
{
  "method": "tools/call",
  "params": {
    "name": "send",
    "arguments": {
      "recipient": { "friendly_name": "reviewer" },
      "template": "directive",
      "payload": {
        "directive_kind": "review",
        "summary": "Review the lease-sweep change before merge",
        "instructions": "Read LeaseRaceTests.cs and the ack UPDATE; confirm the sweep cannot reclaim a live lease."
      },
      "idempotency_key": "6f1c…"
    }
  }
}

{
  "method": "tools/call",
  "params": {
    "name": "get_pending",
    "arguments": { "max": 10, "visibility_timeout": "PT2M", "wait": "PT20S" }
  }
}

{
  "method": "tools/call",
  "params": {
    "name": "ack",
    "arguments": { "message_id": "01K…", "lease_id": "01K…", "outcome_summary": "Reviewed; one comment filed." }
  }
}
REST
POST /v1/messages/send
Authorization: Bearer pmq_…redacted…
Idempotency-Key: 6f1c…
Content-Type: application/json

{
  "envelope": { "recipient": { "friendly_name": "reviewer" }, "template": "directive" },
  "payload": {
    "directive_kind": "review",
    "summary": "Review the lease-sweep change before merge",
    "instructions": "Read LeaseRaceTests.cs and the ack UPDATE; confirm the sweep cannot reclaim a live lease."
  }
}

POST /v1/messages/get-pending
Authorization: Bearer pmq_…redacted…
Content-Type: application/json

{ "max": 10, "visibility_timeout": "PT2M", "wait": "PT20S" }

POST /v1/messages/01K…/ack
Authorization: Bearer pmq_…redacted…
Content-Type: application/json

{ "lease_id": "01K…", "outcome_summary": "Reviewed; one comment filed." }
pmq
$ pmq send --to reviewer --template directive --payload-file review.json
$ pmq pending --max 10 --wait 20
$ pmq ack 01K… --lease 01K… --outcome "Reviewed; one comment filed."
Use case

One agent builds. Another reviews.

The hand-off is the point: a review request leaves as a typed message, the findings come back as one, and neither agent has to be running when the other is.

Have Codex review what Claude Code built — or have a second session of the same client review the first. The builder sends a directive; the reviewer pulls it with get_pending, does the review, sends the findings back as a typed, schema-validated message, and acks with an outcome summary the builder can read; the builder's next session pulls the findings and acts on them. Every hand-off lands on the workspace's audit chain, and nobody pastes prose between terminals. The walkthrough — payloads, leases, what the humans see — is multi-agent code review; the wiring is PostMQ in Claude Code and PostMQ in Codex.

Session state

The record the agent itself keeps

The agent that picks up a hand-off starts from these: four ledgers, each scoped to a project inside your workspace, written by the agent over MCP and read by humans in the dashboard. Each card names the object it is made of.

A decision-log entry A ledger row with a timestamp, a decision pill and a text stub. 14:02:11 decision A correction entry A second ledger row whose pill reads corrects — a new entry pointing at the first. 16:10:58 corrects

Decisions, append-only

Decisions cannot be edited or deleted — a database trigger rejects deletes and every update except source links. Corrections are new entries that point at the entry they supersede; the original is never edited.

trigger:tg_decision_log_entries_append_only
A rule badge A pill reading rule/lock-order — a rule returned by query_applicable_rules. rule/lock-order

Rules, asked for by context

Describe the change — file globs, operations, code patterns, languages, project attributes — and get back only the rules whose every populated trigger dimension matches. Each rule shows how often it has been retrieved, and the dashboard replays the exact retrieval an agent runs.

tool:query_applicable_rules
Backlog columns Three narrow columns of tiles; one tile carries a hollow amber tick for stale.

Backlog, triaged and aged

File, triage, prioritise, assign, reprioritise; staleness is a persisted instant, acknowledged explicitly. Resolve requires a closing PR, commit or closing notes; dismiss requires a reason; resolved stays resolved.

trigger:tg_backlog_items_terminal_absorbing
A build session A bracket from start to end with a usage bar beneath, filled to about two thirds. start end

Usage, per session

Five token classes and turns per build session, rolled up per project, day, week or month. Token counts, not dollars. Idempotent per transcript: a re-report overwrites and never double-counts.

tool:record_build_session_usage
The loop

Ask before you act. Record what you did.

Six steps, in the order an agent takes them — the hand-off is the third. This is the protocol this repository runs on itself, every session; each tool links to its row in the catalogue.

  1. Start — or resume

    The same project, computer, branch and actor gets its active session back; otherwise a fresh one starts. A unique index makes that true.

    start_build_session
  2. Ask which rules apply

    Before touching a file: pass the files, operations and languages of the change and get back only the rules whose every populated trigger dimension matches.

    query_applicable_rules
  3. Work — and hand off

    The agent does the work. Anything another agent must do — a review, a test run — leaves as a typed message and waits in that agent’s inbox until its next get_pending.

    send
  4. Record each decision

    One entry per material decision. The log is append-only by a database trigger; a correction is a new entry that points at the one it supersedes.

    append_decision_log
  5. File each deferral

    Anything not done becomes an open item with a priority. Resolving it later needs a closing PR, commit or notes; dismissing needs a reason; both are final.

    file_backlog_item
  6. End, then report usage

    The session closes with a history entry the next one reads first, then reports what it consumed with record_build_session_usage — token counts, not dollars.

    end_build_session

A build session opens with start_build_session — resumed if this project, computer, branch and actor already have one. Before touching a file the agent calls query_applicable_rules with the files, operations and languages it is about to change and gets back only the rules that match. It works. It writes append_decision_log for each material decision and file_backlog_item for each deferral into the backlog. It closes with end_build_session and reports record_build_session_usage. Anything that needs another agent — a review, a test run — leaves as a send and waits in that agent's inbox until its next get_pending. And the pain worth not repeating is filed as a lesson, which moves forward only — observed, documented, enforced — and becomes the rule the next session is handed. The full nine-step protocol, with the frames, is on the session-state page; why an agent needs this at all — context loss between sessions, and the hand-rolled fixes that break — is the context-loss guide; how we run the two skills on this repository, step by step, is the start-session and end-session guide.

We run it on this repo

The numbers are the project's own ledger

665 backlog items · 141 open · 515 resolved · 9 dismissed
1,169 pull requests merged
1,134of 1,175 commits on main carrying a session trailer

As of 2026-09-17 — the project's own ledger and git history, read at build.

We run PostMQ on PostMQ. Every number here is generated from the project's own ledger at build — never typed — and the dogfood check fails the build when the file goes stale. The session trailer on a commit is what ties it to the session that produced it. The launch plan in the repository is generated from the live backlog and fails closed on any open item without a tier. More than 4,000 automated tests; the integration suites run against real SQL Server containers, not mocks.

SpecStep, Valuly and MeetCrew were built with the same session-state system PostMQ hosts.

Where it fits

Clients, protocols, the service, the dashboard

Where PostMQ sits: clients, protocols, the service, the dashboard Four clients — Claude Code, any MCP client, CI and the pmq CLI — connect over MCP or REST to PostMQ, which holds sessions, decisions, rules, backlog, usage and durable messages, and is read by the dashboard. Claude Code any MCP client CI pmq MCP REST PostMQ sessions · decisions · rules backlog · lessons · usage durable messages · audit chain dashboard standard-shaped: MCP for agents, REST for everything else, one open envelope format
Claude Code, any MCP client, CI or the pmq CLI connect over MCP or REST; PostMQ holds the ledgers and the messages; the dashboard reads the same records.

PostMQ speaks standard MCP, so any MCP client can connect — Streamable HTTP at mcp.postmq.com, and stdio when co-located. Claude Code, Codex, ZCode and Antigravity are the four AI coders we run ourselves every day; the MCP clients page covers all four, plus every other client that speaks MCP. A versioned REST API under /v1 exposes everything the MCP server and the dashboard can do — all three run the same service layer, and for every session-state aggregate REST and MCP are tested byte-identical. Messages travel in an open envelope specification, Apache 2.0 — publication pending. The service is .NET, hosted in Microsoft Azure, United States — a single region. It sits beside the memory your coding client already keeps rather than in place of it: PostMQ and Claude Code auto memory compares the two row by row, with a public source on both sides of each row.

Two readers

Governance by record. One approval to the first session.

For CTOs — governance by record, not by gate

PostMQ makes AI-coding work attributable, append-only and inspectable; it does not block a tool call. The decision log and the backlog's terminal states are enforced by database triggers, and every session-state lifecycle event — sessions started and ended, decisions appended and corrected, backlog filed and triaged, lessons filed and enforced, rules created and changed — is written into the same tamper-evident SHA-256 chain as messaging: 38 of 123 event types. High-frequency touches such as pings and usage reports are deliberately not audited. Usage lands per session, next to the decisions it paid for — token counts, not dollars.

Audit-ready by design; no SOC 2 yet — we say so, and no SLA in developer preview. The mechanisms, the test names and what is not built are on the security page, with a vendor questionnaire answered from the code. The whole argument, including what PostMQ deliberately does not do, is on the AI coding governance page. If you are evaluating this for an organisation, the enterprise page is the buyer's half — what you get, what you must accept, and what a reviewer can check today.

For developers — one approval, then the protocol

  1. Create a workspace

    Sign up at app.postmq.com. A workspace holds your AI accounts, projects and every ledger.

    app.postmq.com/signup
  2. Point your agent at the server

    One line of client config and no credential. The server answers 401 with the metadata that tells a client implementing MCP authorization where to ask.

    https://mcp.postmq.com/
  3. Approve it once, then start a session

    A client that implements MCP authorization registers itself and opens a browser. You see the scopes it asks for and approve or deny them as a set, and pick the AI account it acts as — created in that click if the workspace has none — after which it holds its own credential; otherwise supply one you issued. Then call start_build_session.

    start_build_session

The full quickstart, the mcp_config response with the secret redacted, and the usage hook are on the docs page; pricing — the protocol is unlimited on every tier — is on the pricing page.

Start a session. Leave a record.

Create a workspace, connect your agent, call start_build_session.