- remove() drops the session's slot once the file is gone: a session that comes
back with the same id (reboot restore) starts afresh instead of answering
`removed` until the next restart. A run in flight stays on its own slot's store,
and the routes start a sync in the same tick as their session check, so a
deletion right after it still stops that sync. A store that is not loaded is
deleted without being parsed (deleteStore).
- A new conversation (/clear, resume) first reads the previous transcript, next
to the new one, to its end with its subagents, so edits it got after the last
read are not lost with its cursors. "Load older" reads the stored conversation
while the switch is pending.
- At most MAX_LOADED_STORES (3) stores stay in memory; the least recently used one
without a run is written if it changed (settle) and loads again on demand.
- Tmp files are named per process start, so a reused pid (a container's pid 1)
no longer keeps a stale one through the startup sweep.
- load() keeps only a plain transcript id (the previous transcript's path is built
from it) and no cursors without one; a pending Bash call with an id, cwd or
agent id over 4,096 characters is not kept at parse time either.
- nextRange() is gone: production reads forward through the reader. The store
tests mirror the reader's range choice, and a new end-to-end test runs the
real reader, sync and store (/clear cases, 40 seeded random transcripts).
- The wiring guard scans all of src for holders of the sync.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Forward: after a line longer than the budget, the next line is read from
its start instead of being skipped with it when it crosses the chunk end.
- Backward: the line before a skipped long line is read whole; a floor that
turned out to be a line start no longer stalls.
- A chunk the shared forward budget cut short is not applied (it read as one
over-long line) but read in a later pass, so a first open of a long main
transcript reaches the subagents in the same sync.
- Load older reads a full budget block of up to three transcripts per click,
taking turns, instead of splitting one budget (a normal Write longer than
the share was skipped for good).
- A subagent transcript that vanished loses its cursor, so Load older does not
stay on forever.
- POST /api/cleanup-state forgets the history of every record it drops; a
startup sweep deletes edit-history files no session or record owns and
stale tmp files of other processes.
- load() rebuilds entries from known fields (string diff lines, per-edit cap
re-applied, ids counted against the byte cap), bounds pending Bash fields
and agent ids, and does not parse a file over 32 MiB.
- No re-read after Codeman's own keep-alive turn.
- Tests: regression tests for each of the above (they fail on the previous
commit); the remove-during-write test now meets the write under way; the
sync and route tests remove their stores before the temp dir, so no late
save recreates it in /tmp.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
transcript:complete fires on a `result` line, and claude's interactive transcript has
none (checked against a live session: user/assistant/system turn_duration, no result),
so an open panel never re-read at the end of a turn. A background subagent writes after
the Agent tool end that started it, so its edits stayed missing until the next edit-
capable tool end or a manual refresh.
The panel now re-reads on its session's session:idle, through _onSessionIdle (the SSE
handler map holds one handler per event). Live: a backgrounded subagent's Write shows
up in the open panel at the end of the turn, without a click.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- The history is deleted when the session's persisted record goes (forgetEditHistory):
kill via cleanupSession (a pinned record is only demoted and keeps it), the
persisted-only DELETE, and stale cleanup at startup. After remove() neither a sync in
flight nor a later request writes the file again; writes are serialized.
- load() re-bounds a damaged file: cursors, pending Bash calls (MAX_PENDING_BASH) and
subagent labels (MAX_AGENTS, oldest unused dropped first).
- Load older reads one block of every transcript, budget split between them, and only
the files it asked for; a block short of budget reads its end. A line longer than the
budget is skipped going back as well (floorMidLine).
- Concurrent first callers share one store; a forward request folded with an older one
still reads forward.
- Images outside the workspace get no thumbnail (one attachment id per render); a click
opens the shared preview, which registers once with notify: false.
- Docs: per-edit diff cap, deletion rule, load-time bounds, thumbnails, follow triggers.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Part 1 of #643 (local sessions only; remote follows as its own PR). An
opt-in, per-device header button opens a panel with the active session's
edits, newest first: every Edit/Write/MultiEdit/NotebookEdit of the main
agent and its subagents with its diff, plus images a Bash call rendered or
the agent sent. Remote, Docker and non-claude sessions answer `unsupported`.
- Reads only on request: `?sync=1` when the panel opens; while it is open on
that session it re-reads after an edit-capable `transcript:tool_end` or a
`transcript:complete` from the existing transcript watcher. No hook, timer,
second watcher or new SSE event; a wiring guard pins it.
- Main transcript: the path the transcript watcher follows (hook-reported),
else `<id>.jsonl` under the session's own CLAUDE_CONFIG_DIR override or
the server's config dir. A config dir set outside Codeman is found with
the next hook; until then the panel says so.
- Store: dataPath('edit-history')/<id>.json, 0600 via tmp+rename, capped at
500 entries and 4 MiB of diff text per session, deleted with the session,
blocked in isSensitivePath, never fed to /api/search. Every route goes
through findSessionOrFail with req.
- Images only through existing file surfaces: file-raw inside the
workspace, the attachment route with notify:false outside it.
- textContent rendering, bounded byte-offset reads, marker-class header
button, hidden on phones (locked in the mobile policy test).
Docs: Working-With-Files, Settings-Reference, architecture invariants, API
reference, CLAUDE.md key pattern.
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>
A user tmux config that leaves the prefix table empty makes
unbind-key -a -T prefix fail and drop later commands in the \; chain,
including send-prefix and remain-on-exit failed. Bind C-q send-prefix,
clear the table, then bind it again.
Also accept macOS ttys in the TUI switch-guard test without reading
/proc, and describe the client_pid hook escape as defensive on tmux 3.4.
Co-authored-by: DDT <1786035110@qq.com>
Docker and owned-remote tmux servers kept the stock prefix table, so Ctrl+Q
then )/(/s/w/L switch-client'd the inner client onto a sibling. Those create
chains now leave only C-q send-prefix.
codeman tui bound M-1..M-9 in the server-wide root table. The keys stay, but
switch-client runs only for the attaching client (tty plus a one-shot pid).
A browser tab's Esc-then-digit no longer jumps sessions, and a killed TUI
leaves a binding that matches no live client.
Co-authored-by: DDT <1786035110@qq.com>
Single-user mode has no per-username bucket, so its per-IP bucket is the
only limit on guesses against the one password, and behind a tunnel every
client shares 127.0.0.1. Put it back on AUTH_FAILURE_MAX (10).
AUTH_IP_FAILURE_MAX (50) now gates only the multi-user per-IP pre-gate,
which runs before the scrypt verify and so refuses even a correct password;
the per-username bucket still caps each account at 10.
Move the headroom test out of the single-user suite and add a multi-user
test that sprays wrong passwords across usernames (9 each) and expects 401
for the first 50, then 429 for the 51st and for a correct password.
Update the docs that still described 10 per IP for every mode, and resync
the plugin mirror of the codeman skill.
One AUTH_FAILURE_MAX (10 per 15 min) gated the single-user per-IP
bucket, the multi-user per-IP pre-gate, the per-username lockout and the
hook-secret bucket. Clients sharing one egress IP (NAT, a tunnel, an
automated client retrying) were throttled after 10 failures in total.
Add AUTH_IP_FAILURE_MAX = 50 and use it for the two per-IP login buckets
only. The per-username lockout and the hook-secret bucket keep
AUTH_FAILURE_MAX = 10, so guessing against any one account, or against
the hook secret, is no easier than before.
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.
After a server restart the byte recording a tab switch replays (`?tail=`) holds only the tmux client's attach repaint, so an inline Claude session came back with an empty local buffer while tmux still held its scrollback. The hollow-buffer fallback then sent PageUp/PageDown to Claude, which moved the composer cursor instead of the transcript, until a page reload. Pre-existing on an unmodified build; every restart of the server showed it.
The paging fallback now stands down when the last capture reported tmux scrollback (`paneHistoryLines` > 0), dropping the gesture debt and any page keys already queued, so the gesture falls through to the local path, whose scroll-to-top pull fetches `full=1` and the gesture scrolls on from there. Zero and unknown counts keep paging (fullscreen Claude, codex, opencode, remote and Docker panes). The pull no longer inherits a downgrade refusal's 60 s latch for such a buffer, drops a completion that outlived a tab switch back to the same session (select generation, not only the session id), records no count from an error envelope (a JSON 5xx would otherwise forget it and hand the next gesture back to paging), and re-positions the local-echo overlay after its rewrite. The refresh path records its count only once it still owns the terminal, for the same reason. The `[scroll]` line prints `paneHistoryLines` so an unloaded buffer reads apart from a hollow one, and the invariant docs record the exception.
Measured with phone emulation after a restart: before, the second visit landed at baseY 0 and every swipe sent PageUp; after, the first swipe fetched the history once with nothing sent to Claude and the gesture scrolled 21 rows, with an unsent draft intact on the overlay.
nextSession/prevSession walk the canonical sessionOrder, but the case and state tab layouts, a sorted rail and named groups reorder the strip, so with any of them a swipe jumped to tabs that were not next to the current one on screen. The swipe now asks for the drawn order, read with the same walk the strip's arrow keys already use (factored out as _visibleTabRows); Alt+[ / Alt+], Ctrl+Tab and Alt+1..9 keep the canonical order. An active session the list does not draw steps in at the first or last row, and an empty drawn list goes nowhere instead of falling back to a hidden session.
Second merge of #674: the author pushed the thumb-arc handback from the review themselves (e8fc08be), byte-identical in code to the merge-time fix already here, so this brings their round in and keeps the browser test and docs added at merge.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
# Conflicts:
# docs/wiki/Mobile-Guide.md
# src/web/public/terminal-ui.js
# test/session-swipe-gesture.test.ts
Conflict in CLAUDE.md's State Files line resolved by keeping #661's new
config-backup entries and this PR's "case default groups" in state.json.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Conflicts in CLAUDE.md, README.md, docs/api-reference.md and
src/web/server.ts were additions beside #662's Copilot quota read and
#666's keep-warm sections; both sides kept.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A scroll whose first move past the tap slop leaned sideways (a thumb arc) locked as 'swipe' for the whole contact, and SwipeHandler drops a swipe at 50 px of vertical drift, so the gesture did nothing. Past that limit the drag is now a 'y' scroll. Also brings the mobile-handlers fileoverview to the current 500 ms / 50 px limits and notes in the Mobile Guide that a selection pauses the swipe and a wide terminal pans instead.
Squash of PR #658. Discovery records each llama.cpp model's parallel slot
count (from --parallel/-np, or /props total_slots when the launch command
states none), and a Claude launch warns when the context split across
those slots leaves less than 40K tokens per request. An explicit
--parallel beside a /props context records a slot count of 1, so a later
round overwrites an earlier count, and a non-2xx /props is remembered per
launch command while a network error is retried.
Merge-time fix by the maintainer: the architecture-invariants sentence on
the explicit --parallel case now says it records a slot count of 1.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Merge-time fix for #676 (review finding 3 and nit 9). The State Files entry
for state.json names the new caseDefaultGroups key, and the Owner tab layouts
paragraph says where a case's default session group sits in the placement
order of sessionCreated() and that only TabLayoutService reads or writes it.
The wiki endpoint map counts the two new case routes (37 to 39).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Squash of PR #662. The header plan-usage chip gains a Copilot row: the
calendar-month premium-request quota, with the request counts and the
reset in the tooltip, read every 10 minutes from GitHub's
copilot_internal/user endpoint with the Copilot CLI's own sign-in token.
It rides showPlanUsageLimits and is on by default while the CLI is
installed and signed in; any failure leaves the chip without the row.
Merge-time fixes by the maintainer: the gates (setting, then token, then
the CLI probe) live in readCopilotPlanUsageIfEnabled() so a host with no
Copilot sign-in never runs the login-shell probe each tick, with a test
that Plan Usage Limits off means no request; the X-GitHub-Api-Version
header the Copilot CLI itself does not send is dropped; and CLAUDE.md,
architecture-invariants, README and copilot-integration describe the read.
The hooks-config import in server.ts was merged with #666's.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Merge-time fix for #676 (review nit 6). Once a Case settings popover had been
opened, every adopted tab layout (each create, delete and reorder) replaced
both selects' options, even with the popover hidden and the group list
unchanged, so a session created elsewhere could reset a native dropdown the
user had open. _refreshCaseDefaultGroupOptions now returns early when the ids
and names are the same. A vm case pins it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Merge-time fix for #676 (review nits 4 and 5). sessionCreated forgot a stale
case default inside the try whose catch rolls the registration back; it now
does so after the try, once the session is committed, so the cleanup can
never drop a committed session from the live map. caseDefaultGroup (behind
GET /api/cases/:name/default-group) no longer deletes a stale id as a side
effect: it reports Ungrouped and leaves the value for the next creation to
forget. The two tests that expected the read to clear it now expect it to
write nothing; the creation test still pins the cleanup.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The remote-history PATH bootstrap that rode along in this PR was left out
at merge (it belongs in its own PR that routes every remote tmux call
through one wrapper), so 242bbfcd's title overstates what lands here.
Conflicts in CLAUDE.md and docs/architecture-invariants.md resolved by
keeping this PR's rewrite of the wheel/touch paragraphs and re-adding
#664's touch-forwarding sentence to both.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Merge-time fix for #676 (review finding 1 and nit 7). The Git worktrees modal
launches Claude through POST /api/sessions with workingDir
<case>/.claude/worktrees/<name>, which inferLocalCaseName did not recognise,
so the session landed in Ungrouped while the same gesture for any other CLI
(quick-start with the case name) landed in the group. The inference now
strips a trailing .claude/worktrees/<name> lexically before its two checks,
reusing CLAUDE_WORKTREE_PATH (hoisted to module scope). It also runs only for
a named workingDir, never on the server's own cwd, as the hook and skill
injection above already do. Route tests for both, failing without the fix;
api-reference and architecture-invariants say so.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Merge-time fix for #660. CLAUDE.md and architecture-invariants still said a
raw npx vitest skips setup.ts, contradicting the updated Testing note: both
now say it resolves the root vitest.config.ts (and why that file must stay).
The architecture-invariants entry for safeRmHomeTree adds the sandbox-HOME
requirement (isSandboxedHome, TEST_HOME_PREFIX), the same rule for
isUnderTestHome, the route-harness assert and the 2026-10-10 incident.
CLAUDE.md's repo-root preamble lists vitest.config.ts with its reason, so a
tidy-up does not move it into config/ and reopen#640. The
assertSandboxedHome JSDoc no longer claims every route test is covered: only
the ones built on createRouteTestHarness are.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Squash of PR #669. Browser terminals could be moved onto another session
by tmux's prefix keys: Ctrl+B then ), (, s or w switched the tab's tmux
client to a different session while the tab kept the original's name.
Codeman's tmux socket now keeps only Ctrl+B Ctrl+B (send-prefix) in its
prefix table, and codeman tui and scripts/tmux-manager.sh get F1 as their
way out instead of Ctrl+B d.
Merge-time fixes by the maintainer: on an empty prefix table the
unbind-key that fails no longer skips binding send-prefix; a key tmux lists
escaped (prefix C-\ shows as C-\\) compares equal, so the reset does not rerun on every create;
and reconcileSessions() reduces the table at boot when the socket has live
sessions, so sessions that survive an upgrade are covered too.
Co-authored-by: christianhaberl <116439789+christianhaberl@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Merge-time fix for #661, as promised in the reviews. The long Config backups
paragraph in CLAUDE.md moves into a new architecture-invariants section that
gives each rule with the reason behind it (allowlist, volatile keys, retention
by successor and the date floor, the origin id, retention after a bad or
reset settings.json, the bounded probe, one walk per pass, verify before
write, the undo copy and the partial-restore answer, the reload hooks,
redaction, admin only). CLAUDE.md keeps the short rules and a pointer, and the
State Files list names config-backup-id and backups/config/ with the
retention each manifest records.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Merge-time fix for #665.
- A markdown link converted to a preview link became `a.rv-path`, so its
label ("Our incident record") took the path style: monospace, 0.92em
and `word-break: break-all`, which split prose mid-word on a phone. The
anchor now also carries `rv-path-link`, and
`.rv-text a.rv-path.rv-path-link` resets font and word-break, as the
File Viewer does for its labelled document links.
- The first new test adds `/tmp/a.json?raw=true` (pins the `?query`
strip) and `/tmp/` (pins the trailing-slash folder check, since
findFilePathLinks finds no folder in a bare `/tmp/`), and checks the
new class.
- The `_linkifyFilePaths` JSDoc and docs/architecture-invariants.md say
the function also rewrites anchors whose href is a scheme-less,
non-`//` path, on purpose without FILE_PATH_LINK_PATTERN's root and
extension filter.
- Mobile Guide: "a web link" in the response viewer opens a tab.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Merge-time fix for #661. The pre-restore copy was forced, so a restore made
while the live files equalled the newest backup spent a retention slot on a
duplicate. It now goes through the normal change check: when nothing changed
since the newest backup, no copy is made and safetySnapshot names that
backup. Two tests that relied on the forced copy now change a file before the
restore; a new one pins the reuse.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Merge-time fix for #674: the mobile-handlers.js @fileoverview still
gave the old 300 ms and 100 px limits (now 500 ms and 50 px), and the
Mobile Guide now says a visible text selection pauses the swipe and a
terminal wider than the screen pans instead of switching.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Merge-time fix for #674: the first travel past the tap slop decided the
axis for the whole contact, so a scroll that opened on a thumb arc
(+9 px across, +5 px down, then straight down) locked as 'swipe'. Every
later touchmove returned before the scroll code and SwipeHandler gave
the swipe up at its 50 px drift limit, so the gesture did nothing. Past
SwipeHandler.maxVerticalDrift the drag is now handed to 'y' (and the
published axis with it), so it scrolls; a clean sideways swipe, a
vertical scroll and a long-press drag are unchanged.
The source pin in test/session-swipe-gesture.test.ts moves with the
code and also pins the drift value, and a new browser test drives the
real touch listeners: the thumb-arc drag ends as a scroll (it fails
without the fallback, axis stays 'swipe') and a clean sideways swipe
stays a swipe with nothing scrolled. The architecture note and
src/web/CLAUDE.md now say a swipe drag is consumed only up to that
limit.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Merge-time fix for #659.
- Alt+Left/Right (File Viewer back/forward) is a display-only entry in
DEFAULT_SHORTCUTS, so the Ctrl+? overlay lists it, and a row in the
help modal's Panels group, pinned in help-modal-shortcuts.test.ts.
zh-CN entry for the new label; the "Restore size" entry is dropped,
since nothing shows that string any more.
- The wiki row moves from "Sessions and tabs" to "Everything else".
- The grip comment in styles.css says only phones hide the grip;
tablets show and use it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>