feat(agent): per-session agent inbox (discussion #558)

A coordinator can leave text for a worker without typing into its pane.

- Store (src/web/agent-inbox.ts): read until ack by id, seen only for the
  receiver, 16 KiB/message, 512 KiB + 200 messages per inbox, full = 409
  CONFLICT (never drops the oldest), 12 h TTL from each message's createdAt.
- Routes (routes/inbox-routes.ts): POST/GET /api/sessions/:id/inbox
  (?wait= parks while nothing is unseen), POST .../inbox/ack (receiver
  only, 403 otherwise), GET /api/agent-inbox/summary (owner-scoped counts,
  not in toState()). Caller = resolveParentSessionId().
- Inbox waiters are the wait registry's third kind: same caps, ended by
  cancelAll/cancelEverything.
- Opt-in snapshot (agentInboxPersistEnabled, synced, default off):
  agent-inbox.json via dataPath(), 0600 tmp+rename, off deletes it, boot
  prune only after a successful restore. Dropped on delete, kept on detach.
- CLI: codeman agent post | inbox [--wait] | ack, INBOX column in ls.
- waitCapacityResponse/abortOnClientHangUp moved to route-helpers.ts
  unchanged.
- Docs: api-reference, README, CLAUDE.md, architecture-invariants, skill +
  plugin mirror.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
RandalixandClaude Opus 5.5 committed 2026-10-23 12:45:48 +02:00
1 parent 002de32a08
commit c6dfaa2cac
34 files changed
+2308 -83

No files matched your search

+7 -4
View File
@@ -35,7 +35,8 @@ This skill works the same whichever CLI you are: Codeman puts it where each CLI
for skills (`.claude/skills/` for Claude Code, `.agents/skills/` for Codex, Gemini CLI,
OpenCode, Antigravity, Grok, DeepSeek and OMP). A **pi** or **shell** worker never gets
it, but has the same environment: tell it *"other sessions: `codeman agent --help`"*.
That bundled CLI (`ls`, `spawn`, `send`, `wait`, `read`, `interrupt`, `rm`) is the same
That bundled CLI (`ls`, `spawn`, `send`, `wait`, `read`, `interrupt`, `rm`, and the inbox
verbs `post`, `inbox`, `ack`) is the same
verbs over the same endpoints, with the guards below (no control bytes, no self-delete,
no guessed URL) enforced in code, and it is also your own way through when this
machine has no `jq` for the preamble below.
@@ -119,9 +120,9 @@ fi
AUTH=(); [ -n "${CODEMAN_PASSWORD:-}" ] && AUTH=(-u "${CODEMAN_USERNAME:-admin}:$CODEMAN_PASSWORD")
# -k: harmless on http, required on https (self-signed cert).
# X-Codeman-Parent-Session: tags workers YOU spawn as your children, so the web UI can
# draw the lineage. Set once here and every present and future create call carries it;
# it is ignored on every other endpoint. Purely cosmetic (see §5.1) and it can never
# fail a spawn, so there is no case where you would want to leave it off.
# draw the lineage. Set once here and every present and future create call carries it.
# The agent inbox reads it too, as "who is calling" (§5.15); every other endpoint ignores
# it. It can never fail a spawn, so there is no case where you would want to leave it off.
# X-Codeman-Agent-Origin: marks a case directory a spawn CREATES as agent scratch, so the
# user can find and delete it long after your workers are gone (§5.14). Same deal: set
# once, cosmetic, never fails a spawn, and it labels only directories Codeman creates.
@@ -602,6 +603,7 @@ One row per job. Acting on this table alone is correct; the §5 links are the de
| find yourself, list what exists | `GET /api/v1/sessions`, match your `$SELF` by **prefix** | [§5.11](reference/verbs.md#511-list-and-find-yourself) |
| read or record what the user wants | `GET/PUT .../intent`, and `POST .../readmymind` to predict | [§5.12](reference/verbs.md#512-read-my-mind) |
| talk to a claude worker directly | `ListAgents` / `SendMessage`, when the feature is on at both ends | [§5.13](reference/verbs.md#513-messaging-claude-workers) |
| leave a message a session reads when it is ready, any mode, multi-line | `codeman agent post <id> "…"` (`POST .../inbox`). Nothing is typed into its pane and no turn starts; the receiver reads with `codeman agent inbox --wait` and removes with `codeman agent ack <id>`. An idle receiver never looks on its own: `codeman agent ls` shows unread mail, then `send` it one line | [§5.15](reference/verbs.md#515-agent-inbox) |
| clean up | `delete_session "$SID"` per id you created. Case directories and git worktrees are **not** removed with it; `GET /api/v1/cases/agent-created` lists the scratch case dirs your spawns left behind, for you to report | [§5.14](reference/verbs.md#514-clean-up) |
## 3. Rules digest
@@ -705,6 +707,7 @@ these**; open the one row you actually hit.
| [5.12 Read My Mind](reference/verbs.md#512-read-my-mind) | read or record what the user wants for a case |
| [5.13 Messaging claude workers](reference/verbs.md#513-messaging-claude-workers) | `ListAgents` / `SendMessage` instead of the HTTP path |
| [5.14 Clean up](reference/verbs.md#514-clean-up) | what deleting a session does **not** remove, and how to list the case dirs you left |
| [5.15 Agent inbox](reference/verbs.md#515-agent-inbox) | messages that wait for the receiver instead of being typed: post, read until ack, who counts as the receiver |
## 6. Setup and auth
+3 -3
View File
@@ -18,9 +18,9 @@ fi
AUTH=(); [ -n "${CODEMAN_PASSWORD:-}" ] && AUTH=(-u "${CODEMAN_USERNAME:-admin}:$CODEMAN_PASSWORD")
# -k: harmless on http, required on https (self-signed cert).
# X-Codeman-Parent-Session: tags workers YOU spawn as your children, so the web UI can
# draw the lineage. Set once here and every present and future create call carries it;
# it is ignored on every other endpoint. Purely cosmetic (see §5.1) and it can never
# fail a spawn, so there is no case where you would want to leave it off.
# draw the lineage. Set once here and every present and future create call carries it.
# The agent inbox reads it too, as "who is calling" (§5.15); every other endpoint ignores
# it. It can never fail a spawn, so there is no case where you would want to leave it off.
# X-Codeman-Agent-Origin: marks a case directory a spawn CREATES as agent scratch, so the
# user can find and delete it long after your workers are gone (§5.14). Same deal: set
# once, cosmetic, never fails a spawn, and it labels only directories Codeman creates.
@@ -608,6 +608,22 @@ Approving a permission dialog on your own is exactly the laundering this skill f
can access, which includes the user's own working sessions. An approval belonging to one
of those is something you **report**, never something you answer.
### Agent inbox: hand a worker text without typing into its pane
Stored, not typed: the receiver reads it when it asks. Semantics, CLI form and the
receiver loop: `verbs.md` §5.15.
| Task | Call |
|------|------|
| post to a session's inbox | `POST /api/v1/sessions/:id/inbox` body `{"text":"…"}` → `.data.message.{id,from}`; 409 = inbox full, nothing stored |
| read (yours marks seen, anyone else's is a peek) | `GET /api/v1/sessions/:id/inbox` → `.data.{receiver,pending,unseen,messages[]}` |
| block until something unseen arrives | `GET /api/v1/sessions/:id/inbox?wait=60000` → `.data.wait.{posted,timedOut,ended}` |
| remove what you handled (receiver only, idempotent) | `POST /api/v1/sessions/:id/inbox/ack` body `{"ids":["…"]}`; 403 from anyone else |
| counts per session, never text | `GET /api/v1/agent-inbox/summary` → `.data.sessions[id].{pending,unseen,oldestUnseenAt,parked}` |
`from` and "who is the receiver" both come from the `X-Codeman-Parent-Session` header the
preamble's `CURL` sends, never from the body.
### The wait primitives
Three bounded long-polls. Shared semantics:
@@ -764,3 +764,46 @@ Confirm cleanup with `GET /api/v1/sessions`, never with `/api/v1/sessions/unifie
(that one folds in transcript history from the whole machine and will keep showing
your worker forever).
### 5.15 Agent inbox
Every session has an inbox. A message posted there is stored, not typed: the receiver's
pane is untouched, no turn starts, and the text may span several lines. The receiver
reads when it is ready. Use it for results a worker hands back, for a follow-up task a
busy worker should pick up after its current one, and for anything longer than one
composer line. Use `send` when you want a turn to start now.
```bash
codeman agent post "$WORKER" "$(cat task.md)" # ONE quoted argument; prints pending/unseen
codeman agent inbox --wait --timeout 300000 # your own inbox; blocks while nothing is new
codeman agent ack <id> [<id>…] # remove what you handled, by the full id inbox printed
codeman agent inbox "$WORKER" # peek at someone else's: marks nothing
codeman agent ls # INBOX column: pending (new), parked
```
The same over HTTP: `POST /api/v1/sessions/:id/inbox {"text":…}`, `GET
/api/v1/sessions/:id/inbox?wait=<ms>`, `POST /api/v1/sessions/:id/inbox/ack {"ids":[…]}`,
`GET /api/v1/agent-inbox/summary`. The preamble's `CURL` already sends
`X-Codeman-Parent-Session`, which is how the server knows who you are.
Rules that matter:
- **Reading removes nothing.** Every read returns every pending message until you ack
it, so a crash between reading and doing loses nothing. Ack once the work is done,
not when you have read it: a sender looking at `ls` sees unacked mail as not done.
- **Only the receiver's own read counts.** Your read of your own inbox marks messages
seen (`seen: true` on the next read); reading another session's inbox is a peek and
changes nothing for it. Only the receiver can ack (403 otherwise).
- **`--wait` waits for something new.** It returns at once when there is unseen mail
and otherwise blocks until a post arrives; seen but unacked messages come back with it
and do not end the wait. Timeout is exit 2, the session vanishing is exit 3.
- **The sender is resolved, not claimed.** `from` is the session the header resolves to,
or `null` (the user). In single-user mode that is a hint, since every local process
holds the same credentials.
- **Full means refused.** 200 messages or 512 KiB per inbox, 16 KiB per message. A full
inbox answers 409 and stores nothing; the oldest is never dropped. Messages expire 12
hours after they were posted.
- **Nobody is woken.** An idle receiver does not look at its inbox by itself, and the
server never types into its pane. When `ls` shows a worker idle with unread mail and
not parked, `send` it one line ("read your inbox: `codeman agent inbox`").
- **Lifetime.** An inbox goes with its session on delete. A server restart empties all
inboxes unless the user turned on *Keep agent inbox across restarts* in Settings.