The test waited for a real 1000 ms timeout and read the pid file afterwards.
On a loaded machine the kill could land before the fake ssh wrote it. It now
waits for the pid and sends the timeout's SIGTERM itself, which is all Node's
timeout does. Without exec the fake still survives and the test goes red.
The invariants said "the reads" go through buildRemoteEditHistoryCommand; the
image probes go through remoteProbePaths, on the same limiter.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every string the block shows now has a zh-CN entry, harvested by a test from the
real module. The row's mode comes from the CLI catalog, not a list of ids that
had already missed omp and copilot. The close dialog's line is static, so the
translator can reach it.
A non-admin's poll answers 403, which read as a failed poll: "Could not reach
the server" under a heading they cannot use, and a request every eight seconds.
That now stops the polling and shows no block.
Wide-scan notes are kept apart from local ones, so the next local poll no longer
wipes "remote X: unreachable" while its rows stay on the list.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The wrapper's workingDir is the neutral /tmp, and everything master grew since
1.33.2 that branches on session.remote or session.docker read that as a local
session working in /tmp, because an adopted session keeps its connection on
adopt.remote and adopt.docker.
So uploads wrote into /tmp and closing the tab recursively deleted the shared
folder; the Git panel, its fetch and the fetch scheduler ran git on whatever sat
there; the image watcher, Read My Mind and the Discord bridge used that folder;
last-response paired a transcript by it and could show another conversation's
answer as this one's; a magic link in a container's output registered a file
from this host's /tmp; and guarded input could type into a human's composer.
Each now refuses or answers unsupported, the browser hides the rows that cannot
work, and Session Options shows where the foreign pane was instead of /tmp.
The custom-model route refuses as well, since it restarts the CLI in the pane.
The local version probe is skipped: the local binary says nothing about a
process someone else started, possibly on another host.
Also moves the guard test to port 0, which test-ports-guard now requires.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The read script carries one case line per subagent cursor, and the whole
script is one argument of the ssh line, which Linux caps at 128 KiB. From
about 1000 subagents the command failed with E2BIG. The cursors have no cap
and the stored conversation is read first, so the session stayed unavailable
and not even a /clear got past it.
A read with more than 300 subagent requests now takes several round trips.
The cut is by name range, not only by count: the script takes a file no case
line names for a new one, so plain groups would read each other's files as
new. Each round trip walks only the names between its cuts, through one awk,
so a new file lands in exactly one range. Budget and meta budget carry over,
and a later round trip still lists its files, because a requested file missing
from the result loses its cursor. A failed round trip fails the whole read and
nothing of it reaches the store. A read of up to 300 runs no awk at all.
Each round of the 60-seed fuzz is also cut at a random size and compared with
the local reader. Run outside the suite too: bash --posix, BusyBox and mawk,
and bash 3.2 in a container, which is macOS sh. That shell cannot parse a case
inside $(...), so the names come from a shell function.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Docker discovery now spawns dockerEngineArgv(), not the shell-quoted
buildDockerBaseArgs(): execFile passed those quotes through, so docker
--context "'default'" failed and every container behind a named context or
remote daemon read as unreachable.
The ssh probe and attach gain withRemoteTmuxPath() like every other remote tmux
call, run in an explicit sh because the guard chain is POSIX and the remote
login shell may be fish, and the probe holds a slot of the remote-ssh limiter
and execs ssh so a timeout reaches it. Notes from a remote scan are no longer
dropped, and every note now opens with its location.
The adopted branch of killSession no longer blocks the event loop on docker or
tmux, kills the in-container view by exact name, and clears the pane-exit and
reconnect state the non-owned remote branch does. An adopted wrapper reports
no injected statusline, since nothing could carry it. Key tables on the foreign
server are left alone on purpose, and the invocation now says so.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Session 0 in two containers, or one name on this machine and on an ssh host
with the same uid, shared a (socket, name) pair, so opening one returned the
other's tab and its row read "Go to tab". foreignTargetKey() is now the one
key, built from the location, the host identity, the socket and the name, and
both the one-wrapper check and adoptedBy read it.
Both also read live sessions instead of mux records, which only exist once the
first attach starts the pane. An in-flight mark closes the rest of that window:
the route awaits the registries and the layout lock before the session lands in
the map. Docker targets are keyed per engine for the same reason, or a second
daemon's container of one name was never scanned.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Applies the review of #766.
The live lookup ran tmux on the bare PATH of the ssh command. On a host
where tmux sits in ~/.local/bin or /opt/homebrew/bin it found no pane, the
2>/dev/null hid that, and the panel stayed on the launch conversation after
a /clear. It now goes through withRemoteTmuxPath(), inside the sh script and
scoped to the $(...), so the other tools keep the PATH they had. The fake
tmux of the test block moved to $HOME/.local/bin for the same reason: that
directory is first on the new PATH, so a real tmux is never asked.
The two ssh commands get exec. Node signals the local sh -c, and dash stays
the parent of a plain command, so ssh survived the timeout. A test with a
fake ssh shows the process dies now and survives without exec.
The comment at the meta budget claimed a meta past it comes with a later
read. A meta comes only with a subagent's first read, so it is never sent.
Docs: the store is named as the exception to "never copied to local disk",
remoteProbePaths and the remote transcript lookup are described, and the
line saying a remote session loads no store is gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fair rounds give a directory its node before reading it, so every folder
still queued when the 5,000-node budget ends sat in the tree with empty or
partial children and read as an empty folder. Keep the node on the tree cursor
and set an additive incomplete: true on every cursor left in the queue, then
draw one dim, inert row under such a folder while it is expanded. The envelope
check ignores keys it does not know, so old and new clients stay compatible.
Stop each round at the top once the budget is spent, before the shift: the
rest stay queued unread, and flagged, rather than read and sorted for nothing.
Say what the walk is: a per-directory round-robin (breadth-first), fair per
directory and not per subtree, with search results now shallow-first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The owner's answer to the PR's open question: the switch does not also pass
--sandbox workspace-write. Settings, wiki, skill and release note now state
the consequence: a folder Codex runs read-only keeps the network closed and
asks per command. A golden test pins the decision by name, and the stale
launch-defaults comments now name networkAccess.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both sides rewrite the /files walk. Keep the fair rounds from #614 and carry
the two things #772 does to the walk onto them: both queues start at walkRoot,
and the search predicate reads a path relative to the listing while every
returned path stays relative to workingDir.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The This Mac tab runs install.sh --no-admin itself. The two questions only a
person can answer move into the app: which coding agent to install, shown only
when this Mac has none, and who may reach Codeman. The installer's output goes
to a file, so a running install survives the app quitting and is picked up
again when it reopens.
install.sh gains --ai-cli, --progress and a preflight subcommand, which is what
the app reads its choices from, so it keeps no copy of the CLI catalogue.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings the 1.33.2 based adoption feature up to master. Conflicts were
resolved by keeping master's intent and re-applying the feature on top:
both sides kept for imports, the script tag and the stylesheet blocks; the
trust-dialog guard keeps master's stopped check plus the adopted refusal;
the Ralph and image watchers keep master's remote branch plus the adopted
skip; the close confirmation keeps master's worktree option plus the
adopted wording.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflict in src/session-cli-registry-bridge.ts: kept the PR's gateVersion() (gates measured against the gated CLI's own version) and added master's Discord startup hint engine value.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A tcsh login shell worked on 1.48.0 only because the local sh rewrote
${SHELL:-/bin/sh} to this host's shell. With the launch verbatim, tcsh gets -i -l,
refuses it and every stock launch leaves a dead pane. The changeset and
docs/remote-sessions.md described that as a standing limitation; they now name it
as a change in this release, with what the override workaround costs.
Also: extraSshOptions joins the list of values now literal at launch, the stock pane
command is shown as /bin/sh -c in the docs and the builder's JSDoc, the invariants
state how the launch must reach the far end, the manual pages cover $ in an override,
and the invariants no longer say extraSshOptions refuses a bare $ (it never did).
Reworded the em-dashes on the invariants line this rewrites.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Records the limit on a session with its own CLAUDE_CONFIG_DIR, which does not
carry its conversation across an endpoint because the isolated projects link
goes to the server's tree. Also records that an own home set before a restart
with no selection is lost on clear, that only an absolute path is persisted,
and tells the user manual what a clear gives back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ownerClampedEnvKeys() read enabledClis(), so disabling a CLI dropped its
privileged keys from the clamp, and a revoked owner got a saved home back on
recovery or reboot restore. getCli() still returns a disabled entry, and a
session can outlive the switch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
getCustomModelForPersist() trusted homeEnvVar to name a path, but the registry
types it as any env name, so a clis.json override could write a session's own
credential to state.json. Only an absolute path is written now.
A re-apply resolved the CLI's real home from what the session's map held, which
under a selection is the injected directory, and recovery resolved none at all.
An entry the own home lacks (settings.yaml) was then linked from the server's
home on an endpoint switch and on recovery. The route now reads the new
Session.ownEnvValue(), and recovery reads the clamped record through the same
bounded guard, under the bulk cap since it walks every saved session. Also
corrects the comment that said only a removed directory gets its links again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The gate comment in terminal-ui.js said a stale-false flag falls through
to the PageUp/PageDown fallback, which is false for pi since the second
commit of this PR removed that fallback. The FOOTGUN comment above it made
the same claim. Say what happens instead.
"Every pi below 1.0.0 emits no mouse enables" was too absolute: regular
was the default there, but fullscreen exists from 0.84.0. Measured with
--tui-mode fullscreen under tmux on 0.84.1, 0.85.0, 0.86.0 and 0.99.2:
alternate_on=1, mouse_any_flag=1, mouse_sgr_flag=1. Default settings on
0.99.2 report all 0.
Document that Wheel Scrolls Local History leaves the wheel dead in
fullscreen pi (wiki, integration guide, changeset), add a pi block to
Troubleshooting, cross-reference the accepted exception from the dead
gesture rule in architecture-invariants, and mark the pi-integration-plan
passages this PR reversed as superseded.
Comments and docs only, no runtime change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RemoteCommandOverridesSchema is .strict() and stopped at deepseek, so the API
refused a commands.omp or commands.copilot override although both modes can run
remotely and in a container (RemoteCommandMode, DockerCommandMode) and the launch
builders honour the key. The docs call the override the way out for a login shell
the stock launch cannot run, which was not true for those two modes.
The test walks the CLI registry, so the next CLI cannot miss the schema the same way.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The API reference still said a registry failure on create and clone is
500 and did not list the rename's new 409s. CLAUDE.md said both files
are restored after a failed registry write, and left out the
config-backup restore exception.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The rename's checks inside mutateLinkedCases and the refused-registry
branches of rename and clone had no test: removing them left every
case route test green. A hook right after the folder move lands a link,
an unlink, a re-pointed entry or a torn file between the route's first
check and its registry write. Each new test fails when its check is
removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The security wording claimed a folder strictly below workingDir was the whole
argument, which the symlink fix showed was only the lookup's half. The rest is
docs that were missing: liveWorktree, root=live and worktree in the API
reference, the mechanism section the CLAUDE.md paragraph had no pointer to, what
a root=live query matches against, and the limits a reader meets in the wiki
(the Git panel stays on the session's folder, a session started below the
repository's top is not followed, a page under .claude/worktrees opens as
source).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The lookup judges liveWorktree.path lexically and only when the pane moves,
while the route's probe and readdir both follow symlinks. A symlink put at
that path, or at a parent of it, without moving the pane re-rooted the listing
outside workingDir. Names and sizes leaked, while file-content and file-raw
still refused the paths.
The route now resolves both folders on every request and walks the worktree
only when it lies strictly inside the resolved workingDir, otherwise it walks
workingDir. Only the check uses the resolved paths: the walk keeps the session's
spelling, so a workspace reached through a symlink still gets paths relative
to workingDir.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The graphical install runs install.sh --no-admin, so this branch needs both
PR #763 (the app) and PR #768 (the route that needs no password).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
transcriptPageKeys applied to every pi, and a pi in `tuiMode: regular` binds
PageUp/PageDown to its editor. It emits no mouse enable, so the flag stays
false, and on a fresh pane the buffer is hollow: the wheel sent PageUp, the
caret of a multi-line draft moved, and the next keystroke landed on the wrong
line. Measured on real pi 1.1.0, 1.4.0 and 1.5.0. Regular mode is not exotic,
it only takes one line in pi's settings. Telling the two modes apart would
need a new capability, since the paging gate takes no id, so pi has no
fallback: a fullscreen pi with "Wheel scrolls local history" on stays where
master left it.
altScreen, the half of the fix that does the work, was pinned by nothing:
reverting it left 1328 tests green. The strip decision table reads its
expected value from the same registry entry. pi now has its own block using
the bytes it really emits, which also pins that focus reporting survives.
The fullscreen default dates from pi 1.0.0, not 1.4.0, so the comments say
that, and the sites still calling pi narrow-strip or main-screen by default
are corrected. Where a comment listed CLIs it now names the capability.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Handing the launch to bash verbatim exposed `"${SHELL:-/bin/sh}"` to the far
end for the first time. tmux runs a one-argument command through the remote
default-shell, not /bin/sh as the comments said, and fish refuses `${...}`:
on a fish host, or a bash login with `set -g default-shell fish`, every new
pane died with status 127. master only survived because the local sh had
already rewritten that token to this host's shell path.
Codeman's own text now goes to `/bin/sh -c` as separate tmux arguments, which
tmux execs without a shell. A commands.* override stays one argument and keeps
the default-shell it was written for: under `/bin/sh` a `source` or `[[` in an
override dies with 127 on bash and zsh hosts.
The tests read the argv tmux would receive through a real sh, so they need no
fish: every stock launch must reach /bin/sh and an override must not. The
docker half of bashCArg was pinned by nothing, so it gets its own test.
Comments that described the removed double-quote layer are corrected, and
remote-hosts.ts no longer claims overrides are seeded from the default text
or that they rescue nushell and elvish.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude Code's EnterWorktree moves the running process into
<repo>/.claude/worktrees/<name>, while Session.workingDir stays on the main
checkout. The Files panel kept walking workingDir, so it listed a tree the
agent had left.
paneCwd already follows the move, measured on Claude Code 2.1.318 in both
directions, so nothing new has to track the agent. It also follows every cd
in a shell, so a new SessionState.liveWorktree narrows it to a linked
worktree, read from the .git file without starting git.
Only a folder strictly below workingDir is ever an answer, and the listing
only starts there: returned paths stay relative to workingDir, so the
preview, raw and edit routes are unchanged and confinement does not widen.
A worktree outside the session folder stays unfollowed.
The panel asks with root=live, shows a strip naming the worktree with a
switch to the session folder, and follows the agent in and out while open.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pi 1.4.0 is an alt-screen TUI that enables mouse tracking once at start.
Codeman's replay (tab switch, reload) drops that mode in xterm
(mouseTrackingMode none, baseY 0), so the wheel hit an empty local buffer
and stayed dead until a resize made pi redraw.
Same shape as copilot: altScreen strip-mux-and-mouse (the server records
the stripped DECSETs as cliMouseTracking, which survives the replay),
wheelForward always, transcriptPageKeys (PageUp/PageDown fallback for a
stale flag, measured on pi 1.4.0). _shouldForwardWheelToApp treats pi like
copilot. Registry pins and docs updated.
Tests: 2 new in terminal-scroll-routing (red before). Full run: 12989
passed, 5 red from the de_DE locale / XDG env (custom-model-run-menu-ui,
git-worktrees, edit-history-sync), all green under LANG=C.UTF-8.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
(cherry picked from commit 53e4136d9f4401e9250ac6a122599ffcb2a57e8a)
respawn-pane runs `bash -c "<ssh …>"` through the local sh, so every `$` in
the remote command is expanded on the Codeman host. withRemoteTmuxPath
(`export PATH="$HOME/.local/bin:…:$PATH"; tmux …`, 5b9445de) therefore sends
the Codeman host's home and PATH to the remote host: a remote user with a
different home (tmux in ~/.nix-profile/bin) gets "tmux: command not found" and
every new remote pane exits 127. `${SHELL:-/bin/sh}` in the remote launch was
the local shell for the same reason.
Remote and docker commands are already quoted for the far end, so they now go
to `bash -c` single-quoted (bashCArg); local commands keep the double-quoted
form. Two tests run the captured respawn-pane line (create and respawn)
through a real sh with a foreign HOME/PATH and check that ssh gets `$HOME` and
`$PATH` unexpanded.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 2818e9455489b9acd65841258df4cd035609954c)
Codeman itself always lived under $HOME; administrator rights were only
spent on what it needs first (system packages for Node.js, tmux and git,
Homebrew on a Mac). An account without those rights could not install it:
the installer died in ensure_sudo, or in Homebrew's own installer.
install.sh now has a no-admin route, taken by itself when the account has
no administrator rights (no sudo binary; outside sudo/wheel/admin and
`sudo -n true` fails; a refused sudo password; a Mac account outside the
admin group with no writable Homebrew) and forced with --no-admin or
CODEMAN_NO_ADMIN=1:
- Node.js: the official tarball from nodejs.org, checked against the
SHASUMS256.txt published beside it, into ~/.codeman/runtime/node.
- tmux: the tmux project's own build (tmux/tmux-builds 3.7c), checked
against a SHA-256 pinned in the installer, into ~/.codeman/runtime/bin.
- Both are linked from ~/.local/bin (never over a file that is not ours),
and the runtime is on PATH and on the LaunchAgent's PATH.
- git is never installed. Without a working git the source comes as an
archive, the install is marked with .install-source, and it updates by
re-running the installer: the new tree is built beside the old one and
swapped in only after the build succeeded. A later re-run on a machine
that has gained git turns it back into a clone.
- Nothing on this route runs sudo, Homebrew or `xcode-select --install`,
and the route is never decided by `sudo -v`.
What it cannot supply is checked before the first download: a C/C++
toolchain on Linux (node-pty 1.1.0 compiles there), musl, and macOS below
15 (the tmux build's deployment target).
Along the way:
- check_git no longer trusts `command -v git`. On a Mac without the Xcode
Command Line Tools /usr/bin/git is a stub that opens Apple's install
dialog; it is judged by `xcode-select -p` and never executed. Any other
git is asked for its version.
- scripts/git-hooks.mjs answers "not a checkout" without running git, for
the same reason: postinstall calls it on every npm install.
- The in-app updater knows the install kind `archive` and points such an
install at the installer instead of `npm i -g aicodeman@latest`.
- download() and download_to_stdout() resolve DOWNLOADER themselves. Only
main() used to set it, so `install.sh cloudflared` died on `set -u` with
the message swallowed by the step log.
- The AI CLI menu leaves out a Homebrew-only install command where there
is no Homebrew.
Verified with the real installer: a bare Ubuntu 24.04 container as an
unprivileged user (install, a PTY round trip through the API, update by
re-run, cloudflared, uninstall), the same without a compiler (stops before
any download), with a broken then working git, with sudo present but not
usable, and on macOS 26 (arm64) in an isolated HOME with sudo, brew, git,
the compilers, launchctl and tailscale replaced by tripwires. Not verified
on a real standard macOS account: that account was an administrator.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Applying a custom model writes the endpoint's values over the keys it
injects, and clearing the model unset those keys. A session that had set
one of them itself lost that value: its own DSH_HOME, CODEX_HOME,
GROK_HOME or GEMINI_API_KEY was gone once the model was cleared, and so
was CLAUDE_CONFIG_DIR, which silently moved a Claude session onto the
default account. Switching endpoints did the same.
setCustomModel() now records what the session held under each injected
key and restores it on clear and on switch. Quick-start passes the
caller's values to the Session constructor so they are seen before the
selection lands.
The selection overwrites the only other copy, tmux's setenv, so the
session's own home directory has to survive a Codeman restart on its
own. __customModel therefore stores that one path, the single exception
to persisting keys only, and recovery runs it through
clampEnvOverridesForOwner() so a revoked grant cannot bring it back. Any
other own value, a credential included, stays in memory only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Playwright supports Ubuntu 26.04 from 1.61.0, so the browser suite can go
back to ubuntu-latest. Raises the floor in package.json as well, because
the old ^1.58.0 still admitted versions that refuse to install on the
image every workflow now runs on. Bundled Chromium moves from 148 to 156.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ubuntu-latest moved to 26.04 and the locked Playwright 1.60.0 refuses to
install chromium there (support starts at 1.61.0), so the nightly stopped
before running a test. Pin to the image of the last green run, so a failure
now points at our code rather than at a new OS and Chromium. Unpin when
Playwright is bumped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The probe-stall test and the removal test were written against the path-only deps. With the session argument the first failed outright and the second passed without reaching the probe. Both now use the real shape; removing the isRemoved() guard turns the second red. The route comments no longer call remote sessions unsupported.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review round 3 (all low):
- the "written last" test left solo newer than conv-1 for the rest of the
file, so the live test passed by mtime alone. solo's mtime is reset in a
finally, and the live test now runs with the live transcript older than
the stored one (a /resume into an older conversation before it writes).
- the server wiring guard matches the remote branch of the ternary as one
expression, so swapped branches fail it.
- note on whole-second `-nt` (BusyBox, bash 3.2) at the lookup.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Review round 2: with the stored conversation tried before the launch id, a
respawn that resumed the launch conversation after a /clear was missed once
claude exited again: the cleared-to conversation still had a file and won,
so the edits made after the resume were never read. No fixed order of the
ids is right both after a /clear and after such a resume. Without a live
conversation the lookup now takes the most recently written transcript of
all known ids (stored, claudeSessionId, id); the live one still goes first.
Also from the review:
- the remote no-transcript note and the docs name login profiles too
(.profile, .bash_profile): sshd's `bash -c` reads neither, like .zshrc;
- a wiring guard pins that server.ts looks a remote transcript up with
remoteTranscriptIds and leaves the local lookup on the hook path;
- the browser tests undo the injected remote session in a finally.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Review findings on the remote edit history:
- A forward read may carry 13 MiB and "Load older" 29 MiB; the flat 30 s
read timeout cut those off below ~1 MiB/s. The timeout now adds the
transfer time at 256 KiB/s (remoteEditSourcesTimeoutMs). It ends a hung
read, it does not cut a slow one.
- The transcript lookup looked only under $HOME/.claude. It now uses an
absolute CLAUDE_CONFIG_DIR of the ssh command's environment, else
$HOME/.claude, for both the projects and the live sessions/<pid>.json.
A value set only in .zshrc stays invisible (the lookup is a plain sh);
documented, and the panel's remote no-transcript note says so instead
of promising the next prompt (a remote host sends no hooks).
- After a remote /clear claudeSessionId stays the launch id, so once the
remote claude exited the lookup fell back to the cleared conversation
and the panel flipped back. The sync now hands the lookup the stored
conversation, tried right after the live one (remoteTranscriptIds).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The edit history panel now covers remote claude sessions. The sync stays
path-based; its deps get the session and server.ts routes them to the local
reader or to two new ssh reads in remote-files.ts:
- remoteLocateClaudeTranscript: one round trip; the conversation the remote
pane's claude writes now (~/.claude/sessions/<pid>.json via the pane's
process tree, since remote hosts send no hooks), else claudeSessionId,
else the session id, newest $HOME/.claude/projects/*/<id>.jsonl.
- remoteReadClaudeEditSources: one round trip per read pass, with the local
reader's semantics (offset clamp, block end at offset+len, new files read
their tail, shared main-first budget, onlyRequested, 8 KiB meta cap).
So /clear catch-up, "Load older", cursors and the store LRU work remotely
unchanged. Reads use buildRemoteFileCommand + runWithRemoteSshLimit only:
a sleeping host is a connect timeout -> unavailable, stored entries kept.
Tests: both scripts run on a real sh against a temp HOME; the read is
compared byte for byte with readClaudeEditSources on 60 seeded random
transcripts. A transitive import guard pins that the edit-history read path
reaches neither remote-wake.ts nor session-routes.ts. Browser: a remote
session lists images without thumbnails, a click previews. Docs updated.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Five routes changed the linked-cases registry: link, unlink, rename, and a
create or clone into a custom folder. Each read the file, checked it and wrote
it back on its own, so two requests that both read before either wrote lost
an entry. Two clones that claimed one name and finished together both passed
the name check, both answered success, and the later write replaced the
earlier entry. The same race made "a name linked while the clone ran is a
409" in test/routes/case-clone-custom-folder.test.ts fail about half the time.
- New src/web/linked-cases-writer.ts, mutateLinkedCases(), shaped like
mutateSettingsFile(): mutations run one at a time, each write is a temp
file renamed over the registry (through a symlink, keeping its mode), and
a file that is not a JSON object is refused (409 CONFLICT) instead of being
read as {} and overwritten.
- All five writers go through it, with their name checks inside the
callback, so exactly one of two requests for one name wins. Rename also
re-checks that the old name still points at the old folder, and drops its
manual registry undo, since a failed write no longer changes the file.
- Tests: test/linked-cases-writer.test.ts (writer behaviour, and concurrent
link/unlink through the routes with slowed writes, which fail on the old
code). Tests that forced a failed registry write with chmod 444 or a
directory now inject the failure at the rename, or expect the refusal.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds packages/desktop, a small native app (Rust: tao, wry/WKWebView, muda,
objc2) that opens Codeman in its own window. The universal app is about 5 MB
(3.2 MB as a DMG) and runs on macOS 11.3 or later.
- Connect screen with two tabs. This Mac finds the local Codeman and offers
Open, Start or Install. Start uses launchd for a LaunchAgent install and
codeman web -d otherwise. It never starts a second server while one runs,
because two servers on one data dir share live sessions. Another machine
connects by address (recent servers, Keychain logins) and can install
Codeman there over SSH.
- Installs run Codeman's install.sh in Terminal from a self-deleting
.command script. The installer needs a terminal for sudo, Homebrew and
ssh. A local install uses --local --service and the first free port from
3000, and the app opens Codeman once it answers.
- The local Codeman's login is read from its LaunchAgent or
~/.codeman/.env. It is sent only to 127.0.0.1 on that port, only while
the user's own Codeman process holds it, and is never saved.
- Native JS dialogs (alert, confirm, prompt) and HTTP Basic challenge
answers, both scoped to Codeman's own pages, so the page keeps working
after a server restart. App cookies are cleared before every connect.
- Pop-outs through window.CodemanHost, with an allowlist (webview/ never
opens a window), external links in the default browser, downloads to
~/Downloads, real full screen and window state that is remembered.
- The connect page refuses a WebKit older than Safari 16.4, which Codeman's
terminal needs.
Codeman side: viewSubagentTranscript shows a toast when the pop-up is
refused instead of failing, with zh-CN strings and a test that pins them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The gate belonged to whichever read arrived first, and sessions load their stores
independently, so under CI timing another session took it, the loop never reached
release() and the test timed out (master, CI run 41930703100). The code cannot block
one session on another: sync() only waits on its own slot.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The release note and the security doc read as if no file view could return it. The
attachment, folder and download routes refuse it; file-raw serves a session's own
workspace without the blocklist, so a session rooted at the home directory can
still read it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Consolidates the four contributor changesets, and rewords the em-dashes this release
added in comments and docs. The five in test/settings-save-failure-ui.test.ts stay:
they assert existing toast text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The startup hint is no longer typed, a Discord reply is one of the input paths that
set fromUser, edit-history is a state file, POST /api/filesystem/mkdir is in the API
reference, and the store's header says which routes really refuse it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Clone ran an unbounded realpath over every Docker case workspace, and the mkdir
route inherited the picker resolver's realpathSync, which freezes the whole server
on a mount that stopped answering. The picker's browse route has the same call and
is left as it was.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The dedicated route got this order at merge and quick-start kept the old one, so
a user could agree to a swap only to get the 400. It also runs on the pure build
now, so a refused id writes no config dir to clean up.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>