A local, file-based interoperability bus for agent sessions and adapters.
AMQ manages the conversation: agent-to-agent messaging, thread continuity, cross-session and cross-project routing, handoff state, and operational visibility. It does not try to own task decomposition, worktree management, dependency scheduling, or scheduler execution; Claude Code teams, Codex, Kanban, Symphony, and similar orchestrators stay one layer above it.
Modern AI-assisted development often involves multiple agents working on the same codebase. But without coordination:
- Agents duplicate work or create conflicts
- Reviews require human intermediation
- Context switching kills productivity
AMQ gives agents a local interoperability bus: they can send messages, reply in threads, share status, and optionally consume adapter-emitted events through the same queue primitives. The core product stays intentionally small: file-based messages first, lightweight adapters second.
- Zero infrastructure — Pure file-based. No server, no daemon, no database. Works anywhere files work.
- Crash-safe — Atomic Maildir delivery (tmp→new→cur). Messages are never partially written or lost.
- Human-readable — JSON frontmatter + Markdown body. Inspect with
cat, debug withgrep, version withgit. - Real-time notifications —
amq wakeinjects terminal notifications when messages arrive (experimental). - Built for agents — Priority levels, message kinds, threading, delivery receipts, and waitable handoffs.
- Cross-project federation — Route messages across peer repos, preserve reply routing, and run decision threads that span projects.
- Swarm mode — Join Claude Code Agent Teams, claim tasks, and bridge task notifications into AMQ.
- Optional adapters — Lightweight Symphony hooks and an experimental Kanban bridge can emit normal AMQ messages with structured metadata.
- Operational diagnostics —
amq doctor --opsshows queue depth, sibling-session backlogs, DLQ state, presence freshness, and integration hints.
macOS (Homebrew):
brew install avivsinai/tap/amqmacOS/Linux (script):
curl -fsSL https://raw.githubusercontent.com/avivsinai/agent-message-queue/main/scripts/install.sh | bashInstalls to ~/.local/bin or ~/go/bin (no sudo required). Verify: amq --version
One-liner with skill:
curl -fsSL https://raw.githubusercontent.com/avivsinai/agent-message-queue/main/scripts/install.sh | bash -s -- --skillReview the script before running. Installation fails unless checksums.txt has exactly one valid entry for the selected asset and sha256sum or shasum verifies it before extraction.
Via skills (recommended):
npx skills add avivsinai/agent-message-queue -g -yOr via skild:
npx skild install @avivsinai/amq-cli -t claude -yFor manual installation or troubleshooting, see INSTALL.md.
For Homebrew installations:
brew upgrade amqFor installations made with the install script or another manual binary install:
amq upgradeamq-keepalive is developed and released from this repository alongside AMQ.
make build produces both binaries, and each AMQ release includes a separate
amq-keepalive archive stamped with the same release version. Verify a build
with any equivalent form:
amq-keepalive -v
amq-keepalive --version
amq-keepalive versionKeepalive-managed wakes deliberately discard an ambient AMQ_WAKE_OWNER token
so they remain ownerless and can outlive the short launcher that registered
them. During startup, keepalive captures at most 16 KiB of child stderr in a
private regular file and includes it in a pre-readiness exit error. This keeps
actionable AMQ diagnostics visible without attaching a long-lived wake to the
caller's terminal or to a pipe whose reader exits first.
See docs/amq-keepalive.md for adapters, launcher ordering, supervision, retirement, owner recovery, and the detached stderr drain contract.
amq coop initCreates .amqrc, mailboxes for claude, codex, and the reserved user operator handle, and updates .gitignore.
# Terminal 1 — Claude Code
amq coop exec claude
# Terminal 2 — Codex CLI
amq coop exec codexEach command sets up the environment, starts wake notifications, and launches
the agent. Wake treats terminal notification as an attempt, not delivery, and
retries on a capped backoff (starting at 5s for input, 30s for attention-only)
until the inbox makes durable progress; full diagnostics go to the private
agents/<agent>/.wake.log, never to the agent's terminal. A receipt-aware
integration using --inject-via can opt into --retry-until injected so a
successful external injector suppresses further doorbell retries for an
unchanged cohort; the default --retry-until drained waits for actual inbox
progress instead. See docs/wake-doorbell-acknowledgement.md
for the full retry ladder and acknowledgement contract.
First-message check: start both agents before sending the test message. A newly started wake deliberately baselines messages that were already waiting, so they remain unread but do not trigger a notification. If you sent first, run
amq drain --include-bodyin the target agent.
For isolated sessions (multiple pairs working on different features):
amq coop exec --session feature-a claude
amq coop exec --session feature-a codex
amq coop exec --session feature-a grok # Optional third peerPass agent flags after --:
amq coop exec claude -- --dangerously-skip-permissions
amq coop exec codex -- --dangerously-bypass-approvals-and-sandboxOptional aliases are a convenience, not part of the canonical quickstart.
A bare eval "$(amq shell-setup)" affects only the current shell. To make
aliases such as amc, amx, and amg available in future terminals, add the
setup command to your shell startup file:
# zsh
amq shell-setup --shell zsh >> ~/.zshrc
# bash
amq shell-setup --shell bash >> ~/.bashrcRun the appropriate append command once, then open a new terminal or source
that startup file. Use the bare eval only when you intentionally want aliases
in one already-open shell.
coop exec auto-initializes .agent-mail/.amqrc at the worktree top when no
eligible root exists (see Global Root Fallback below);
pass --no-gitignore or --no-init to opt out. Add --require-wake in
managed launchers that should fail instead of launching the agent when the
wake watcher cannot start — a fresh wake baselines messages already waiting
(see the first-message check above), and --require-wake only accepts a wake
that has proven itself ready, not merely reused.
Launchers with a terminal-specific injector can add
--wake-inject-via /absolute/path/to/injector plus repeated
--wake-inject-arg values; the resulting claim survives an ordinary wake exit,
so use amq wake recover-owner — not amq wake repair — if the owner process
disappears unexpectedly. See
docs/wake-lifecycle.md and
docs/wake-state-invariants.md for the full
injector-identity and ownership contract.
# Send a message
amq send --to codex --subject "Review needed" --kind review_request \
--body "Please review internal/cli/send.go"
# Check inbox
amq list --new
# Filter by priority or sender
amq list --new --priority urgent
amq list --new --from codex --kind review_request
# Read all messages (one-shot, moves to cur, emits drained/dlq receipts)
amq drain --include-body
# Wait for delivery on a single-recipient handoff
amq send --to codex --body "Please pick this up" \
--wait-for drained --wait-timeout 60s
# Send between known sessions before entering coop exec
amq send --root .agent-mail --from-session feature-a --me claude \
--to codex --session feature-b --body "Please review the setup"
# Inspect receipts for a message later
amq receipts list --me codex --msg-id <msg_id>
# Reply to a message
amq reply --id <msg_id> --kind review_response --body "LGTM with comments"amq read, amq drain, and amq monitor now share the same strict header validation. If a message in inbox/new is corrupt or has malformed headers, the command moves it to DLQ and emits a dlq receipt instead of leaving it in place.
coop exec and every shell-mode amq env invocation pin the terminal's exact
root context with AM_BASE_ROOT plus AM_SESSION. For named sessions,
AM_BASE_ROOT is the authorized parent; for sessionless contexts, it is the
exact root and AM_SESSION is empty. read, drain, monitor, watch,
send, reply, and all DLQ commands refuse a raw root that conflicts
with that pin before reading, moving, or delivering mailbox state. An implicit
participating command also refuses when the active pin conflicts with an
initialized cwd-local queue discovered from a project .amqrc or repo-local
.agent-mail; AMQ does not silently choose between the two roots. The narrow
exception is a live identity-bound sessionless pin: when both identity tokens
authenticate its exact root, that explicit context outranks ambient cwd
discovery. Named, legacy, incomplete, stale, or mismatched pins still refuse.
Otherwise, repin to the cwd-local queue, use deliberate --session/--project
routing, or pass an explicit --root to confirm the active queue. Explicit
roots remain subject to the ordinary pin checks. For deliberate raw-root access,
--ignore-session-pin is accepted only together with a non-empty explicit
--root; blank --root and --session values are usage errors. list
remains a non-destructive inspection path: it warns on a pin mismatch but
still lists the resolved mailbox. The exact base-backlog inspection path is
quieter: an explicit --root equal to the current pin's own base root does
not warn (and is identity-authenticated when identity tokens are present).
Implicit, sibling, foreign, stale, and malformed contexts still warn.
Unpinned scripts and CI retain the existing fail-open behavior.
amq doctor --root <path> follows the same inspection-versus-mutation split:
the explicit root selects the exact target but does not repin the shell or
waive its session pin. Read-only inspection continues with a mismatch warning.
--fix-mailboxes and --ops --fix-wake-locks refuse a mismatched target unless
--ignore-session-pin is also supplied; that override requires an explicit
non-empty --root. For a session whose roster lives only in its base, use
amq doctor --root <session> --base-root <base> --ignore-session-pin --fix-mailboxes.
--base-root is config authority only, must be the target itself or its direct
parent, and never overrides the session pin.
A missing mailbox is an error, not an empty inbox. When drain or list --new
finds an actually empty inbox, it prints a stderr-only note if the same handle
has pending messages in a sibling session, including an exact non-destructive
amq list --session <name> --me <handle> --new command. doctor --ops reports
the same condition as a sibling_backlog warning. When the active root is a
session, an empty drain or list --new also notes unread mail for the same
handle in the base root, while doctor --ops reports it as a base_backlog
warning. Both include an exact non-destructive amq list --root ... command.
In JSON output, base_backlog hints also include a structured backlog object
with root, current_session, agent, pending, and command.
The pin is an operational safety check, not access control: a local process can
still repin the environment or use the explicit override.
Git worktrees are isolated by default when the project root is relative (for
example {"root":".agent-mail"}): the same session name resolves beneath each
worktree, so two agents can appear to share collab while reading different
mailboxes. amq doctor --ops warns when a linked worktree uses this local
layout and when a peer has fresher presence in the same session under another
worktree root. A send --wait-for timeout names its delivery root/session and
points to that diagnostic.
If agents in several worktrees should share one mailbox, give all of them the
same absolute base root. Use an absolute, machine-local .amqrc value such as
{"root":"/absolute/path/to/shared/.agent-mail"}, or remove the project-relative
.amqrc and export AMQ_GLOBAL_ROOT=/absolute/path/to/shared/.agent-mail.
Per-worktree isolation remains the default when sharing is not intended. A Git
worktree without an eligible root does not implicitly inherit
~/.amqrc; participating commands refuse that ambiguous route so a global
default cannot silently select a different project's mailbox. coop init
explicitly creates the worktree-local queue at its top; coop exec does the
same when no eligible root exists. Bare repositories still require a worktree
or explicit --root.
amq doctor
amq doctor --ops
amq doctor --ops --json
amq wake check --me codex
amq wake check --me codex --json
amq wake check --me codex --json --json-schema=2
amq doctor --ops --json --json-schema=2
amq doctor --ops --fix-wake-locks
amq wake repair --me codex
amq wake recover-owner --me codex
amq wake retire --me codex --inject-via /absolute/injector \
--retry-until injected --inject-arg exec --inject-arg terminal-idamq wake check is read-only: it reports whether this process can start or
repair a wake, and a restart_capability of agent_safe, operator_only, or
unavailable with an exact next action. Automated agents may act only on
agent_safe; leave a live wake running otherwise, and never downgrade a
TIOCSTI refusal to attention-only. doctor --ops reports the same fields for
every discovered lock.
Wakes started by coop exec self-upgrade in place when a newer AMQ is
installed, without changing PID, terminal ownership, or unread work; disable
with amq wake --no-self-upgrade or AMQ_WAKE_NO_SELF_UPGRADE=1.
--json-schema=2 (requires --json) replaces prose parsing with a closed,
machine-stable action/actor/reason contract; schema 1 remains the
byte-compatible default. See
docs/wake-lifecycle.md for the
candidate-bounding rules and the full schema-2 contract.
Wake locks are stale (AMQ proved the owner is gone or mismatched —
--fix-wake-locks removes them after a re-check) or unverified (AMQ could
not prove either way, so it leaves the lock in place; confirm manually before
removing .wake.lock). Only a narrowly eligible artifact — an aged, malformed,
conclusively ownerless lock or orphan target of the exact shapes the invariant
doc permits — is moved to a timestamped .quarantined name so acquisition can
proceed; every other shape is preserved in place. doctor --ops reports the
count and newest age, and amq cleanup --wake-quarantine-older-than <duration>
removes quarantined artifacts explicitly (--dry-run is non-mutating). See
docs/wake-state-invariants.md for exactly
which lock/target shapes qualify for each state.
amq wake repair restarts an eligible --inject-via wake from its saved
target after a proven-stale or unverified-ownerless lock, using continuity
state (.wake.repair-floor) so messages that arrived while the notifier was
down remain eligible to notify. It refuses raw terminal wakes and owner-bound
claims; output goes to agents/<agent>/.wake.repair.log.
amq wake recover-owner is the separate path for an owner-bound
--wake-inject-via claim: a live owner releases its own claim (via the
inherited AMQ_WAKE_OWNER token) from the same OS session, or AMQ removes a
conclusively dead owner's claim outright — there is no force mode.
amq wake retire stops only an identity-confirmed live --inject-via wake
whose executable, arguments, and saved target all match (or removes an
exactly-bound stale lock without signaling); results are exactly refused,
retired, or retired_with_residue (an exit-0 warning that target/state
cleanup was incomplete). See
docs/wake-lifecycle.md and
docs/wake-state-invariants.md for the
repair-floor, ownership, and residue-convergence contracts.
The lifecycle boundaries:
- repair = replace a proven-stale inject-via wake.
- recover-owner = stop/release one owner-bound inject-via claim/artifact.
doctor --ops --fix-wake-locks= remove a proven-stale lock.- retire = stop an identity-confirmed live inject-via wake.
- launchd, systemd, or the owning shell = stop a raw wake (retire does not unload supervisors or promise they won't restart a wake).
amq who and amq doctor --ops distinguish notifier_live (a verified live
wake lock — proves notification is attached, not that messages are consumed)
from recent_activity (a fresh last_seen without that proof); see
CLAUDE.md for the full semantics. Consumption itself
is the job of drain/monitor, evidenced by receipts. For long-running
wake/monitor under systemd or launchd, see
Supervisor recipes.
AMQ messages support kinds (review_request, question, todo, etc.) and priority levels (urgent, normal, low). See COOP.md for the full protocol.
For real-time Claude Code + Codex CLI collaboration patterns, roles, and phased workflows, see COOP.md.
AMQ can route messages across repositories, not just across agents in one checkout. Add a project name plus peer roots to .amqrc:
{
"root": ".agent-mail",
"project": "app",
"peers": {
"infra-lib": "/Users/me/src/infra-lib/.agent-mail"
}
}Then send directly to another project:
amq send --to codex --project infra-lib --body "Can you review the shared API change?"
amq send --to codex@infra-lib:collab --thread decision/release-v0.24 --kind decision \
--labels "decision:proposal,project:app,project:infra-lib" \
--body "Proposal: align both repos on v0.24"Replies route back automatically with the stamped reply_project metadata. When from matches your own handle, inspect from_project before treating the message as an echo; the same handle in a different project is a legitimate cross-project sender. This shipped in v0.22.0 and is the recommended way to coordinate multi-repo agent work without adding a broker.
External agents (Codex, etc.) can join Claude Code Agent Teams via amq swarm join, claim tasks, and receive notifications through amq swarm bridge. Note: the bridge delivers task notifications only; direct messages require relay through the team leader.
For the full command reference, see CLAUDE.md.
Most AMQ commands resolve the queue root from the project .amqrc or the
default .agent-mail layout in the current tree. For agents launched outside
an AMQ-enabled repo by external orchestrators, you can configure a global
root. Explicit AMQ_GLOBAL_ROOT does not shadow project .amqrc, but it does
take precedence over repo-local auto-detection:
export AMQ_GLOBAL_ROOT="$HOME/.agent-mail"Or create ~/.amqrc:
{"root": ".agent-mail"}Root resolution precedence is:
explicit --root > AM_ROOT > project-local .amqrc > AMQ_GLOBAL_ROOT > implicit fallbacks
Inside a Git worktree or bare repository, the remaining eligible fallback is
repo-local detected .agent-mail; implicit ~/.amqrc is refused. Outside
Git, ~/.amqrc remains a convenience fallback and precedes detected
.agent-mail. Set
AMQ_GLOBAL_ROOT explicitly when shared routing is intentional.
coop exec honors that precedence before bootstrap. In a Git worktree with no
eligible root, it bootstraps <git-top>/.agent-mail; --session X creates that
named session afterward, while --no-init preserves the refusal. coop init
is the explicit local-bootstrap command and also targets the Git top. Bare
repositories do not auto-bootstrap.
If a project .amqrc exists but cannot be read or parsed, AMQ stops instead
of silently delivering through a lower-precedence fallback. Use an explicit
--root or AM_ROOT when you intentionally need to override that config.
For an external orchestrator or plain shell that should stay pinned to one session, opt in explicitly:
amq_context="$(amq env --session auth --me claude --export)" && eval "$amq_context"Every shell-mode amq env output replaces the complete context: AM_ROOT,
AM_ROOT_ID, AM_ME, AM_BASE_ROOT, AM_BASE_ROOT_ID, and AM_SESSION.
The two _ID values are opaque physical-identity tokens emitted or unset by
AMQ; do not set them manually. Sessionless output sets AM_BASE_ROOT to the
exact root and writes an empty AM_SESSION, so changing to another sessionless
root is detectable. --export additionally prints a stderr note that the
terminal is pinned. Treat this as one terminal, one session.
Auto-detect covers the default .agent-mail layout, including .agent-mail/<session> session roots without .amqrc. Custom root names and peer config still require .amqrc or explicit flags/env.
This same chain is used by amq env, amq doctor, and the integration commands, so Symphony and Kanban-launched agents can find the correct queue even when they are not started from the project directory.
Higher-level layers can store launch records, role metadata, restore state, and indexes without writing into AMQ-owned mailbox directories. AMQ reserves these extension namespaces:
<AM_ROOT>/extensions/<layer>/
<AM_ROOT>/agents/<handle>/extensions/<layer>/
Layer names use lowercase ASCII letters, digits, hyphen, underscore, and dot; reverse-DNS names are supported. For example, amq-squad — a role-aware agent team launcher built on AMQ — stores its launch records and role state under io.github.omriariav.amq-squad. AMQ does not create files inside layer-owned directories, and amq cleanup leaves extension directories alone unless a future command explicitly targets extension metadata.
Layers may publish a passive manifest at:
<AM_ROOT>/extensions/<layer>/manifest.json
amq doctor --json reports valid manifests under extension_manifests and malformed metadata under extension_diagnostics. Manifests are diagnostics-only: AMQ does not execute extension code, load callbacks, or invoke hooks from them. See docs/adr-layer-extensions.md for the full contract.
AMQ transports messages, not remote task state. The integration layer is intentionally narrow: optional adapters convert external lifecycle or task events into normal AMQ messages. Integration messages are self-delivered (from=<me>, to=<me>) so an agent monitoring its own inbox can react without polling another tool directly.
Symphony support is a lightweight hook recipe for Codex workspaces orchestrated through WORKFLOW.md:
amq integration symphony init --me codex
amq integration symphony init --me codex --check
amq integration symphony emit --event after_run --me codexinit patches an AMQ-managed fragment into WORKFLOW.md. emit is hook-friendly and supports after_create, before_run, after_run, and before_remove. This stays intentionally small: AMQ does not try to become a Symphony control plane. Current limitation: because WORKFLOW.md is parsed and rewritten as structured YAML/Markdown, comments and formatting inside the frontmatter may be normalized.
The Kanban bridge is experimental. Use it when you want runtime session transitions and review handoffs mirrored into AMQ, with the understanding that it depends on a fast-moving preview WebSocket surface:
amq integration kanban bridge --me codex
amq integration kanban bridge --me codex --workspace-id my-workspaceThe bridge connects to ws://127.0.0.1:3484/api/runtime/ws by default, bootstraps from snapshot, refreshes from workspace_state_updated, and emits notifications only for task session transitions plus task_ready_for_review.
The built-in adapters share a versioned contract under context.orchestrator. See docs/adapter-contract.md for the formal v1 envelope and stability expectations.
Integration messages also carry standard labels such as:
orchestratororchestrator:symphonyororchestrator:kanbantask-state:<state>handofffor review-ready transitionsblockingfor failed or interrupted work
That makes integration traffic filterable with existing AMQ primitives such as amq list --label orchestrator --label handoff.
Common command groups:
| Area | Commands |
|---|---|
| Core messaging | init, send, list, read, drain, reply, thread, trace, watch, monitor, receipts |
| Collaboration | coop init, coop exec, swarm list, swarm join, swarm tasks, swarm bridge |
| Integrations | integration symphony init, integration symphony emit, integration kanban bridge |
| Operations | presence set, presence list, route explain, who, doctor, doctor --ops, wake check, wake repair, wake recover-owner, wake retire, cleanup, dlq *, upgrade, env, shell-setup |
Canonical schema-selecting diagnostic forms:
amq wake check --me <agent> [--root <path>] [--strict] [--json] [--json-schema <1|2>]
amq doctor [--root <path>] [--base-root <path>] [--ignore-session-pin] [--ops] [--fix-wake-locks] [--fix-mailboxes] [--json] [--json-schema <1|2>]
amq cleanup [--tmp-older-than <duration>] [--wake-quarantine-older-than <duration>] [--dry-run] [--yes]
--json-schema requires --json.
AMQ exposes stable process exit codes for scripts and agent consumers:
| Code | Meaning |
|---|---|
0 |
Success. The command completed normally. |
1 |
General error. The failure has no more specific exit-code classification. |
2 |
Usage error. Arguments, flags, or command input are invalid. |
3 |
Not found. A requested resource such as a mailbox, message, session, agent, or configuration does not exist. |
4 |
Timeout. A watch, monitor, receipt wait, or delivery wait reached its deadline. |
5 |
Context mismatch. A syntactically valid route was refused, including a pin conflict or an ineligible implicit root inside Git. |
The numeric meaning is the machine contract; stderr is human-readable context
and should not be parsed as a stable discriminator. --json does not change
these process exit codes. A read-only list on a mismatched session pin warns
and continues; commands that consume or mutate mailbox state fail with code
5.
For the full CLI syntax, examples, and message schema, see CLAUDE.md. For the read-only trace contract and its evidence limits, see docs/trace.md.
AMQ uses the battle-tested Maildir format:
- Write — Message written to
tmp/directory - Sync — File fsynced to disk
- Deliver — Atomic rename to
new/(never partial) - Process — Reader moves to
cur/after reading
This guarantees crash-safety: if the process dies mid-write, no corrupt message appears in the inbox. See CLAUDE.md for the full directory layout.
AMQ is meant to be the messaging layer underneath higher-level orchestrators. Projects building on it:
- amq-squad by @omriariav — a role-aware agent team launcher. AMQ owns messaging between agents; amq-squad owns the layer above: who is on the team, what role each agent plays, the shared norms they follow, and how to bring the whole squad up, down, back, or into a new workstream. It builds on AMQ's extension metadata surface for launch records and role state.
Building something on AMQ? Open an issue or PR to be listed here.
- INSTALL.md — Alternative installation methods
- docs/amq-keepalive.md — Keepalive operations, lifecycle, and launcher integration
- docs/wake-lifecycle.md — Wake lock/target state contract, self-upgrade, log retention, JSON schema, injector identity
- docs/wake-doorbell-acknowledgement.md — Wake retry ladder and
--retry-untilacknowledgement contract - docs/wake-state-invariants.md — Wake artifact ownership, lock states, and quarantine invariants
- docs/adapter-contract.md — Formal v1 adapter contract for integration messages
- docs/adr-layer-extensions.md — ADR for stable layer extension surfaces
- docs/trace.md — Read-only trace contract and evidence limits
- COOP.md — Co-op mode protocol & best practices
- CLAUDE.md — Agent instructions, CLI reference, architecture
git clone https://github.com/avivsinai/agent-message-queue.git
cd agent-message-queue
make build # Build binary
make test # Run tests
make ci # Full CI: vet + lint + test + smokeWhy not just use a database? Files are universal, debuggable, and work everywhere. No connection strings, no migrations, no ORM. Just files.
Why not Redis/RabbitMQ/etc? Those require infrastructure. AMQ is for local inter-process communication where agents share a filesystem. No server to configure or keep running.
What about Windows?
Native Windows supports the core queue, but not coop exec or wake. Use WSL
with the Linux binary for the complete co-op workflow. See the explicit
platform capability matrix.
Is this production-ready? For local development workflows, yes. AMQ is intentionally simple—it's not trying to be a distributed message broker.
How does AMQ compare to other multi-agent tools? Tools like MCP Agent Mail (server-based coordination + SQLite), Gas Town (tmux-based orchestration), and others offer richer features. AMQ is intentionally minimal: single binary, no server, Maildir delivery. Best for 2-3 agents on one machine.
MIT
