mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-02 13:39:41 +02:00
The feature as pitched in docs/readmymind-plan.md: pressing the header
brain button predicts the prompt you were about to type, from the case's
intent profile plus everything the session already knows.
Backend:
- readmymind-context.ts: pure budgeted context assembler (9 ranked
sources: pending approval dialog, user goals, last assistant turn tail,
recent prompts, tool activity, git workspace signals, away context,
sibling sessions, rethink state; 30 KB budget, whole-section drop from
the bottom of the ranking, trust tiers stated in the prompt)
- readmymind-collectors.ts: transcript tail reader (the live watcher
keeps only a 500-char snippet) and git signal collection (execFile,
2s timeout, skipped for remote-SSH cases)
- readmymind-predictor.ts: one-shot claude -p in a throwaway tmux
session, opus by default (readMyMindModel setting), strict JSON
contract with 1-3 suggestions (continue / verify / redirect), newline
stripping, 90s timeout; mutable singleton so route tests can stub it
- POST /api/sessions/:id/readmymind: claude-mode only (400), one
prediction in flight per session (409 CONFLICT), rethink body
{ steer, rejected }; ownership via findSessionOrFail
Frontend:
- readmymind-ui.js (loadorder 11.3): header brain button, marker-hidden
until readMyMindEnabled is ON, desktop only (phone key is phase 3);
modal with editable suggestion + rationale and Send / Insert /
Rethink / Dismiss; suggestion text rendered via value/textContent only
and nothing ever auto-sends
- App Settings -> Panels checkbox for readMyMindEnabled; en + zh-CN
strings
Verified end to end against a live isolated instance: transcript
capture, a real opus prediction grounded in the stated goals, rethink
steering, the 409, and the browser modal incl. Insert leaving the text
unsubmitted on the composer. 41 new unit/route tests; full test:ci
sweep green (4680 tests).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
292 lines
22 KiB
Markdown
292 lines
22 KiB
Markdown
# Codeman API reference for agents
|
||
|
||
Loaded on demand from the `codeman` skill. Assumes the guard variables from SKILL.md
|
||
(`$API`, `$SELF`, `"${CURL[@]}"`). Canonical contract: `docs/api-reference.md` in the
|
||
Codeman repo; this file is the agent-relevant subset, verified live.
|
||
|
||
## Envelope and errors
|
||
|
||
Every JSON response: `{"success":true,"data":…}` or
|
||
`{"success":false,"error":"…","errorCode":"…"}`. Branch on `errorCode`:
|
||
|
||
| `errorCode` | HTTP | Meaning |
|
||
|-------------|------|---------|
|
||
| `INVALID_INPUT` | 400 | malformed request; the message names the bad field |
|
||
| `UNAUTHORIZED` | 401 | auth required or failed (send `-u user:password`). ⚠️ The 401 body is plain text, NOT this envelope — `jq` dies with a parse error, see the guard in SKILL.md |
|
||
| `FORBIDDEN` | 403 | authenticated but not permitted: an admin-only route in multi-user mode, a `workingDir`/case path outside your own workspace, or a shell session without the can-bypass-permissions grant. ⚠️ **Not** what an ownership miss on a session returns: a session you do not own answers 404 `NOT_FOUND`, identically to one that does not exist (deliberate, it leaks no existence) |
|
||
| `NOT_FOUND` | 404 | no such session, or one this caller does not own |
|
||
| `SESSION_BUSY` | 409 | on a **wait**: this session's waiter cap (16, combined signal+output) is full. On **quick-start**: the 50-session cap is full, so clean up before starting more |
|
||
| `CONFLICT` / `ALREADY_EXISTS` | 409 | conflicts with current state |
|
||
| `OPERATION_FAILED` | 422 | well-formed but could not be completed |
|
||
| `RATE_LIMITED` | 429 | per-owner or process-wide waiter pool is full — back off; switching sessions will not help |
|
||
| `INTERNAL_ERROR` | 500 | server bug |
|
||
|
||
`SESSION_BUSY` vs `RATE_LIMITED` on the wait endpoints is deliberate: the first means
|
||
"too many waiters on *this* session", the second means the *pool* is full.
|
||
|
||
⚠️ **The guards that run before any handler answer in PLAIN TEXT, not this envelope**,
|
||
so `jq` reports a parse error and `.errorCode` is simply absent. All of them:
|
||
`401 Unauthorized` (Basic auth, carries `WWW-Authenticate`), `401 Unauthorized: hook
|
||
secret required`, `403 Forbidden: host not allowed` (Host allowlist), `403 Forbidden:
|
||
cross-site request blocked` (Origin/CSRF guard), and the auth rate limiter's
|
||
`429 Too Many Requests` (with `Retry-After`; distinct from the JSON `RATE_LIMITED`
|
||
above, which is the waiter pool). When a call returns something `jq` cannot parse,
|
||
read the status with `-w '%{http_code}'` and the raw body before assuming a bug.
|
||
|
||
## Sessions
|
||
|
||
| Task | Call |
|
||
|------|------|
|
||
| list sessions (metadata only, ~1.5 KB each, safe to poll) | `GET /api/v1/sessions` |
|
||
| one session (has `.data.pid`, `null` until the PTY spawns) | `GET /api/v1/sessions/:id` — ⚠️ **neither a liveness nor a busy check**, see below |
|
||
| unified list incl. history | `GET /api/v1/sessions/unified` → `.data.sessions[]` (NOT `.data[]`), and it folds in transcript history from the whole machine — never use it to verify cleanup; `GET /api/v1/sessions` is the cleanup check |
|
||
| start case + session in one call | `POST /api/v1/quick-start` |
|
||
| send input | `POST /api/v1/sessions/:id/input` |
|
||
| **read a worker's answer** (claude/codex) | `GET /api/v1/sessions/:id/last-response` → `.data.{text,timestamp}` — clean transcript text, no TUI noise. ⚠️ **Poll it**: the transcript flush lags the `stop` signal, so a read taken the instant send-and-wait returns is `""` (verified live). Also `""` before the first completed turn, and always `""` for `shell`/`opencode`/`gemini`/`antigravity` (no transcript) |
|
||
| read terminal (tail is in **BYTES**, raw ANSI) | `GET /api/v1/sessions/:id/terminal?tail=3000` → `.data.terminalBuffer` — for *diagnosis* (unsubmitted prompt?), not for reading answers |
|
||
| full tmux scrollback (context bomb; post-mortems only) | `GET /api/v1/sessions/:id/terminal?full=1` |
|
||
| background agents, one session | `GET /api/v1/sessions/:id/subagents` |
|
||
| background agents, global list | `GET /api/v1/subagents` (admin-only in multi-user mode) |
|
||
| the case's intent profile (Read My Mind: user goals + recent real prompts) | `GET /api/v1/sessions/:id/intent` → `.data.intent.{goals,recentPrompts}` (empty with `updatedAt: 0` until something is recorded) |
|
||
| replace the user-goals text on the case's intent profile | `PUT /api/v1/sessions/:id/intent` body `{"goals":"…"}` (≤ 8192 chars, strict schema; REPLACES the text, read + merge first) |
|
||
| forget the case's intent profile (only when the user asks) | `DELETE /api/v1/sessions/:id/intent` → `.data.deleted` |
|
||
| predict the user's next prompt (Read My Mind; claude-mode only, 5-90 s, costs real tokens) | `POST /api/v1/sessions/:id/readmymind` body `{}` (rethink: `{"steer":"…","rejected":["…"]}`) → `.data.suggestions[].{prompt,why,kind}` — suggestions are PROPOSALS; never send one to a session unless the user asked. 409 = one already running; 400 = non-claude mode |
|
||
| server status / version | `GET /api/v1/status` → `.data.version` |
|
||
| delete one session (yours only, via `delete_session`) | `DELETE /api/v1/sessions/:id` — never call it bare; the fail-closed helper in SKILL.md §0 is the only self-protection that exists. Answers `{"success":true,"data":{}}`: an **empty** body is the success signal, there is nothing to read back |
|
||
|
||
`DELETE /api/v1/sessions/:id` takes one undocumented query parameter, `killMux`, and
|
||
it defaults to `true` (anything other than the exact string `false` means kill). With
|
||
`?killMux=false` the call **detaches instead of killing**: the tmux session and the
|
||
agent inside it keep running, the session drops out of `GET /api/v1/sessions` so it
|
||
looks deleted, and it is deliberately left in persisted state for recovery (the
|
||
lifecycle log records `detached`, not `deleted`). That is the wrong tool for agent
|
||
cleanup: your worker keeps burning tokens where neither you nor the user can see it,
|
||
and the list you would check to confirm cleanup shows it gone. Delete plainly, and let
|
||
`killMux` default.
|
||
|
||
⚠️ **`.data.status` is a heuristic and is often simply wrong. Never branch on it.**
|
||
Measured on a live claude worker: `status` read `idle` while the worker was mid-turn
|
||
and actively producing output, with `lastActivityAt` equal to the moment of the call.
|
||
It is wrong in both directions, so neither value tells you anything you can act on:
|
||
|
||
- **`idle` does not mean finished.** Use `stop` (the definitive end-of-turn hook) via
|
||
send-and-wait, or an output marker. If you must judge from outside, sample
|
||
`terminal?tail=` twice a few seconds apart and compare: a changing buffer is the
|
||
only cheap positive proof that a worker is still working.
|
||
- **`idle` does not mean alive.** A worker that dies inside its pane keeps
|
||
`status:"idle"` and a pid (that pid is the local tmux attach client, not the
|
||
worker). `wait?until=exit` is the death check.
|
||
|
||
Treat `status` as a UI hint. Every synchronization decision in these recipes is built
|
||
on signals and markers for exactly this reason.
|
||
|
||
⚠️ `GET /api/v1/sessions/:id/output` → `.data.textOutput` looks like the obvious read
|
||
but stays **empty for interactive tmux-backed sessions** (it is fed only by the legacy
|
||
JSON-stream path). Verified empty on live claude and shell sessions. Use
|
||
`last-response` for claude/codex answers; only fall back to `terminal?tail=` for
|
||
hook-less modes, or to diagnose a prompt that was never submitted, and strip ANSI:
|
||
|
||
```bash
|
||
# `\x1b` is a GNU-sed extension. BSD sed (macOS, the default there) reads it as a
|
||
# literal "x1b", matches nothing, and hands back raw ANSI, silently. Feed sed a real
|
||
# ESC byte instead; that form works on GNU and BSD alike.
|
||
ESC=$(printf '\033')
|
||
… | jq -r '.data.terminalBuffer' | sed -e "s/${ESC}\[[0-9;?]*[a-zA-Z]//g" -e "s/${ESC}([B0]//g"
|
||
```
|
||
|
||
`POST /api/v1/quick-start` body (all optional):
|
||
`{"caseName":"worker-1","mode":"claude","sessionName":"w9-worker","effort":"high"}`
|
||
— `mode` ∈ `claude|shell|opencode|codex|gemini|antigravity`; response is
|
||
`.data.{sessionId, caseName, casePath}`. Creates the case directory (a real directory
|
||
on the user's disk) if missing — do not retry it in a loop, and remember the name.
|
||
|
||
⚠️ **Branch on `.success` before reading `.data.sessionId`.** On any failure the field
|
||
is absent, `jq -r` prints the literal string `null`, and every later call then targets
|
||
`/api/v1/sessions/null`, burning the full readiness budget and reporting jq noise
|
||
instead of the real cause. Failure modes here are `SESSION_BUSY` (the **50-session
|
||
cap**, not the waiter cap), `FORBIDDEN`, `CONFLICT`, `OPERATION_FAILED` and
|
||
`INVALID_INPUT`; none of them are retryable in a loop.
|
||
|
||
⚠️ `caseName` resolves through the linked-cases registry first, so a name that happens
|
||
to match a case the user linked in lands in that **real repo**, not a fresh scratch
|
||
directory. Pick distinctive scratch names, and use a linked name deliberately when you
|
||
do want a worker in an existing checkout.
|
||
|
||
`POST /api/v1/sessions/:id/input` body:
|
||
`{"input":"one line\r","useMux":true,"clientId":"agent-1","seq":1}` plus optionally
|
||
`"wait"` / `"waitTimeout"` (below).
|
||
|
||
- ⚠️ **The input must contain `\r`** (the JSON escape, i.e. a real carriage return)
|
||
**or Enter is never sent**: the text is typed onto the worker's prompt and sits
|
||
there unsubmitted. Verified live — this is the number-one silent failure, and no
|
||
response field catches it: `delivered:true` means "written to the pane", not
|
||
"submitted". A `\r`-less send with `wait` reports `delivered:true` and then every
|
||
wait on that turn times out. Without `wait`, fire-and-forget returns an **empty**
|
||
`{"success":true,"data":{}}` — no `delivered`, no `duplicate`; those fields exist
|
||
only on the `wait` variant, so a fire-and-forget flow gets no delivery
|
||
confirmation at all.
|
||
- `input` must be single-line (newlines are stripped). To send a bare Enter (confirm
|
||
a dialog), send `{"input":"\r"}`.
|
||
- `input` is capped at **100 000 characters**; one character over is a 400
|
||
`INVALID_INPUT` and **nothing is typed** (the schema rejects the whole body, so it
|
||
is not a truncation). Since the value is one line anyway, a prompt that big means
|
||
you are pasting a file into the composer: write it to disk in the worker's case
|
||
directory and send a path instead. `clientId` is capped at 128 characters on the
|
||
same terms.
|
||
- `clientId`+`seq` give exactly-once delivery: the server applies each pair at most
|
||
once. Increment `seq` per new input.
|
||
|
||
## The wait primitives
|
||
|
||
Three bounded long-polls. Shared semantics:
|
||
|
||
- **Timeout = HTTP 200** with `wait.timedOut:true`. Loop over short waits (60 s);
|
||
`tailscale serve` / cloudflared cut idle connections.
|
||
- Timeouts are **clamped** to `[1000, 600000]` ms (operator-tunable); the applied
|
||
value is echoed as `wait.timeoutMs` — read it back, never assume.
|
||
- ⚠️ Clamping only covers **positive integers**. `timeout=0`, a negative value, a
|
||
fraction (`timeout=1500.5`) and anything non-numeric (`timeout=30s`) are rejected by
|
||
the schema as a 400 `INVALID_INPUT` naming the field, not silently clamped up to
|
||
the floor. Omit the parameter to take the 60 000 ms default; never send a computed
|
||
remainder without rounding it and checking it is still above zero. Same rule for
|
||
`waitTimeout` in the input body, where the value must additionally be a JSON number
|
||
(a quoted `"60000"` is a 400).
|
||
- All three nest the result under `.data.wait`, same shape, so one helper parses all.
|
||
- `.data.status` (post-wait `SessionStatus`) and `.data.limitPaused` ride along.
|
||
`limitPaused:true` means the session is paused on a usage limit and will emit
|
||
nothing until reset — a timeout is then *expected*; do not retry hard, and do not
|
||
kill the worker.
|
||
|
||
### Signals by mode
|
||
|
||
| Signal | Meaning | Available for |
|
||
|--------|---------|---------------|
|
||
| `idle` | output stabilized + prompt detected — heuristic, can flap mid-turn | every mode |
|
||
| `working` | session started producing output | every mode |
|
||
| `stop` | Claude Code `stop` hook — the definitive end-of-turn | `claude` only |
|
||
| `blocked` | `permission_prompt` / `elicitation_dialog` hook — the worker needs an answer | `claude` only |
|
||
| `exit` | PTY exited or session deleted | every mode |
|
||
|
||
Default `until` set: `stop,idle,exit`. On non-claude modes the server silently drops
|
||
`stop`/`blocked` from the *default* set (echoed back as `wait.until`, e.g.
|
||
`["idle","exit"]` on shell); requesting them *explicitly* there is a 400 naming the
|
||
mode. ⚠️ On hook-less modes the lifecycle signals are also **coarse in practice**: a
|
||
short shell command produced **no** `idle` transition within 60 s (verified live), so
|
||
a `fresh=1` / fresh-delivery wait can burn its whole timeout while the work finished
|
||
long ago. Synchronize hook-less modes with `wait-output` markers instead.
|
||
|
||
Two more places hooks go missing even in claude mode: **Docker cases** need
|
||
`CODEMAN_DOCKER_BRIDGE_HOOKS=1` on the server (without it only `idle`/`working`/
|
||
`exit` arrive), and **remote-SSH cases** run the agent on another host whose hooks may
|
||
never reach this server. When unsure, ask for `stop,idle,exit`.
|
||
|
||
⚠️ **Signals are edge-triggered with no history.** A signal that fires while no
|
||
waiter is registered is gone; no later wait can observe it (`until=stop` on a worker
|
||
whose turn already ended just times out, with or without `fresh` — verified live).
|
||
Register the waiter before the event can happen: send-and-wait does exactly that,
|
||
and `wait-output` markers with `from=buffer` are latched by construction. Never
|
||
fire-and-forget N prompts and then gather signal-waits worker by worker; every
|
||
worker that finishes before its gather is unobservable (see recipes.md Flow 3b).
|
||
|
||
### `GET /api/v1/sessions/:id/wait`
|
||
|
||
| Param | Default | Notes |
|
||
|-------|---------|-------|
|
||
| `until` | `stop,idle,exit` | comma list; unknown token → 400 naming it |
|
||
| `timeout` | 60000 | ms, positive integer only (0/negative/fractional = 400); clamped, applied value echoed as `wait.timeoutMs` |
|
||
| `fresh` | `0` | `1` requires an actual *transition*, ignoring the state at call time |
|
||
|
||
⚠️ A session whose PTY has not spawned (`pid:null`) or has exited counts as `exit`
|
||
**right now**: with the default set the call answers immediately
|
||
(`signal:"exit", immediate:true`). That is how you detect a dead worker cheaply — but
|
||
it also means "wait for my just-created session" needs the readiness recipe in
|
||
SKILL.md, not this endpoint.
|
||
|
||
### `GET /api/v1/sessions/:id/wait-output`
|
||
|
||
| Param | Default | Notes |
|
||
|-------|---------|-------|
|
||
| `match` | required | literal substring, 1–200 chars, ANSI-stripped; chunk-straddling matches found; **no regex** — a `regex=` param is a 400 |
|
||
| `nocase` | `0` | case-insensitive compare; snippet keeps original casing |
|
||
| `from` | `now` | `buffer` scans the tail (~256 KB) of existing output first |
|
||
| `timeout` | 60000 | same clamp, same positive-integer rule |
|
||
|
||
Four traps, all observed live:
|
||
|
||
1. **The echo of your own typed command is output.** A marker appearing verbatim in
|
||
the input line matches the moment the text is typed, before the command runs.
|
||
Split the marker with a shell variable: send `M=DONE; …; echo ${M}_1234\r`, wait
|
||
on `DONE_1234`.
|
||
2. **`from=now` misses text printed before the wait landed** — a marker echoed just
|
||
before the request registered timed out at full length. After sending a command,
|
||
always wait with `from=buffer`.
|
||
3. **`from=now` can also match too much**: tmux repaints old screen content as
|
||
ordinary output on attach/resize/redraw, so a *generic* marker (`BUILD OK`)
|
||
matches stale text. Unique-per-call markers (`DONE_$RANDOM`) make both `from`
|
||
modes safe.
|
||
4. **TUI output can be space-less in the stream.** Full-screen TUIs (claude, codex,
|
||
…) position words with cursor-movement escapes rather than literal spaces, so
|
||
the stripped stream can read `Yes,Itrustthisfolder` while the pane shows the
|
||
spaced phrase. Whether a given phrase keeps its spaces depends on how the TUI
|
||
drew it (observed live: some multi-word matches fire, some never do), so treat
|
||
multi-word matches against TUI screens as unreliable and match a **single
|
||
space-free token** (`trust`, `shift+tab`). Plain command output (shell workers,
|
||
`echo` lines) keeps real spaces and multi-word matches work there.
|
||
|
||
Build the query with `-G --data-urlencode` (a `+` in a hand-built query decodes to a
|
||
space). Result extras: `wait.matched`, `wait.match`, `wait.snippet` (bounded window
|
||
around the match, blank runs collapsed — the snippet is often all you need to read).
|
||
|
||
### `POST /api/v1/sessions/:id/input` with `wait`
|
||
|
||
| Field | Notes |
|
||
|-------|-------|
|
||
| `wait` | `true` (default signal set) or the same comma grammar as `until`; absent = historical fire-and-forget |
|
||
| `waitTimeout` | ms, same clamp; a JSON number, positive integer (`"60000"` is a 400) |
|
||
|
||
Registers the waiter **before** typing, which closes the race where send-then-wait
|
||
sees the previous turn's idle state and returns instantly. Response adds `delivered`
|
||
and `duplicate` beside the standard `wait` object.
|
||
|
||
A **tagged duplicate** (same `clientId`+`seq` already applied) does not retype but
|
||
still honors `wait`, answering from the session's *current* state instead of
|
||
requiring a new transition (`delivered:false, duplicate:true` — verified: ~20 ms,
|
||
command ran exactly once). That is what makes the resend-identical-request loop in
|
||
SKILL.md correct: iteration 1 delivers and needs a transition; later iterations
|
||
resolve immediately if the turn ended in between. ⚠️ The flip side: a duplicate's
|
||
`immediate:true` answer is the current state and nothing more — an idle worker
|
||
whose prompt was never submitted (missing `\r`) produces the same
|
||
`signal:"idle", immediate:true` as one that finished the turn. Confirm from
|
||
`terminal?tail=` before reporting success; SKILL.md's loop shows where.
|
||
|
||
### Outcome parsing, in order
|
||
|
||
1. `wait.signal != null` (or `wait.matched == true`) — the thing happened.
|
||
`wait.immediate:true` rides along and means the condition already held at call
|
||
time; if that is not what you meant, you wanted `fresh=1` or send-and-wait.
|
||
2. `wait.timedOut` — poll boundary; loop again.
|
||
3. `wait.ended` — session deleted/torn down mid-wait; stop looping.
|
||
|
||
## Troubleshooting
|
||
|
||
| Symptom | Cause / fix |
|
||
|---------|-------------|
|
||
| every curl fails with a certificate error | you dropped `-k`; `CODEMAN_API_URL` is HTTPS with a self-signed cert |
|
||
| `jq: parse error` on every call | plain-text 401s: the server has a password. Check with `-w '%{http_code}'`, use the guard's `.env` fallback, and if no `.env` exists, stop and ask the user for credentials |
|
||
| input arrives but nothing happens; later waits all time out | the input had no `\r`, so Enter was never sent; the text is sitting on the worker's prompt. **Submitting it with `{"input":"\r"}` is the ONLY recovery** — Ctrl+U (0x15) and Esc do NOT clear the composer (verified live) — and the flush costs one turn in which the worker reasons about the junk; open the next real prompt with "ignore the garbled line above:" |
|
||
| `GET .../sessions/$CODEMAN_SESSION_ID` 404s | Docker case: the env id is truncated to 8 chars; find yourself with `startswith($SELF)`, and always self-compare by prefix, in both directions |
|
||
| `CODEMAN_MUX` unset but you seem to be in a session | remote-SSH case: the env vars are not exported there. Fail closed — refuse to act |
|
||
| connection refused from inside a container | a loopback-bound server is unreachable from a container, and `CODEMAN_DOCKER_BRIDGE_HOOKS=1` does **not** fix that: it opens a hooks-only listener, so hook events start flowing but `/api/v1/*` stays refused. Driving the API from inside a Docker case needs a reachable bind (an operator decision); report it, don't retry |
|
||
| wait routes 404 on a valid session id | read the `.error` text: a `Route ...` prefix means the server predates the wait endpoints (< 1.13.0; a dev build can serve them while reporting an older version, so probe, never version-compare) — poll `terminal?tail=` and say so. `Session ... not found` means your id is wrong, not the server |
|
||
| wait on `stop` never resolves | non-claude mode, or hooks not reaching the server (Docker/remote), or a case created by Codeman < 1.13.0 against an `--https` install (its hook curls lacked `-k` and TLS-failed silently; a 1.13.0+ server rewrites them the next time a session starts in that case). Use markers or `idle,exit` |
|
||
| new claude worker ignores its first prompt | it was showing the first-run trust dialog and Codeman's auto-accept missed; use the readiness recipe in SKILL.md (wait for `shift+tab` first, accept the dialog only as the bounded fallback) |
|
||
| readiness burns its whole budget, then the worker answers fine anyway | you matched `bypass`, which is the statusline of ONE permission mode. Codeman spawns `--dangerously-skip-permissions` by default, but the server's `claudeMode` setting also has `auto` (`auto mode on`), `allowedTools` and `normal` (both `don't ask on`), and the mode is not exposed on `GET /api/v1/sessions/:id`. Match **`shift+tab`** instead: every mode's status bar ends `(shift+tab to cycle)` (measured per mode against claude-cli 2.1.226). ⚠️ It must go through `--data-urlencode`, or the `+` decodes to a space and you silently search for `shift tab`. Expect `blocked` signals mid-turn on the non-default modes |
|
||
| ANSI escapes survive the strip pipeline | `sed -e 's/\x1b…'` on macOS: `\x1b` is GNU-only, BSD sed matches nothing and strips nothing. Use the `ESC=$(printf '\033')` form above |
|
||
| `wait-output` times out although the pane shows the text | multi-word match against a TUI screen; the stream has no spaces there — match one token |
|
||
| `wait-output` matched instantly with stale text | generic marker + tmux repaint; use `DONE_$RANDOM` |
|
||
| 409 `SESSION_BUSY` on a wait | too many concurrent waiters on that session (cap 16 combined); reuse one wait per worker |
|
||
| 429 `RATE_LIMITED` on a wait | global/owner waiter pool full; back off, do not switch sessions |
|
||
| ready claude worker missing from `ListAgents` | cross-session messaging is off for that end: CLI < 2.1.224, the feature flag not (yet) on (observed: two 2.1.226 sessions on one box, only one with an inbox socket), a telemetry-disabling env var, a Docker/remote case, or a non-claude mode. Not an error: drive it over the HTTP recipes. See `reference/messaging.md` |
|
||
| `SendMessage` says "not an agent in this conversation" | first contact with a peer needs the ref: re-send with the exact `name [ref]` string from the `ListAgents` row, or from that error's own suggestion |
|
||
| message sent, worker never acts, no reply, no `stop` | the message was held (permission-class mismatch: a non-default `claudeMode` spawns prompting-class workers, and the approval dialog expires unattended after ~5 min) or refused (`crossSessionInbound`). Run the bounded backstop, then deliver once over HTTP input. See `reference/messaging.md` |
|