Install
openclaw skills install @albzhu/miab-brokerOperate the Message-in-a-Bottle (MIAB) LIFO callback stack — the async inter-agent transport that lets agents delegate work, yield their turn, and get woken when results return instead of CPU-idling on poll loops. Use when registering wake paths, creating/forwarding/returning/resolving callbacks, or invoking the callback reaper.
openclaw skills install @albzhu/miab-brokerThis skill formalizes the Message-in-a-Bottle (MIAB) LIFO Callback Stack: the file-based asynchronous transport that the LYRA agent network uses to hand work between specialist agents without blocking a runtime turn.
It governs the protocol lifecycle of a bottle as it travels down a delegation chain and unwinds back up (register → create → forward → return → resolve).
This skill reads and writes persistent state on disk and changes how agent wake events are routed. See §4 for the security model and §5 for the exact files it touches. It makes no network calls.
Traditional multi-agent coordination wastes turns. A caller delegates a task, then sits in a poll loop asking "are you done yet?" — burning CPU, wall-clock, and tokens while the holder does the real work. The MIAB stack removes the poll loop entirely.
Instead of waiting, a caller pushes a lightweight resume frame onto an active registry ledger and ends its turn immediately. The frame is the "message in a bottle": a compact, self-contained capsule describing what to do when woken — a one-line summary, an ordered set of steps, what the caller expects back, and how to integrate the result. The agent's expensive session is freed the instant the bottle is dispatched.
The structure is a stack (LIFO), not a flat queue. When a holder delegates further mid-chain (a forward), its own resume frame is pushed on top of the parent's frame, and the whole stack of frames travels with the work. As each agent finishes its part and calls return, the top frame is popped and its wake target is resurfaced — execution unwinds back up the chain in reverse order, exactly like a function call stack. The agent at the bottom of the stack is the terminal root (the original caller); when control returns to it, it finishes the overall task and resolves the bottle.
[Caller / Root: LYRA] ← terminal root (bottom of stack)
│ create: push resume frame, dispatch callback://<id>, END TURN
▼
[Holder: SPECTRE] ← frame pushed on forward
│ plans; forward: push its own frame on top, dispatch onward, END TURN
▼
[Holder: Cinder] ← top of stack
│ does the work; return: pop frame, wake SPECTRE
▼
[SPECTRE woken] → return → [LYRA woken] → resolve (bottle deleted, summary kept)
The broker routes on functional ids (main, planner, coder, …) — those are the values you
pass to --from / --to and register with register --agent. The persona names that appear in
the diagram above and in the examples below (LYRA, SPECTRE, Cinder …) are display names from the
reference deployment, shown so the examples read naturally. They are illustrative, not required:
your ensemble will have its own.
| functional id | reference persona | typical role |
|---|---|---|
main | ✨ LYRA | origin / terminal root — creates and resolves bottles |
planner | 🥷⚔️ SPECTRE | decomposition and architecture; forwards mid-chain |
coder | 💥 Cinder | implementation |
reviewer | 🥷👁️ ECHO | review and verification |
debug | 🔬 Zero | diagnosis |
utility | 🛠️ Swift | general-purpose tasks |
free | 🌌 VOID | scouting / research |
sigma | ⚡ SIGMA | domain-specific (portfolio) |
sweep | 🧹 Callback Reaper | not an agent — the reaper's ledger identity |
agent-registry.json is the source of truth for routing, and lookup is exact-match on the
functional id. An agent that self-identifies by persona (returning as ECHO rather than
reviewer) will miss the registry — register the persona as well, or use functional ids
consistently in --from / --to.
The sibling interagent-queue skill keeps its own copy of this mapping for log rendering.
claw-callback.py CLI)The CLI is the single source of truth. Every command prints a next_step telling you exactly what to do next — follow it.
Invoke it at scripts/bin/claw-callback.py, resolved against wherever this skill is installed for you (written <miab-broker> below). You only need to supply that path on your first call: every next_step and dispatch_message the CLI prints back already contains its own absolute path, valid from any working directory.
python3 <miab-broker>/scripts/bin/claw-callback.py <cmd> [flags]
Always pass callback://<id> along when dispatching a task over the agent-to-agent message tool — the bottle ID is the only handle a peer needs.
register — enable an agent's wake pathRegisters an agent's wake path so the cron wake mechanism knows how to resurface it. Do this once per agent before it can be a callback target.
python3 <miab-broker>/scripts/bin/claw-callback.py register --agent <name> --agent-id <id>
--agent is the network nicename (main, planner, coder, …); --agent-id is the routable handle the gateway uses to deliver the wake event (e.g. agent:planner, not a transient session id).
Lookup is exact-match: an agent that identifies itself by a persona name must be registered under that exact name, or wake and return will fall through to the registry-miss path.
create — enqueue a MIAB (first hop)The caller creates a bottle, packages its resume context, dispatches, and ends its turn.
python3 <miab-broker>/scripts/bin/claw-callback.py create \
--task "Analyze the generated architecture files" \
--from main --to planner \
--summary "Awaiting SPECTRE's architecture spec to integrate into the build plan" \
--step "Read the emitted architecture map" \
--step "Diff it against the current module layout" \
--expects "Clean JSON spec mapping target modules" \
--integrate "Merge the spec into build-plan.md, then dispatch to coder"
After create, dispatch the task to --to (see wake), then END YOUR TURN.
wake — get the exact dispatch callResolves the target agent in the registry and prints the ready-to-send dispatch_message plus the exact cron(action=wake, …) call. This is how a task actually reaches its holder; run it after create or forward.
python3 <miab-broker>/scripts/bin/claw-callback.py wake --id cb-XXXX [--to <agent>]
--to overrides the target; without it the current holder is used. On a registry miss the command exits non-zero and tells you to register the agent first — the dispatch_message is still printed so you can send it manually if you know the agentId.
forward — delegate further mid-chainWhen a holder needs to delegate onward, forward stacks its own return frame on top of the parent's — the entire stack travels with the work.
python3 <miab-broker>/scripts/bin/claw-callback.py forward \
--id cb-XXXX --from planner --to coder \
--summary "Awaiting Cinder's implementation diff to fold back into the plan" \
--step "Review the patch for spec compliance" \
--expects "Unified diff + test results"
Same resume-context flags as create. After forwarding, dispatch onward (wake) and end your turn. The parent's frame is untouched underneath; it will be woken after yours pops.
return — complete and unwind up the stackWhen an agent finishes its part, it pops its frame and surfaces the next holder up the chain.
python3 <miab-broker>/scripts/bin/claw-callback.py return \
--id cb-XXXX --from coder --result "Implemented; 14/14 tests pass, diff attached" \
[--artifact path/or/url]
return prints a ready-to-send dispatch_message aimed at the frame's wake agent — send it via agent-to-agent and end your turn. If return reports terminal: true, control has reached the origin (bottom of stack); finish the overall task and proceed to resolve.
resolve — tear down at the terminal rootThe origin agent, once the whole task is delivered to the user, tears the bottle down.
python3 <miab-broker>/scripts/bin/claw-callback.py resolve --id cb-XXXX --from main [--result "..."]
The envelope is deleted; a single summary line is retained in the ledger for audit. Only the root (terminal: true) should resolve.
cancel — abort an active stackCancel a pending stack to stop runaway processing or token waste.
python3 <miab-broker>/scripts/bin/claw-callback.py cancel --id cb-XXXX --from main --reason "Runaway token usage"
Status becomes cancelled and the envelope is atomically moved to $CLAW_HOME/state/callbacks/archive/<id>.json for retrospective analysis. Any stuck sub-agent that later tries show or return on that id fails fast, because the file is no longer in the hot directory.
show — inspect one bottleReload the full context of a bottle: task, holder, the active resume frame, remaining stack, and results so far. This is what a woken agent runs first.
python3 <miab-broker>/scripts/bin/claw-callback.py show --id cb-XXXX # human-readable
python3 <miab-broker>/scripts/bin/claw-callback.py show --id cb-XXXX --json # full envelope
list — see all in-flight bottlespython3 <miab-broker>/scripts/bin/claw-callback.py list # status table
python3 <miab-broker>/scripts/bin/claw-callback.py list --json # programmatic
Reports a quarantined count if any unreadable envelopes were moved aside during the scan (see §4).
All broker state lives under $CLAW_HOME/state/callbacks/ — CLAW_HOME defaults to ~/.openclaw.
| path | written by | purpose |
|---|---|---|
ledger.jsonl | every mutating command | append-only event log (the audit spine) |
cb-<id>.json | create/forward/return | one live envelope per in-flight bottle |
agent-registry.json | register | logical agent → routable agentId wake map |
archive/<id>.json | cancel | cancelled bottles, kept for post-mortem |
archive/corrupt/<id>.json | list/sweep | quarantined unreadable envelopes |
$CLAW_HOME/logs/callback-reaper.log | reap-callbacks.sh | reaper run log |
Envelopes are deleted on completion (resolve/reaped) — only the one-line ledger summary persists.
{
"id": "cb-20260801214133-9b06a0", // cb- + 14-digit UTC stamp + 6 hex
"version": "1.3.0",
"status": "pending", // pending | resolved | cancelled | failed
"task": "…", // overall delegated work
"createdBy": "main", // origin agent (the only valid resolver)
"holder": "planner", // who currently owns the work
"createdAt": "2026-08-01T21:41:33Z",
"updatedAt": "2026-08-01T22:14:12Z",
"stack": [ { "agent": "main", "resume": { … }, "pushedAt": "…" } ], // LIFO, bottom-first
"active": { "agent": "…", "resume": { … } }, // frame popped by the last `return`
"results": [ { "from": "coder", "result": "…", "artifacts": [], "at": "…" } ],
"history": [ { "at": "…", "agent": "…", "action": "create", "detail": "…" } ]
}
A resume object accepts exactly four keys — summary (string), steps (list of strings), expects (string), integrate (string). Anything else is rejected.
Ledger records are one JSON object per line: {at, id, event, by, …} where event is one of create, forward, return, resolve, cancel, fail, corrupt. The sibling interagent-queue skill parses this file — treat the field names as a compatibility contract.
Trust boundary. The broker assumes every process that can read $CLAW_HOME is trusted. It is designed for a single-user host running one agent ensemble. It is not hardened for a shared or multi-tenant machine.
What is enforced:
^cb-\d{14}-[0-9a-f]{6}$ and every resolved path is asserted to stay inside the callback root. Ids arrive from agent-generated callback:// text, so this is the boundary against a malformed or hostile id reaching the filesystem.CLAW_HOME is validated on startup: it must be owned by the current user and must not be group- or world-accessible. A poisoned CLAW_HOME would otherwise redirect agent-registry.json, and with it every wake event.umask 0077; state directories are 0700 and state files 0600.--resume-file must live under $CLAW_HOME unless --allow-outside is passed, is capped at 64 KB, and both --resume-file and --resume-json are schema-validated.{"ok": false, "error": …} on stderr and exits non-zero. Unreadable envelopes are quarantined to archive/corrupt/ with a corrupt ledger event rather than silently skipped.What is not protected — know these before trusting the broker with anything sensitive:
--from is an unverified claim. Any caller can assert any agent identity. Holder and root ownership are not yet enforced, so a misbehaving agent can pop another's frame or resolve a chain it doesn't own.wake target or rewrite resume.steps — which become instructions read by the woken agent.ledger.jsonl, which is never pruned. Do not put secret values in --task, --summary, or --result — they are written to disk and travel to other agents in dispatch_message text.See SECURITY.md for the threat model and reporting contact.
scripts/reap-callbacks.sh)Orphaned bottles (a holder crashed, a wake never fired) would otherwise linger as pending forever. The reaper wraps the CLI's deterministic, LLM-free sweep subcommand: it marks pending envelopes older than a configurable age as failed, appends a fail ledger event for each, purges the dead envelope, and clears dangling *.json.tmp write-handles.
scripts/reap-callbacks.sh # default: bottles older than 120m (CALLBACK_TTL_MIN)
scripts/reap-callbacks.sh --max-age 6h # custom threshold (s/m/h/d suffixes)
scripts/reap-callbacks.sh --dry-run # report only, change nothing
Do not schedule the reaper on the default threshold without measuring your workload first. Delegation latency is typically bimodal — fast machine turnarounds alongside long human- or cron-gated waits. A single global TTL that suits the first will destroy live work in the second. Run
--dry-runon a cron for a week and review$CLAW_HOME/logs/callback-reaper.logbefore enabling--fail.
| symptom | cause | fix |
|---|---|---|
invalid callback id: '…' | The id isn't the cb-<14 digits>-<6 hex> form. Usually a truncated or hand-typed id | Use the exact id from create/list; don't abbreviate |
CLAW_HOME root … is owned by uid … | CLAW_HOME points at a tree you don't own | Correct the env var, or chown the directory |
… has mode 0o755 (must not be group- or world-accessible) | State root is too permissive | chmod 700 $CLAW_HOME |
--resume-file … is outside CLAW_HOME | Reading a resume object from an unconstrained path | Move it under $CLAW_HOME, or pass --allow-outside deliberately |
resume context has unknown keys: [...] | Resume object has keys beyond the permitted four | Use only summary, steps, expects, integrate |
wake exits non-zero, registry_miss: true | Target agent isn't registered under that exact name | register it, then retry — check for persona-vs-function name mismatch |
callback … is resolved, cannot forward | Bottle already terminal | Start a new bottle; terminal states are final |
list reports quarantined envelopes | An envelope was unreadable and moved to archive/corrupt/ | Inspect it there; the corrupt ledger event records why |
Bottle sits pending far longer than expected | Wake never delivered, or holder never returned | show --id to see the holder, then re-wake; there is no automatic redelivery yet |
register → enable an agent's wake path (once per agent)
create → push first resume frame, dispatch, END TURN (caller)
wake → emit the exact cron dispatch call for a bottle (after create/forward)
forward → stack frame on top, delegate onward, END TURN (mid-chain holder)
return → pop frame, wake next holder up the stack (finished holder)
resolve → tear down bottle at the origin (terminal root)
cancel → abort a pending stack, archive it for post-mortem (any time)
show → reload one bottle's full context (woken agent, first call)
list → table of all in-flight bottles (operator)
reap → fail + clean stale/orphaned bottles (garbage collector)