--- id: CUST-WP-0015 type: workplan title: Agent Inbox — Inter-Agent Coordination via State-Hub domain: custodian repo: the-custodian status: done state_hub_workstream_id: 382c2e8a-28db-4db9-8c89-8bc2fea5159a created: 2026-03-16 updated: 2026-03-16 --- # CUST-WP-0015 — Agent Inbox ## Problem Two Claude instances (state-hub Claude on WSL2, worker Claudes on coulombcore and other repos) share the state-hub as a blackboard but have no way to address messages directly to each other. Progress events are broadcast with no recipient; dispatch is pull-only. There is no lightweight protocol for a worker to say "I need a decision from you" or for the state-hub Claude to say "review this before proceeding". ## Goal Add an agent inbox to the state-hub: a simple message-passing layer that lets any Claude session send a structured message to a named agent, and any receiving Claude to poll its inbox and reply. No real-time push required — agents poll on their natural iteration cadence (ralph loop, session start). ## Design ``` Worker Claude State-hub State-hub Claude ───────────────────────────────────────────────────────────────────────────── send_message( to="state-hub", subject="MRKD-WP-0001 T03 done — ready for review", body="..." ) ──────────────────────────► agent_messages table get_messages("state-hub") ◄────────────────────── reply_to_message(id, "Looks good, proceed") ──────────────────────► get_messages("worker-marki-docx") ◄────────────────────────────── ``` ### Agent naming convention - `state-hub` — the custodian / state-hub Claude running locally - `worker-` — a Claude session working inside a specific repo (e.g. `worker-marki-docx`, `worker-coulombcore`) - `broadcast` — special recipient: all agents receive it ### Message lifecycle `unread` → `read` → (optional) `archived` Thread support: replies carry the original message's `id` as `thread_id`, grouping a conversation. ## Schema ```sql CREATE TABLE agent_messages ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), from_agent VARCHAR(100) NOT NULL, to_agent VARCHAR(100) NOT NULL, -- or 'broadcast' subject VARCHAR(500) NOT NULL, body TEXT NOT NULL, thread_id UUID REFERENCES agent_messages(id) ON DELETE SET NULL, read_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT now() ); CREATE INDEX ON agent_messages (to_agent, read_at); CREATE INDEX ON agent_messages (thread_id); ``` --- ## Task: Migration + model ```task id: CUST-WP-0015-T01 status: done priority: high state_hub_task_id: "47035731-0d9b-42c0-ae87-29275046b1d8" ``` - Alembic migration: create `agent_messages` table (schema above) - SQLAlchemy model `api/models/agent_message.py` - Pydantic schemas `api/schemas/agent_message.py`: `MessageCreate`, `MessageRead`, `MessageReply` - Register model in `api/models/__init__.py` --- ## Task: API router ```task id: CUST-WP-0015-T02 status: done priority: high state_hub_task_id: "17d61bc4-9793-48a4-929d-7c8ce0f03ba0" ``` `api/routers/messages.py`, prefix `/messages`: - `POST /` — send message → `MessageRead` (201) - `GET /?to_agent=&unread_only=false&limit=50` — inbox - `GET /thread/{thread_id}` — full thread ordered by created_at - `PATCH /{id}/read` — mark read (sets read_at=now) - `PATCH /{id}/archive` — soft-delete (read_at stays, add archived_at) Register in `api/main.py`. Acceptance: `curl -X POST /messages/ -d '{"from_agent":"worker-marki-docx","to_agent":"state-hub","subject":"test","body":"hello"}'` returns 201. --- ## Task: MCP tools ```task id: CUST-WP-0015-T03 status: done priority: high state_hub_task_id: "e847b88b-2549-4f02-94c8-e855f5ac6dde" ``` Add to `mcp_server/server.py`: ```python send_message(to_agent, subject, body, from_agent="state-hub", thread_id=None) get_messages(to_agent, unread_only=True, limit=20) mark_message_read(message_id) reply_to_message(message_id, body, from_agent="state-hub") ``` `reply_to_message` resolves the original message's thread root, posts a new message with `thread_id` set, and marks the original as read in one call. Update `mcp_server/TOOLS.md` with the new tools. --- ## Task: Dashboard inbox page ```task id: CUST-WP-0015-T04 status: done priority: medium state_hub_task_id: "5af2a794-923a-4aa7-a00a-a7f4862e7b2c" ``` `dashboard/src/inbox.md`: - Unread messages table: from, to, subject, age, thread indicator - Mark-read button per row (POST to API) - Thread view: clicking subject expands the reply chain inline - KPI: unread count per agent (small summary bar at top) Add to nav in `observablehq.config.js` after Repo Sync. --- ## Task: Session protocol update ```task id: CUST-WP-0015-T05 status: done priority: medium state_hub_task_id: "5a80fc23-fc35-4dd5-833f-9280dddf6819" ``` Update CLAUDE.md files so agents know to check their inbox: - `~/.claude/CLAUDE.md` (global): add `get_messages("state-hub")` to session start - `state-hub/CLAUDE.md`: same, inside session protocol Step 1 - `marki-docx/CLAUDE.md`: add `get_messages("worker-marki-docx")` to session start - `activity-core/CLAUDE.md`: add `get_messages("worker-coulombcore")` to session start - Template (`state-hub/scripts/project_claude_md.template`): add inbox check --- ## Milestones | # | Milestone | Tasks | |---|-----------|-------| | M1 | Messages in DB | T01 | | M2 | Full API live | T02 | | M3 | MCP tools available | T03 | | M4 | Dashboard inbox | T04 | | M5 | All agents check inbox on session start | T05 | ## Dependencies - State-hub v0.5 dynamic domains — live ✓ - Repo sync / dispatch (CUST-WP-0014) — live ✓