The launch-id check is pure, so it now runs before the context warning and the
swap confirmation: nobody agrees to evict a model only to get the 400. Also the
search-box and in-use comments, the model-plain-colon wording, and the wiki's
order of the picker's marks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Classify OpenSSL and LibreSSL's SSL_connect errors as unreachable again (`_` is a
word character, so \bssl\b missed them and the user read the temp directory as the
reason), point a cases-folder refusal at Clone Repo instead of Create New, and move
the mechanism and history out of CLAUDE.md into architecture-invariants.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Round-four review smaller item: the discovery comment claimed a shared ceiling while schemas.ts restated the literal twice; the constant now lives in schemas.ts (routes already import from it, no cycle) and both bounds reference it.
Round-four review item 3: the swap route wrote the new config.toml into the very dir a live codex session uses as CODEX_HOME before checking the pattern, and its refusal then deleted that dir. buildCustomModelInjection is pure, so the check runs on its result and a refused id never touches the filesystem. Pinned by a two-apply codex route test.
- share one context-length resolution (discovered figure, endpoint default fallback) across the routes and boot recovery, with a recovery test
- record listed per-slot context figures with a slot count of 1 and force the slot-count fallback once per rediscovery
- open up the codex model launch token for qwen3-style ids (model-plain-colon), pinned by a spawn-golden case
- pass the full discovered window to opencode as limit.context
- translate the picker search strings and document the discovery precedence
A folder made in a cases directory is already a case (no scaffold), which Link
Existing then refuses by name and which deleting would take the linked folder
with. The route refuses a cases directory as the parent (the shared one and the
caller's own), a leading tilde in the name, and maps a vanished parent to 404.
Docs now say every name it accepts is one Link Existing accepts, and the wiki
describes the button.
git's refusal quotes the destination, so a folder named openssl or rustls
classified as HOST_UNREACHABLE and the cleanup emptied it. The cleanup now
tests git's own text, DESTINATION_EXISTS is matched first, and ssl/tls are
matched as words. Also: a test for the Docker refusal on the default branch,
the route JSDoc and constant comment, and the folder copy (empty, or new).
The name rule now matches safePathSchema (letters, digits, space and _ - . ~,
no .., no leading dot, at most 255 bytes) so every folder it makes can be
linked. A file as the parent is a 400, errno text is no longer returned,
showHidden is forwarded like browse, and Create is disabled while the request
runs. Adds multi-user scoping tests.
A direct clone whose git-slot wait timed out came back with no spawnError, so
the cleanup emptied a folder git never touched. GitRun now says whether git
started and the cleanup requires it. Cloning into a folder at or inside a
Docker case workspace is refused, as git status, fetch and worktrees do, since
git would run the new repository's hooks and filters on the host.
The picker could only choose an existing folder. POST /api/filesystem/mkdir
creates one folder inside a folder the picker may browse (same roots,
ownership and blocked-tree rules as browse), and PathPicker gains an opt-in
allowNewFolder that adds a New folder button and selects the result.
A direct clone into an empty folder emptied the folder after any failure, which
deleted a concurrent attempt's finished clone when git had written nothing.
Clones into one folder are now mutually exclusive in-process, the cleanup is
skipped when git refused the folder, never ran, or the mount is unresponsive,
and a name linked while the clone ran is a 409 that keeps the folder.
Documents path for POST /api/cases/clone.
The custom folder was treated as a parent, so the repository landed in <folder>/<case name>. The chosen folder is now the clone destination: an empty existing folder is cloned into directly (CloneOptions.intoEmptyDir, no temporary sibling, never removed on failure), a new folder is created, and a folder with files is refused.
Clone Repo could only clone under ~/codeman-cases. POST /api/cases/clone now takes an optional path (parent folder plus case name) and the Clone tab gets a Clone into a custom folder option with a Browse button, mirroring Create in a custom folder. The path is judged by prepareNewCasePath, the clone is registered as a linked case, the option is admin-only in multi-user mode, and an existing target folder is refused.
Export isPathWithinRoot from file-routes.ts and use it for the remote
@fix_plan.md confinement, so the two checks cannot drift; rewrap the
readRemoteFixPlan JSDoc and say NOT_FOUND; list fix-plan/read and the two
write refusals in docs/remote-sessions.md and architecture-invariants.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A remote-SSH session's workingDir is a path on the remote host, but the
Ralph file routes used local fs on it: fix-plan/read imported a same-named
LOCAL @fix_plan.md (or 404'd), and fix-plan/write and ralph-prompt/write
wrote their files into the local twin of the remote folder. Same root
cause as the Ralph @fix_plan.md watcher fix.
- fix-plan/read reads the file on the remote host through
src/remote-files.ts (the File Viewer's ssh path): one probe of the file
and the workspace, the File Viewer's confinement (a file resolving
outside the workspace is treated as missing), then a capped read (1 MB).
A missing remote file is the same NOT_FOUND as a missing local one; a
non-file, an oversized file or an unreachable host is a failed read,
never a local fallback.
- fix-plan/write and ralph-prompt/write refuse a remote session
(INVALID_INPUT): remote writes are out of scope by design, as for the
File Viewer's remote edit. The Ralph panel already shows the error.
A remote-SSH session's workingDir is a path on the remote host, and the
image watcher watched it on the local disk: a folder that is not there,
or a same-named local folder whose images then popped up as the remote
session's. Same root cause as the Ralph @fix_plan.md watcher fix.
Three places start a watch, and all three now skip a remote session:
setupSessionListeners(), the app-wide toggle in PUT /api/settings (which
re-watches every enabled session), and POST /api/sessions/:id/image-watcher,
which refuses to turn it on (INVALID_INPUT, like the Git fetch settings
route) and still lets it be turned off. The Session Options row is hidden
for a remote session, the way the Git tab is, since the client's
_apiPost() does not throw on a refused request.
The merged PRs (mostly #714's git fetch) added about 120 em-dashes in
comments, docs, test names and three settings strings. Each one is now a
comma, colon, semicolon or parentheses. The English settings strings in
index.html and their i18n.js keys change together, and the zh-CN value
for the fetch interval uses full-width parentheses instead of a double
dash. The one left is the regex in constants.js that matches a dash
typed as a word, which is behaviour, not punctuation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
readDockerCases() reads an unreadable or half-written docker-cases.json as [], so a fetch (which runs
repository hooks, and runs unattended on the interval) could run inside a Docker workspace. The fetch
paths now read it through a strict reader: the Fetch button answers a failed run (state 'error') and the
scheduler skips the tick, undoes its picks and logs once. The status and diff routes are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts resolved by keeping both sides:
- CLAUDE.md (Agent CLI): #694's opening (an HTTP client, the experimental
agent-session-routes behind agentCoordinationEnabled) with #708's clause
on the directory spawn and its two refusals.
- docs/api-reference.md (codeman agent): #708's spawn paragraph and --json
shape, with #694's inspect/stage/submit in the verb list and the guarded
endpoints named before "adds no route for the other verbs".
- src/cli-agent.ts (@fileoverview): #708's quick-start / directory spawn
routes and #694's experimental routes in one endpoint list; the two
node:fs/promises imports (#708's realpath+stat, #694's open+stat) merged
into one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflict in CLAUDE.md (Adding Features, App setting) resolved by keeping
both sides: #714's sentence that a setting the server acts on must be
synced, after the `.strict()` clause, and #719's mutateSettingsFile() rule
at the end of the bullet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflict in src/web/routes/system-routes.ts (PUT /api/settings) resolved by
keeping both sides: #718's line that never stores an unauthorized
tunnelEnabled:true runs first, then #719's serialized mutateSettingsFile()
write replaces the inline read and plain writeFile.
test/routes/system-routes-tunnel-guard.test.ts: #718's new tests asserted
plain writeFile calls on settings.json. With #719 every write goes to a temp
file renamed over it, so the assertions read writes through a settingsWrites()
helper (writeFile contents keyed by their rename), and the concurrent-stop
harness applies a write and the stop at the rename.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- src/web/routes/session-routes.ts:2102: the guarded-submit 409 now applies to tagged input (clientId + seq) only; untagged input is held like a WebSocket frame. The raw write (:2228) goes through holdForGuardedLease and answers 200 once held (a send-and-wait reads `delivered` from the held write; a full queue releases the waiter and answers 409); a mux write takes its own turn in writeViaMux.
- test/guarded-lease-held-input.test.ts: real-Session route tests for an untagged raw POST during the lease (written after the Enter) and a held send-and-wait (delivered: true); both fail without the fix.
- test/routes/session-routes.test.ts + test/mocks/mock-session.ts: the 409 test now covers tagged input only; new tests for untagged raw (held), untagged mux (own turn) and a full hold queue (409); MockSession gains holdForGuardedLease/releaseGuardedLease and a lease-aware writeViaMux.
- src/session.ts:4787 (guardedInputHeld JSDoc) + write() + docs/architecture-invariants.md: write() refuses during the lease only, so cron's paste mode can land ahead of input still draining.
- docs/api-reference.md: the input route's 409 is for tagged input; untagged input is held.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- src/web/settings-writer.ts:110: export assertSettingsWritable(), the writer's own read-for-write rule run on its own
- src/web/routes/case-routes.ts:1860: an admin's rename checks settings.json before it ends any session or moves the folder, and answers 409 CONFLICT with the writer's message (placed before the session-starting check, so nothing is awaited between it and the lock)
- test/routes/case-rename-routes.test.ts:314: the torn-file pin now expects 409 CONFLICT (no stoppedSessions, like the other pre-checks)
- test/routes/case-rename-routes.test.ts:331: new test, a live session with stopSessions and a torn file: 409 CONFLICT, cleanupSession not called
- docs/api-reference.md: the Settings paragraph says an admin's rename answers this refusal as 409 CONFLICT before ending sessions
- src/hooks-config.ts:1161, src/prompt-cache-keepwarm.ts:341: comments describe the atomic writer, not an in-place rewrite
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- docs/api-reference.md:1222: the tunnel status shape is { running, url, loginConfigured } (getStatus() returns only running and url)
- src/web/routes/system-routes.ts:1233: the unauthenticated-tunnel warning keys on !isLoginConfigured(), so it no longer fires in multi-user mode
- test/install-sh-invariants.test.ts:537: install.sh must not contain --allow-unauthenticated-tunnel either (it writes service argv as well as env)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The input lease is held across the awaited tmux Enter, so WebSocket frames
keep arriving while it is held. A tagged frame (seq N) was refused and its
seq rolled back, but the browser's WebSocket path does not wait for ACKs:
seq N+1 landed after the lease and moved the watermark past N, and the
redelivered N was then dropped as a duplicate. The keystroke was lost.
Input that arrives during the lease is now held and written in arrival order
when the lease is released, whatever the submit's outcome. A WebSocket frame
is applied and ACKed only once written; managed writes (writeViaMux) and
named keys (send-key) wait for their turn the same way, and a held write that
is still being sent holds everything behind it. A keystroke held before the
guard runs still moves the input revision, so it still invalidates the Enter.
The hold is capped per lease (256 inputs, 1 MiB): past it the WebSocket is
closed with 1013 and nothing later from it is applied, so the browser resends
every unACKed frame in order, and send-key answers 409 CONFLICT instead of
logging a tmux failure as INTERNAL_ERROR. POST .../input keeps its 409 for the
lease plus the held input's drain; its browser drainer is serialized.
Docs: correct the api-reference paragraph that said these keystrokes are
redelivered, and describe the capture fallback and the per-scope cursor cap
from the previous two commits in api-reference, architecture-invariants and
the changeset.
The network acknowledgement that install.sh writes for every passwordless LAN install also satisfied the tunnel guard, so one click on the welcome screen's tunnel link published full terminal control to a public URL with no login and no confirm. The tunnel now needs CODEMAN_ALLOW_UNAUTHENTICATED_TUNNEL or --allow-unauthenticated-tunnel (carried by web -d and service install), and with no login the browser asks before every enable, reading a new loginConfigured field from GET /api/tunnel/status.
The guard is now decided once per request and required again where the tunnel starts, so a save that found the tunnel running can no longer restart it unguarded after a concurrent stop.
Closes#706
Quick-start saved lastUsedCase with an un-awaited, non-atomic read-modify-write, and the settings PUT, model config, case order, case rename, Ralph start and the boot tunnel reset each ran their own. Two overlapping truncate-then-write calls left one write's bytes over the tail of the other (the CI flake in test/quick-start.test.ts), and most writers then read the torn file as {} and wrote back only its own keys, erasing the rest of the user's settings.
All of them now go through one writer that runs one change at a time, replaces the file atomically, and refuses (409 CONFLICT) to overwrite a file that does not parse.
`execFile` signals the `git` parent only, so a fetch that times out can leave its http helper
still running, and on the interval that repeats every run with nobody watching. `runFetch` now
spawns `detached`, so the fetch and the helper it spawns share one process group, and the kill
is SIGTERM to the negative pid then SIGKILL `FETCH_KILL_ESCALATION_MS` later; the escalation is
not cancelled when the promise settles, because the caller is answered at the timeout while the
group is still dying. The output cap is kept, and the clone's slot queue is not needed here.
The tick is two passes: every enabled local session is collected by folder, a folder is due when
ANY session open on it is due, the folder is probed once for the group, and the run is published
to every session on it. A single pass decided by the order the sessions were read, so [long,
short] published only the short one and the 3600 s session's row never updated again while its
folder was fetched every 30 s.
`gitNonInteractiveEnv` writes BatchMode ssh as a default, not an override: `core.sshCommand` is
read inside the same slot and the same bound as the fetch it serves, and `fetchEnvFor` keeps the
repository's answer when it has one, because that is usually the setup that works on a machine
with several ssh setups. The `accept-new` host key it can write is stated next to the BatchMode
note: a prompt is stopped, a write is not.
The write claim is scoped to the Git panel and to the timer, with cloning and worktree management
named as the writes that happen when the user asked for them, and the zh keys sit with the group
of the UI surface their English string belongs to.
Tests: the process-group test proves the tree by group membership, because the helper is a
grandchild (`git fetch` runs `git remote-http`, which spawns `git-remote-http`) and because the
group is what the negative pid reaches, and it remembers each process once found instead of
re-scanning a group it had already emptied. The non-zero-exit test changed premises: `git fetch
--quiet` in a repository with no remote exits 0 on git 2.47.3, so resolving empty stdout there was
correct, and the case that has to reject is a remote pointing at a path that is not a repository.
The join test asserts after the run and does not await the request before the held run is
released, which would wait on the run the test is holding.
Stage now re-reads the pane after typing (every 50 ms, up to 1 s) and
issues a receipt only for a frame with the same generation, the input
revision its own write moved, and a composer hash equal to the
message's. Codex repaints 30 to 60 ms after the keys, so the first
capture was often stale and stage refused with the text already typed.
Every refusal after typing now says the message may be in the composer
and points at the Ctrl+U recovery. A visible-frame capture reads the
screen and the caret in one tmux invocation, with the old two calls as
the fallback.
Codex draws its suggestion popups above the composer and keeps the
footer, so a staged /stat verified and submit ran /status. Stage now
refuses a leading / or ! and a last word starting with @ or $ before
typing, and stage and submit refuse a frame with popup rows just above
the composer. The pattern and characters are registry data
(composerPopupLine, composerCommandPrefixes, composerCompletionSigils),
read from the codex 0.162.0 bottom_pane sources.
Submit waits until 250 ms after the staged keys: codex turns an Enter
within 120 ms of a typed burst into a newline. POST .../input answers
409 with its seq returned while a guarded submit holds the input lease,
instead of 200 for bytes it never wrote.
Also align the attribution memo comment with the code (a verified file
whose incremental scan fails is not re-walked, since the full walk
reads a superset), and list stage as a fromUser source in CLAUDE.md.
The scheduler is the part that runs with nobody watching, so it is decided per folder
rather than per session: sessions sharing a folder are the same clone and share one run
that both see, `inFlight`/`lastRunAt` are keyed by the resolved folder, the schedule is
stamped when a folder is picked instead of when its run starts, and a tick is skipped
while the previous one is still going. The click route joins a run already going for the
same folder rather than starting a second fetch in one clone.
The tick keeps ONE bound for the git processes of the whole run and hands it to every
folder, and bounds its folders with a bound of its own: the same semaphore cannot serve
both levels, because a folder holding a slot would then wait for a slot its own fetch
needs. Both bounds are `FETCH_CONCURRENCY`, so the ceiling is unchanged. The bound claim
in CLAUDE.md, the `Limiter` jsdoc and docs/api-reference.md now describes it as it is.
Before anything is fetched the folder is probed with the bounded path probe, so a case on
a mount that stopped answering is skipped and never touched.
The rest of the review: the hooks are named as they really are (`reference-transaction`,
`pre-auto-gc` through the maintenance a fetch triggers, config commands such as
`credential.helper`) and `FETCH_CONFIG_ARGS` turns maintenance and gc off for an
unattended run; the claim that an agent in the same repository is not disturbed is
replaced by the ref-lock collision it actually is; the click route publishes its own run
as `trigger: 'click'`; `autoSaveGitFetch` uses `_apiJson` so a 400 shows the error toast
and applies the returned settings; and the two wording fixes.
Tests: the scheduler pins one run per folder, the two bounds, the probe before anything
runs, and that closing the session holding the override keeps the folder while another
session is still open on it; the route test pins the click publish and the join, asserting
the join before the held run is released — releasing first lets the run finish and clears
the lane, so the joiner would be entitled to its own fetch and the test would pass for the
wrong reason.
Fetch is the only git command Codeman runs that writes, and the only one that runs a
repository's own fetch-post hook, so it stays behind a click and off by default. The Git
status window's header row gains Fetch; App Settings -> Bottom bar gains the switch and
the interval, synced because the scheduler acts on them, and Session Options -> Git
overrides both per case. The scheduler reads settings fresh each tick, times the interval
from the start of a run, never overlaps runs, and re-checks the Docker and remote rules
every tick before any git runs. Nothing but remote-tracking refs moves: HEAD, the index
and the working tree are proven unchanged against real git while origin/main advances.
Tests: src/git-fetch.ts and the scheduler, the per-case settings route, the synced/display
setting split, the two new i18n rows, and the Fetch block of the Git status browser suite.
Gate inspect, stage and submit behind the synced agentCoordinationEnabled
setting (default OFF, App Settings > Cross-session features, zh-CN
strings). While off, all three routes answer 403 FORBIDDEN naming the
setting, checked before session lookup; the CLI verbs print it verbatim.
Remember the attributed Codex rollout per session and re-verify only its
head on later calls, rescanning only date directories not older than it
for a /new conflict. The first walk reads 1 KiB prefixes first, so a
store of 2,100+ unrelated rollouts still attributes.
Answer unexpected inspection errors with 422 OPERATION_FAILED and log
the session id and error code only.
Read the guarded composer glyph and footer from the CLI registry: codex
workDetect.promptGlyph and a new workDetect.composerFooterLine
capability, compiled through compileVersionRegex. Codex footer effort
words are one shared list with modelDetect.screenLine.
Fix the wiki API reference link, frame the versioning policy entry as a
case of the experimental rule, join the CLAUDE.md Agent CLI paragraph,
and update the changeset, skill reference and architecture notes.
The rename lock is checked once, in the Session constructor, but a session
reaches the server's map (where the rename route builds its list of open
sessions) only later, after the create awaits the owner's layout lock and a
fresh settings read (and, for POST /api/sessions, inferLocalCaseName). A
rename in that gap neither listed nor stopped the session, moved the folder,
and the create then registered a session whose pane starts with
`cd "<old path>"`, skipping the 409 requiresStop / stopSessions guarantee.
- case-rename.ts keeps a record of sessions constructed but not yet
registered (`noteCaseSessionStarting` / `caseSessionSettled` /
`isCaseSessionStarting`), with a 30 s TTL for one that never registers
(a failed create, a one-shot prompt), swept on every note and check.
- The Session constructor records itself (local sessions only, as the
route ignores remote ones); registerSessionWithLayout()'s register
callback, the boot-restore registration and stop() settle it.
- The rename route answers 409 CONFLICT with renameCode BUSY ("A session is
starting in this case") for a folder move while one is recorded at or
under the folder. The check runs in the same tick that takes the lock, so
every interleaving is either refused here or by the constructor. An
alias-only rename is not refused.
A create whose path was validated before the move but whose Session is
constructed after it is still the ordinary check-then-use race of any
create path; CLAUDE.md now says so instead of "nothing starts mid-move".
Tests: unit (record, settle, stop, TTL), route (BUSY, then requiresStop once
registered; alias-only unaffected) and an end-to-end WebServer test that
holds the create between constructor and registration (it fails without
the route check: the folder moves under the session).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Conflict in src/web/routes/session-routes.ts (the import block) resolved
by keeping both sides: #680's claudeProjectDirName import stays, and the
fastify import takes #690's form without FastifyReply, since #690 moved
abortOnClientHangUp (its only user here) into route-helpers.ts.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
cleanup-state keeps the agent inbox of a removed record whose tmux session the multiplexer still lists (a detached pane the next boot re-adopts under the same id); test and architecture-invariants note added.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude history merges, never overwriting, into a project directory a deleted case of the new name left behind, instead of staying orphaned under the old key.
The free-target pre-check also refuses a dangling symlink (lstat), so stopSessions no longer ends sessions before a 409 EXISTS.
The rename dialog says stopped sessions are closed (their tabs go away) and, after a failed rename, how many were already closed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Boot prune keeps the inboxes of sessions on the reboot-restore offer (their records are already pruned and a rebuild reuses the id), with a case in test/agent-inbox-server-wiring.test.ts.
docs/wiki: post, inbox and ack in the codeman agent command block of Driving-Codeman-From-An-Agent, and "Keep agent inbox across restarts" in Settings-Reference.
codeman agent inbox prints message text through terminalSafeText (ANSI stripped, other C0, DEL and C1 controls shown as \xHH); --json stays as stored.
Shutdown flushes the inbox snapshot again after app.close(), for a post that lands after the first flush.
POST /api/cleanup-state drops the inboxes of the records it removes.
InboxPostSchema derives its fence from MAX_MESSAGE_BYTES instead of the literal.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Restore settings.json (caseOrder, lastUsedCase) when linked-cases.json cannot be written, with the rollback test extended
List Claude Code's per-project ~/.claude.json entry as not migrated in Core-Concepts.md and CLAUDE.md
Say in the 500 that the folder is now at the new path when moving it back fails
Describe the rename dialog's folder checkbox in Core-Concepts.md, carry stoppedSessions on errors, count 5 case SSE events
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- codeman agent post: accept text that starts with "-" (markdown from a
file), --json and -h stay options.
- DELETE /api/sessions/:id on a persisted-only row drops the inbox a detach
kept.
- inbox --wait: wait.ended is exit 3 only when the session list no longer has
the session; a restart or detach ending the wait is exit 2 (wait again).
- A read whose client hung up during ?wait= marks nothing seen.
- InboxPostSchema bound is 4x the byte cap, so every oversize message gets
the store's "post a file path" answer.
- Store: restore sorts oldest first; the TTL sweep checks every message,
not only the first.
- Docs: admins can act as any session of the receiver's owner (now tested
for admin and non-admin); a peek with ?wait= does not block; exit codes.
- Tests: partial PUT /api/settings keeps persistence on.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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>
Only an admin's rename touches the server-wide settings.json, which is now
written before linked-cases.json with the registry restored on failure. The
Resume list follows a renamed folder, the owner's default session group is
carried over, remote sessions are never listed or stopped, errorCode maps
EXISTS and BUSY, and the unmigrated items are documented.