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.
docs/api-reference.md listed POST /api/cases/:name/rename in the Settings
table, whose introduction says every route in it answers 403 FORBIDDEN to a
non-admin in multi-user mode, and its paragraph said caseOrder and
lastUsedCase are always updated. Neither is what the route does (and what
CLAUDE.md, the wiki and test/routes/case-rename-routes.test.ts say): a
non-admin renames their own case (200), and only the server-wide
settings.json write is skipped (`if (isAdmin(req))`).
The row leaves the admin-only table (its body moves into the paragraph), the
paragraph says the route is not admin-only and that caseOrder/lastUsedCase
follow an admin's rename only, and a linked case's 404 for a non-admin is
spelled out.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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>
`run()` relied on execFile's `timeout`, which only sends the child a
signal: execFile's callback, and so the promise, settles when the child
exits. An nvidia-smi stuck on a hung driver (uninterruptible sleep) never
does, so `readNativeGpu()` stayed pending, every later `?gpu=1` request
joined that promise through `inFlight`, and since the route awaits the
sensors before `getSystemStats()`, CPU and MEM froze too for every device
with GPU Stats on. The backoff (`nvidiaSkipUntil`) only armed once the
child finally exited.
`run()` now races its own unref'd timer and answers undefined at the
deadline, clearing the timer when execFile settles first. The existing
failure path then arms the 60 s backoff as for any failing reader. The
PowerShell RAM probe shares `run()` and gets the same bound.
Test: an nvidia-smi that never calls back answers within the deadline, the
next poll does not run it again, and its late exit changes nothing (the
test fails on the previous `run()`). Checked against a real TERM-ignoring
child on PATH: the read settles at 1.5 s instead of when the child exits.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
#689's density steps hide every `.header-system-stats .stat-label` once the
toolbar is narrowed, and #692's readings (GPU, VRAM, GPU MHz, CPU MHz) have
nothing else naming them: at tight, dense and tiny the header showed "40%"
beside CPU's "12%", "3.2G" beside MEM's "7.9G", and two bare "1800 MHz" /
"4200 MHz" with no way to tell the GPU clock from the CPU one. At Compact +
tiny, where values are hidden too, the two clocks (no ring) were left as
0-px items that still took an 8 px flex gap each. Neither PR shows it alone.
- The four host items carry `stat-item--host`, excluded from the density
label hide, so they keep their labels at every step.
- `stat-item--ringless` (static on the two clocks, toggled by
`_updateHostSensors` on VRAM while its total, and so its ring, is unknown)
is hidden at Compact + tiny instead of staying an empty item.
Tests: the browser test drives every density in all three styles against
the real markup and styles.css (it fails on the previous files), and the
unit test pins the VRAM ringless toggle.
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>
Conflict in config/test-suites.ts resolved by keeping both sides: #689's
header-divider and #692's header-host-sensors browser tests are both
listed in BROWSER_TEST_GLOBS.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Conflict in docs/architecture-invariants.md resolved by keeping both
sides: the hollow-pane paragraph carries #681's unloaded-scrollback
sentence, #698's Codex-menu rewording and #699's notch rewording (a
word-level three-way merge; the three edits touch disjoint sentences).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Conflicts resolved by keeping both sides:
- src/web/public/terminal-ui.js (_maybePageCliTranscript): #698's
Codex-menu consume check sits right after the open-dialog consume
check (its sibling, as the docblock lists them), and #681's
unloaded-scrollback fall-through (paneHistoryLines > 0) follows it.
- docs/architecture-invariants.md: the hollow-pane paragraph carries
#681's unloaded-scrollback sentence and #698's menu rewording.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The new-session row's agents now go through _runModeAvailableForCase (session-ui.js), the rule extracted from _refreshRunModeAvailability, for the case the row launches in: a Docker case offers what its container has (live probe, else availableModes, else ungated), so a container-only Run mode is offered and is the row's default again.
The default is resolved per render (pick, else the Run button's mode, else the first offered), so a query that moves the row to another case starts on the Run mode when that case offers it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Reserving a slot per request refused a correct password with the slots it had
filled itself: after a restart the browser replays its cached Basic header on many
parallel requests. Identical credentials from one IP now join the verify already in
flight for them (keyed on a SHA-256 digest), skipping the gates and its scrypt cost.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
test: pin the double-quoted PATH literal of withRemoteTmuxPath() (dash 0.5.11+, bash and busybox pass the shell cases unquoted)
docs: note in architecture-invariants that a new remote pane inherits the widened PATH from the creating tmux client
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>
Give a client that fails the M-1..M-9 guard its key back (send-keys <key> as the if-shell else argument), so a browser tab's Esc then digit reaches its pane instead of being eaten, also after a TUI killed before restore(); only M-1..M-9 are accepted since the key is now interpolated into that command
Test it: the unit test pins the else argument and the key check, and the real-tmux test runs cat -v in alpha and waits for the other client's Esc 2 to echo there, both before and after the TUI dies
Say so in docs/architecture-invariants.md and docs/tui.md, and reword the killed-TUI and failed-arm notes that said the keys do nothing
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
In multi-user mode the per-IP and (username, IP) gates read their counts before
the verify and wrote them after it, so a concurrent burst from one IP passed both
on one stale read (29 wrong guesses plus the right password logged in). A request
that passes both gates now takes its slot before the await; a success clears it.
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>
Keep the Run button's mode when fewer than two agents are offered (_commandPaletteAgent), so a Docker case whose host offers only Shell still starts the container agent the Run button shows; pinned by a new palette test.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
terminal-ui.js: the _maybePageCliTranscript JSDoc names PAGE_KEY_NOTCH_DEBT_MS as the stretched gap after a notch-sized opener
terminal-scroll-routing.test.ts: the notch-opener test comment uses the real numbers (36 rows, 14 rows owed)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
test: record sendInput('-x hello') and sendInput('-\r') and check the shell argv ends -l -- <text> on both send-keys -l branches
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
test/tile-grid-sse-filter.test.ts: search for the renamed 'const eventSource = new EventSource(' end marker and require both ends, so the connect-URL check covers that block again instead of the rest of app.js
test/sse-connect-timeout.test.ts: cover the replaced-EventSource guard in _handleSseConnectionFailure() (a late error from a replaced attempt leaves the new one alone)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Re-read the per-IP failure count after the scrypt await, so concurrent failures from one IP are all counted (regression test added).
Reword the (username, IP) bucket comment: it grows with failed verifies per pair, not no faster than userFailures.
Reword the verify-first test comment: only the per-username bucket never denies a correct password.
Correct docs/multi-user-plan.md: the per-username bucket does not stop cross-IP brute force; the per-(username, IP) bucket caps verified guesses.
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>
host-sensors: never await the PowerShell RAM probe on a read; keep the resolved total and show the memory in use alone until it answers (test with a held PowerShell)
test: rename the Windows sampler test that said the GPU counters are added "for good"
test: make host-sensors.test.ts's @fileoverview describe what that file covers
host-sensors: make the nvidiaSkipUntil JSDoc describe the back-off deadline
docs: Settings Reference says the host readings live inside System Stats and nothing is fetched while it is off
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Multi-user: add a (username, IP) failure bucket checked before verifyPassword, capped at AUTH_FAILURE_MAX, with the same decay as the other buckets; the per-IP pre-gate stays at AUTH_IP_FAILURE_MAX and the per-username bucket stays after the verify
A correct password now also clears that account's (username, IP) bucket, beside the IP and per-username buckets
The new bucket is disposed on server stop like the others; the comments above the gate and the constants' JSDoc describe the three buckets
Tests: 10 wrong then the correct password from one IP is a 429, the same account logs in from another IP, another account behind that IP is unaffected, and a correct password clears the pair bucket
test/multiuser-auth.test.ts: the spray test loops to AUTH_IP_FAILURE_MAX instead of a literal 50
Reworded the rate-limit lines in auth-config.ts, security-architecture.md, README.md, README.zh-CN.md, wiki/Security.md, endpoints.md (plus the plugin mirror), CLAUDE.md and architecture-invariants.md
docs/wiki/Remote-Access.md: add the multi-user note to the tunnel page
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
pageKeysForGesture comment (terminal-ui.js:218): the gap stretches to PAGE_KEY_NOTCH_DEBT_MS after a notch-sized opener, not only PAGE_KEY_DEBT_MS
pageKeysForGesture comment (terminal-ui.js:286): a repaid debt ends either lifetime, not only the one-second one
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
changeset: say Codex does not track the mouse "measured on 0.162.1 and 0.164.0", since 0.163.0 fullscreen was measured tracking it
docs/architecture-invariants.md: list page-keys-dialog-open and page-keys-menu-open among the [scroll] decisions
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
disarmTuiSwitchGuard matches the closed `#{client_tty},<tty>}` form, so a TUI on /dev/pts/4 no longer removes the hook armed for a second TUI on /dev/pts/41 (unit test plus a real-tmux case)
beginAttachHandoff arms the one-shot attach hook as its last tmux call before spawnSync, after the switch keys and the per-session dressing loop, with an order assertion in tui-app.test.ts and a sentence in the invariants paragraph
docs/remote-sessions.md and the buildRemoteLaunchCommand JSDoc show `remain-on-exit failed` at the end of the remote create chain
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Clamp the stored toolbar width to MIN_WIDTH in apply() and step the arrow keys from the width on screen, so a drag to the window edge or a held ArrowRight no longer stores a negative width or leaves an ArrowLeft dead zone (browser test added)
Drop Escape as a reset key on the handle: the global capture-phase Escape handler already closes every panel on it (static pin added)
Say in the changeset, the wiki and the densityFor comment that the percentages go at the narrowest step in the Compact style only, and drop Escape from the changeset
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
palette agent switch: aria-hidden on .command-palette-agent, arrows lose role="button" and aria-label (no nested buttons in the row)
i18n: zh-CN entry for the "Agent for the new session (Left/Right to change)" tooltip
docs: Keyboard-Shortcuts Ctrl+K row says Left/Right change the agent on the New session row
test: pin the aria-hidden switch and the zh-CN tooltip entry in command-palette-ui.test.ts
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
docs: architecture-invariants (arrow-key walk section) names the phone swipe beside the arrow keys as following the drawn order
docs: root CLAUDE.md (Owner tab layouts) carves the phone swipe out of "every other order consumer reads sessionOrder"
test: pin the grouped-tree branch of _stripSessionOrder() (sorted rail, header and web tab skipped, groups not interleaved)
test: the step-in case uses a drawn order (homelab, w1, w2) that master's canonical walk and the old arithmetic cannot satisfy
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
_notePaneHistory JSDoc names the responses it skips (an error envelope on any path, a refresh that lost its owner, a pull whose select generation moved)
selectSession records the pane count only for a non-error envelope (capture.json?.success !== false), pinned in the static guard of test/pane-history-memory.test.ts; the two invariant paragraphs now name the tab switch beside the refresh and the pull
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>
docs: document the SSE connect timeout next to the staleness watchdog in src/web/CLAUDE.md and docs/architecture-invariants.md (clear order, replaced-EventSource guards, offline handler leaves the timer)
test: rename the offline case to "connectSSE() while offline cancels the pending timer" so it names the path it drives
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
src/tmux-manager.ts: escapeTmuxTrailingSemicolon() JSDoc says every send-keys -l passes -- before the text, and its example uses the -l -- form
test/tmux-submit-paste.test.ts: the trailing-semicolon wiring test matches -l (?:-- )? so it pins only the semicolon rule
test/tmux-submit-paste.test.ts: master's two recorded-command tests for typed text expect the -l -- form (merge interaction with the inputRunner() refactor)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>