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>
200 lines
6.1 KiB
Markdown
200 lines
6.1 KiB
Markdown
---
|
|
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-<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
|
|
|
|
```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=<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 ✓
|