Team communication in TinyClaw uses an actor model: each agent has its own mailbox (per-agent promise chain), communicates only by sending messages (queue entries), and processes one message at a time. There is no central orchestrator.
User: "@dev host an all hands meeting"
│
▼
┌──────────────┐
│ @manager │ (team leader)
│ responds │
└──┬───┬───┬───┘
│ │ │
│ │ └─── [@tester: share your testing update]
│ └─────── [@reviewer: share your review status]
└─────────── [@coder: share what you're working on]
│
3 messages enqueued in the queue
each processed by its own agent
- User sends a message to a team (or an agent in a team)
- The leader agent is invoked and responds
- Any
[@teammate: message]tags in the response become new messages in the queue - The queue processor picks them up instantly via in-process events (or on the next poll cycle for cross-process messages)
- Each agent processes its message via its own per-agent promise chain (parallel across agents)
- If an agent's response mentions more teammates, those become new messages too
- When no more messages are pending (
conv.pending === 0), all responses are aggregated and sent to the user
Text outside [@agent: ...] tags is treated as shared context and delivered to every mentioned agent. Agent-specific instructions go inside the tags.
We're doing a standup. Sprint ends Friday, 3 open bugs.
Reply with: (1) status (2) blockers (3) next step.
[@coder: Also list any PRs you have open.]
[@reviewer: Also flag any PRs waiting on you.]
[@tester: Also report test coverage for the auth module.]
Each agent receives the full shared context + their directed message.
One agent mentions one teammate. The chain continues linearly.
@manager → [@coder: fix the auth bug]
│
▼
@coder → [@reviewer: please review my fix]
│
▼
@reviewer → (no mentions, done)
Pending count: 1 → 1 → 1 → 0 (complete)
One agent mentions multiple teammates. All are invoked in parallel.
@manager → [@coder: ...] [@reviewer: ...] [@tester: ...]
│
├── @coder (processes independently)
├── @reviewer (processes independently)
└── @tester (processes independently)
Pending count: 1 → 3 → 2 → 1 → 0 (complete)
Agents can message back to whoever mentioned them. This is natural — the [@manager: ...] tag becomes a new message for manager.
@manager → [@coder: what's your status?]
│
▼
@coder → [@manager: systems operational, no blockers]
│
▼
@manager → (processes coder's response)
After a fan-out, agents can message each other directly.
@manager → [@coder: ...] [@reviewer: ...] [@tester: ...]
│
├── @reviewer → [@coder: check the fail-open behavior]
├── @tester → [@coder: here are the test results]
└── @coder → (no mentions)
│
▼
@coder gets two separate messages (processed sequentially):
1. From @reviewer
2. From @tester
When an agent is invoked as part of a conversation and other teammates are still processing, the system appends a note:
[2 other teammate response(s) are still being processed and will be
delivered when ready. Do not re-mention teammates who haven't responded yet.]
This prevents the "re-ask spiral" where an agent keeps mentioning teammates who already have pending messages. The note is informational — agents are trusted to respect it rather than enforced in code.
Without the indicator, this happens in a standup:
Manager fans out to coder, reviewer, tester
Reviewer responds first → [@manager: here's my update]
Manager is invoked with ONLY reviewer's message
Manager says "Still waiting on @coder and @tester" ← PROBLEM
→ Enqueues ANOTHER message for coder (who already responded!)
→ Enqueues ANOTHER message for tester (who already responded!)
Coder responds: "I already told you!"
Cycle repeats...
With the indicator, manager sees [2 other teammate response(s) are still being processed...] and knows to wait.
Each team interaction creates a Conversation object in memory:
| Field | Purpose |
|---|---|
pending |
In-flight message count. Incremented when a mention is enqueued, decremented when an agent finishes processing. |
responses[] |
All agent responses collected in order of completion. |
files |
Accumulated [send_file:] paths from all agents. |
totalMessages |
Counter for loop protection (max 15). |
outgoingMentions |
How many mentions each agent sent (for future batch-read support). |
A conversation completes when pending === 0 — all branches have resolved and no more messages are in flight.
On completion:
- All responses are aggregated (single response: as-is, multiple: joined with
@agent: responseformat) - Chat history is saved to
~/.tinyclaw/chats/{team_id}/{timestamp}.md - The aggregated response is enqueued in the responses table for the user's channel
- The conversation is cleaned up from memory
totalMessages is capped at 15. When reached, no further mentions are enqueued and active branches resolve naturally.
The previous implementation used a centralized while(true) loop that orchestrated all agent interactions synchronously.
| Chain (previous) | Actor model (current) | |
|---|---|---|
| Control flow | Central while loop |
Decentralized, queue-driven |
| Parallelism | Explicit Promise.all |
Natural per-agent promise chains |
| Backflow | Not supported | Natural — agent mentions sender |
| Fan-out | Scatter phase + gather phase | N mentions = N enqueued messages |
| Completion | Loop exits | pending === 0 |
| New patterns | New code for each topology | Same logic handles any topology |