Adds a message-passing layer to state-hub so Claude instances can coordinate across sessions without polling shared progress events. - Migration f3a4b5c6d7e8: agent_messages table with thread support - FastAPI router: POST/GET /messages/, thread view, mark-read, archive, reply - 4 MCP tools: send_message, get_messages, mark_message_read, reply_to_message - Observable dashboard: /inbox page with unread/read/archived sections + KPI - CLAUDE.md updates: global, custodian, marki-docx, activity-core, template - TOOLS.md: Agent Inbox tools section documented Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
6.1 KiB
id, type, title, domain, repo, status, state_hub_workstream_id, created, updated
| id | type | title | domain | repo | status | state_hub_workstream_id | created | updated |
|---|---|---|---|---|---|---|---|---|
| CUST-WP-0015 | workplan | Agent Inbox — Inter-Agent Coordination via State-Hub | custodian | the-custodian | done | 382c2e8a-28db-4db9-8c89-8bc2fea5159a | 2026-03-16 | 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 locallyworker-<repo-slug>— 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
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
id: CUST-WP-0015-T01
status: done
priority: high
state_hub_task_id: "47035731-0d9b-42c0-ae87-29275046b1d8"
- Alembic migration: create
agent_messagestable (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
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=<agent>&unread_only=false&limit=50— inboxGET /thread/{thread_id}— full thread ordered by created_atPATCH /{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
id: CUST-WP-0015-T03
status: done
priority: high
state_hub_task_id: "e847b88b-2549-4f02-94c8-e855f5ac6dde"
Add to mcp_server/server.py:
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
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
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): addget_messages("state-hub")to session startstate-hub/CLAUDE.md: same, inside session protocol Step 1marki-docx/CLAUDE.md: addget_messages("worker-marki-docx")to session startactivity-core/CLAUDE.md: addget_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 ✓