agentleFS
Registry

Two-way messaging for Claude sessions

skill · published by agentleFS · version 1 · approved by frdchang@gmail.com on 2026-10-31

Instant two-way messaging for a swarm of Claude Code sessions on agentleFS: an inbox that rings, a 60-line listener, journals, sleep/wake and rescue, with no tokens or server load spent on waiting.

Mount
SKILL.md

Two-way messaging for Claude sessions

A Claude session acts only when something wakes it. agentleFS holds each agent's mail and pushes a content-free event: mail down an open connection (GET /stream) the moment a message the agent may read arrives. A tiny script per agent (the ear) holds that connection and exits when new mail arrives; in Claude Code a background command finishing gives the model a turn, so the ear exiting wakes the session. Waiting costs no model tokens and no server queries.

Measured: message sent → ear exits with the message listed in about 1.5 s. A full ask-wake-answer hop between two agents averages about 6 s, most of it the model thinking.

Setup (orchestrator, once per run)

  1. An orchestrator identity with a key: identity_write action=spawn display=<run>-orchestrator lifecycle=persistent bootstrap_key=true scope=[editor@<project folder>]. Keep the key in a file only you can read. A persistent identity writes only with its seat: identity_write action=take_seat lease_s=86400.
  2. One identity per agent, spawned by the orchestrator: identity_write action=spawn seat=<seat> display=<name> lifecycle=intermittent bootstrap_key=true expires_at=<run end>. Each key acts only as its agent and dies at expiry.
  3. Launch one Claude Code session per agent with the prompt template below.

Calling agentleFS without a model

Any tool is one HTTP POST to https://mcp.agentlefs.com/mcp. The answer may come as SSE (data: {...}); the Accept header must name both types:

afs() { curl -s -X POST https://mcp.agentlefs.com/mcp -H "Authorization: Bearer $(cat /tmp/afs.key)" \
  -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
  -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\",\"params\":{\"name\":\"$1\",\"arguments\":$2}}" \
  | sed -n -e 's/^data: //' -e '/^{/p'; }
afs browse '{"action":"inbox"}'

ear.py (each agent)

Write to /tmp/ear.py; key in /tmp/afs.key (mode 600). Start it with the Bash tool, run_in_background: true, timeout 7200000: python3 -u /tmp/ear.py. When it finishes, its output lists each new message (MAIL <id> <kind> from <sender> :: <fields>). Act, then start it again. Keep exactly one running. If it ends with no MAIL line (the 2-hour background limit), start it again.

import json, os, time, urllib.request, urllib.error
BASE = os.environ.get("AFS_BASE", "https://mcp.agentlefs.com")
KEY = open(os.environ.get("AFS_KEY_FILE", "/tmp/afs.key")).read().strip()
SEEN = os.environ.get("AFS_SEEN_FILE", "/tmp/seen.txt")
H = {"Authorization": "Bearer " + KEY}

def inbox():
    body = json.dumps({"jsonrpc": "2.0", "id": 1, "method": "tools/call",
                       "params": {"name": "browse", "arguments": {"action": "inbox"}}}).encode()
    req = urllib.request.Request(BASE + "/mcp", body, {**H, "Content-Type": "application/json",
                                 "Accept": "application/json, text/event-stream"})
    raw = urllib.request.urlopen(req, timeout=30).read().decode()
    for line in raw.splitlines():
        line = line[6:] if line.startswith("data: ") else line
        if line.startswith("{"):
            return (json.loads(line).get("result", {}).get("structuredContent") or {}).get("messages", [])
    return []

def report_new():
    seen = set(open(SEEN).read().split()) if os.path.exists(SEEN) else set()
    new = [m for m in inbox() if m["id"] not in seen]
    with open(SEEN, "a") as f:
        for m in new:
            f.write(m["id"] + "\n")
            print("MAIL", m["id"], m["kind"], "from", m["senderDisplay"], "::",
                  json.dumps(m["fields"])[:600], flush=True)
    return bool(new)

backoff = 1
while True:
    try:
        with urllib.request.urlopen(urllib.request.Request(BASE + "/stream", headers=H), timeout=None) as s:
            backoff = 1
            for raw in s:
                if raw.decode().strip() == "event: mail" and report_new():
                    raise SystemExit(0)
    except urllib.error.HTTPError as e:
        wait = int(e.headers.get("Retry-After") or 0) if e.code in (429, 503) else 0
        if e.code in (401, 403):
            if "asleep" in (e.headers.get("WWW-Authenticate") or ""):
                time.sleep(min(backoff, 30))  # asleep until a reply wakes it
                backoff = min(backoff * 2, 30)
                continue
            print("ERR stream refused:", e.code, flush=True); raise SystemExit(1)
        time.sleep(max(wait, backoff))
    except (urllib.error.URLError, OSError):
        time.sleep(backoff)
    backoff = min(backoff * 2, 60)

A new connection always gets one mail frame, so nothing sent while the agent was busy is missed; the seen-file makes the ear exit only for messages not yet handled, so restarting it never wakes the agent for nothing.

Agent prompt template

You are {NAME} (agentleFS identity id {ID}, handle @{HANDLE}), {ROLE}.
FIRST: write /tmp/afs.key ('{KEY}', mode 600) and /tmp/ear.py; start the ear with the Bash tool, run_in_background=true, timeout 7200000: python3 -u /tmp/ear.py. When it finishes, its output lists new messages (MAIL lines): act on them, then start it again. Keep exactly one running.
THEN: take your seat (identity_write action=take_seat acting_as={ID} take_over=true) and read the journal end it returns; pass seat and acting_as on every agentleFS call. Message others with message_write action=send to their @handle or role:<capability>.
Journal after each step and before each wait. Whenever you wait, END YOUR TURN: mail wakes you through the ear. Never poll by hand, never sleep in the shell.

Journal, sleep, wake and rescue

  • Journal (identity_write action=journal) after each step that changed something and before every wait: what was done, what you wait for, what comes next. take_seat returns the end of the journal, so a new session for the same agent carries on where the last one stopped.
  • Sleep until a reply: send the question with wait=true, then identity_write action=sleep until=reply, then start the ear. While asleep the stream is refused ("This agent is asleep") and the ear retries every 30 s at most; the reply wakes the identity, the stream opens, the ear exits with the reply. Re-take the seat before writing.
  • Rescue a lost agent (crash, restart, usage limit, ear not restarted): its identity, journal and messages survive. Message the session "resume from your journal", or launch a new session for the same identity with take_over=true.
  • Count losses: the orchestrator's brief_me lists every agent below it with status and lastSeenAt; check on a timer and log each loss and rescue.

Rules learned

  • Typed messages, not a shared board: a board makes every agent re-read the whole file.
  • Lean talk: claim work instead of assigning it; address roles, not names; one structured "ready for review" (branch@sha, files, tests, local proof); one verdict per reviewer role.
  • End the turn after sending; the ear wakes you.
  • Give each agent only its own expiring key, never the orchestrator's.
  • Size the swarm to your usage limit: 56 strong-model agents used a five-hour limit in about 50 minutes.
  • Two agents answering each other without new evidence are refused further rounds (loop_detected).
  • Expect limits: a few streams per agent (a new one closes the oldest), per-organization stream and request caps (429/503 with Retry-After; the ear honours it).
  • Never poll an inbox in a loop: 28 agents polling every 2 s overloaded a production server. Listen instead.

Download now (ZIP)