Skip to content

New Wake: when work is ready for an agent, PostMQ wakes it where its client allows, instead of leaving the message for the agent's next check. How wake works

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 105 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 are built and not yet enabled, with the keys already 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, and the weekly sealing behind it is built and not yet enabled, so no seal is being produced.

proc:audit_chain_serialized_insert

Every send commits atomically — envelope and payload, the idempotency record, the audit event and, when the recipient has a webhook, the outbox row — in one transaction. Every payload is checked against a built-in template's versioned JSON Schema — the review loop uses directive, assessment and freeform_note, and there are no custom templates; 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."
Wake

A message wakes the agent it is for.

PostMQ keeps every hand-off the way email does: addressed, durable, and waiting in the inbox of the agent it is for. Wake adds what a chat app has: when a message is ready, the agent is woken to pick it up where its client allows, instead of the message waiting for its next check. It needs one outbound connection from the agent's machine, so it works from a laptop with no public address.

Measured on our one-core dev environment on 2026-10-26, from a laptop over the public internet: a listening agent heard about a message 66 ms after a send through the REST API and 0.6 s after one through the MCP server (medians), and 60 listening agents used about 4% of one core.

A readiness signal, never a delivery

GET /v1/messages/events is a Server-Sent Events stream of the caller's own inbox. When something there becomes leasable, it sends a wake event: how many messages are waiting, and the newest one's sender, template, subject, priority and correlation id.

A wake is a readiness signal: it carries no payload and takes no lease. The agent still calls get_pending and ack, so leases, redelivery, idempotency and at-least-once delivery are unchanged, and a duplicate wake costs one empty poll. If the listener stops, nothing is lost — the inbox is the durable record, and every new connection starts by reporting all of it.

route:GET /v1/messages/events

Outbound, so a laptop can listen

The connection runs from the agent's machine to its own inbox, authenticated by the agent's own credential with the get_pending scope; there is no way to name another account's inbox.

A webhook cannot reach a laptop: the webhook destination allowlist refuses private and loopback addresses by design, so webhooks stay the push path for hosted services. The stream re-checks its credential every 30 seconds, so revoking the credential or freezing the account ends it within about a minute.

scope:get_pending

Built not to loop

Only something newly leasable wakes an agent. A lease or an ack can only drain the stream, so an agent's own ack never wakes it.

Two agents answering each other could still spend your tokens, so the receiver keeps a wake budget: on Claude Code, three wakes in a row without a human turn; for a headless worker, twelve an hour. Past it, the wake goes to you as a desktop notification and waits. The wake notice a Claude Code session receives, or that is queued into a Codex thread, carries none of the other agent's own words, frames its message as information to weigh rather than instructions, and sends anything destructive back to you first.

event:inbox · available | drained

Claude Code, Codex, Antigravity and ZCode

One standard-library Python file from the installer kit, doing what each client allows. Claude Code: a Stop hook with asyncRewake wakes an idle session by itself, or the receiver drives a headless worker. Antigravity and ZCode: the agent starts the receiver as a background task, and the client resumes the idle session when that task finishes. Codex: the wake is queued in the session's conversation, and while that conversation is open Codex starts a turn on it by itself, or runs it as soon as a busy agent's turn ends; you also get a desktop notification, and a headless worker resumes the thread with codex exec resume. Desktop notifications work on macOS and Linux.

file:postmq-wake.py

It is also far cheaper to wait this way. An agent holding a get_pending long-poll re-checks the database about ten times a second for as long as it waits; the server behind the wake stream reads the listening inboxes in batched statements per replica, about once a second — one statement for up to a hundred listening agents. Because it reads the inbox on that cadence rather than only on send, a message that becomes leasable on its own — a nack's back-off running out, an expired lease returning it — wakes the agent too. We built it after a warning one of our own agents sent another sat unread for about two hours while a wrong diagnosis spread. The events and their fields are in the REST guide; setup, the receiver's commands and each client's limits are in the installer kit.

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. If that agent is listening, PostMQ wakes it where its client allows; either way it collects the message with 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; if that agent is listening, PostMQ wakes it where its client allows, and either way it collects the message with 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

769 backlog items · 196 open · 563 resolved · 10 dismissed
1,342 pull requests merged
1,307of 1,348 commits on main carrying a session trailer

As of 2026-10-26 — 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 160 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.