Quick-start wrote a selection's launchModel into the session's stored
<Mode>Config, which is what a clear falls back to, so a clear relaunched
the CLI with the endpoint's model id on its native backend. Only API
callers can reach that, since no page sends a clear.
The first launch now takes the forced model from the session's selection,
through the same _withCustomModelLaunchModel() the respawn path uses, so
the stored config keeps the model chosen at create. That also carries the
model onto a new tmux session for a selection applied to a running
session, which the create path used to lose.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The button sat empty in the page while its strip was hidden, which the
mobile accessibility suite counts as a button with no name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Takes the contributor's wording for the picker title and the toast, the
preview refresh after the preflight and the clearer 409 text, and keeps
the fuller tests and the invariants paragraph from the release branch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The poll had no hidden-tab gate, and the local scan's 5 s cache sits under the
8 s interval, so a single tab never hit it: every tick was a full socket sweep
(1.1 s and 772 forks on a box with 212 sockets) for a list nobody was reading.
`_foreignPollTick()` is now the one gate for the first load, the timer and the
return to a visible tab. The `visibilitychange` listener is stored and removed
in `stopForeignPolling()`, since this runs on every trip to the home screen.
Checked in real chromium with document.hidden stubbed: 2 polls in 9.5 s visible,
0 in 17.5 s hidden, 1 on return, and a tab opened in the background makes none
until it is shown.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A subfolder whose bounded probe answered `unknown` came back as `children: []`
with no `incomplete` and `truncated` false, so a dead mount, or a healthy one
slower than the 1.5 s probe on first touch, read as an empty folder and a
search missed it in silence. `unknown` is never `absent` (#516).
Such a folder now goes on the walk's `unread` list, which flags the node and
sets `truncated` in both walks. A folder the filesystem says is gone drops out
quietly as before. The row said "file limit reached", which would be false
here, so it now reads "Not fully scanned" for either reason.
Reproduced on a real disk with a parent at mode 0444, where the child's stat
answers EACCES: the API, the search and the panel all report it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
That description is the only warning that the switch weakens Codex's
sandbox, and it had no zh-CN entry. The test runs the real translator over
the real index.html.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
105 new em-dashes sat in comments, test names and one test fixture from the
foreign tmux adoption work. Replaced by hand with commas, colons or
parentheses, one reading at a time. The fixture now quotes the real refusal
text, which says "It may still be running, try again."
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Clone Repo's picker now offers New folder, and with the field empty Browse
opens in the cases folder, so the first press there lands on this refusal.
It pointed at Add Case > Create New, which cannot clone. The sentence has to
read right from every picker that offers the button, so it names both tabs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Clone Repo's custom folder became a parent that must already exist (#774),
while its picker still had no New folder button (#761 turned it on for
Create New only). A parent that does not exist yet was refused with no way
to make it from the form, so the same job worked on one tab and dead-ended
on the other.
The test pin that kept Clone off dated from when the field was the exact
target, which could be a folder not yet created. Clone moves to the on side
of that pin, so every picker in the file stays covered.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Edit history panel told an adopted session it needed to be a Claude
session outside Docker, and a stop wait blamed the missing Claude Code hooks
on the mode. Both refusals are right, only the words misled. Each now says
Codeman did not start the session, with a zh-CN entry for the panel.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`POST /api/sessions/:id/run` had no adopted gate, so while the wrapper had no
attached PTY it spawned `claude -p --dangerously-skip-permissions <prompt>`
in /tmp, a launch every other adopted route refuses.
The route answers INVALID_INPUT, and `runPrompt()` refuses too, which also
covers the legacy `sendInput()` path and leaves no direct spawn in Session
that an adopted session can reach.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A claude session on its own CLAUDE_CONFIG_DIR launched on an endpoint with
<isolated>/projects linked to the server's tree. The resume pin looks there, so
apply found no transcript and started a new conversation, and on clear the
endpoint's transcript stayed behind in the server's tree.
The link now goes to <own dir>/projects when the session set one, so the
conversation and what the endpoint run adds stay on the session's own account
across an apply, a clear and a restart. It goes through linkRealHomeEntry(),
which also creates a missing projects folder instead of leaving a dangling
link, in the own dir and in ~/.claude/projects on the default path. A relative
CLAUDE_CONFIG_DIR of the server gets no link, since joining it would create
projects under the server's working directory.
Rule #516: guardSessionConfigDirHome() now covers configDirVar, so all three
apply sites (route, quick-start, boot recovery) probe the own dir bounded before
the apply touches it, and a dir that does not answer falls back to the server's
tree. Under test, HOME is a temp dir and CLAUDE_CONFIG_DIR is stripped by
test/setup.ts, which a new test pins.
Measured on a real Codeman server with a fake claude: apply and re-apply resume
under the isolated dir with the transcript landing in the own dir, a clear
resumes on the own dir, the isolated dir goes while the transcript stays, and the
same holds across a server restart. The documented limits come out.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The attach line now takes the rule #770 gave remote and docker launches: bashCArg() single-quotes it at both respawn-pane sites, so it reaches bash verbatim. quoteAdoptLaunch is retired, since its double quotes kept the same $ and backtick safety but lost a real newline. #510's no-cd condition stays.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The "Will clone into" line named a folder the request would not use: the
preflight replaces the page's guessed case name with the server's (for a
URL such as .../repo?tab=readme the guess is repo-tab-readme and the server
says repo) and nothing refreshed the preview, so it stayed on the old name
while the field and the request moved on. It is now refreshed there too.
The Browse picker's title and the empty-field toast still said "Choose the
folder to clone into", the old exact-target meaning. Both now ask for the
parent folder, matching the field's label.
CLAUDE.md and the invariants paragraph split the two meanings: path on the
API is the exact folder, the Clone Repo tab's field is a parent and the page
sends <folder>/<case name>.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The wiki gains right-to-left notes for Last Response, the phone's Compose
editor and the rendered Markdown view, plus a Troubleshooting entry for the
#715 report: the terminal is xterm.js, which has no right-to-left support
(xtermjs/xterm.js#701). src/web/CLAUDE.md and the invariants now state the dir
pins, the logical-property rule and that the direction pass runs last.
The changeset claimed every file path stays left to right. It now says only
what holds: the paths the viewer links on its own.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`_renderMarkdown` wrote dir="auto" into its string, before the code pins and
the linkifier. An element carrying dir="auto" re-resolves its direction on
every insertion under it, and a block whose letters all sit in `dir` children
walks its whole subtree each time, so a code-only or path-only document went
quadratic, English ones included.
Measured in real Chromium through the real File Viewer, before and after:
9000 inline-code items 8.7 s to 0.35 s, 9000 bare paths 5.6 s to 0.23 s, a
209 KB out-of-workspace list 41.9 s to 1.4 s. All three now match master.
The pass is now `_applyBlockDirection()`, run last on the DOM by both
consumers. The direction results are unchanged. Tests pin that the rendered
string holds no dir="auto" and that the pass runs after the linkifier.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
candidatesFrom had moved outside the try when the transports moved to
runForeignProbe, so a classifier failure would have reached the route as a 500.
The module promises it never throws, so the guard is back for local and docker.
Docs:
- The api reference said `blocked` waits are always refused, and listed Ralph as
refused while two of its file routes were not (they are now).
- The invariants doc and CLAUDE.md gain the rules this review turned up: target
syntax in names, the ping before list-panes, nothing typed into an adopted pane,
quoteAdoptLaunch.
- CLAUDE.md no longer says every claude session gets hooks or a stop wait.
- ForeignTmuxSession's mode and workingDir describe the first listed pane, not
the active one.
- The wiki had no page on adoption: The Dashboard gains an Opened outside Codeman
section, and the remote and docker pages point at it. docs/docker-cases.md now
separates an adopted container from an adopted tmux session.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Names tmux reads as target syntax (`@1`, `%1`, `=eq`, a bare `~`) passed the
has-session guard by name and then joined something else in new-session -t:
another session, or a fresh shell on the foreign server. They are refused at
discovery now, and take the existing skipped note.
One stopped tmux server emptied the whole local list and left a stuck client
per scan. Each server now gets a bounded ping with stdout at /dev/null first,
because a tmux client hands dups of its stdout to the server: a stopped server
holds our pipe open after the client is dead, so a watchdog around list-panes
cannot work. The probe runs in its own process group, all three transports
keep the rows read before a failure, and a missed ping becomes a note.
The attach and the probe pass -u, so a POSIX-locale container no longer garbles
the tab or lists a non-ASCII name as underscores. A docker attach gets the same
TERM and COLORTERM an owned container's exec gets.
A missing tmux no longer reads as "session is gone", and the launch line's `$`
now reaches bash unexpanded, so the ssh attach's PATH line is expanded at the
far end, as the probe's already was.
fix-plan/write, fix-plan/read and ralph-prompt/write refuse an adopted session:
its workingDir is the neutral /tmp, so they read and wrote a predictable name
in a folder anyone can write to.
FOREIGN_MAX_SOCKETS is gone: it capped nothing, and a real cap in glob order
would never reach `default` on a box with many `codeman-*` sockets (this one
has 212). Closing an adopted tab re-reads the outside-sessions list.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The cases-folder refusal grew to 143 characters, and the status line is one
line with an ellipsis and no title, so at 513px on desktop it ended at "or
make the case with Ad..." and lost the instruction. .path-picker-status.error
now wraps: 2 lines on desktop, 4 on a phone, measured clear in Chromium at
both, and it covers the picker's other refusals too.
Tests: a pin on that rule (fails without the fix), and the option check now
reads each picker's whole options object instead of a fixed 400 characters,
which stopped short of the container-workdir picker's 829. Retitled to say
what it checks: Create New has the flag too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The round-robin walk capped what it listed, not what it read: every queued
folder was read whole, sorted and held until drained, so one GET could pull
millions of entries (100 folders of 50,000 files killed a 400 MB heap). A read
now keeps only the entries the listing can use, and MAX_WALK_ENTRIES_READ stops
opening folders at 250,000 entries, leaving the rest queued and flagged.
A folder also spent its whole share on its own files, leaving nothing for the
subfolders it had just found, alone in its round or beside a sibling, so search
hits inside them were lost. Its files now count as one more share beside those
subfolders. Math.max(directoriesInRound, 2) would have fixed only the sole-folder
case and cut a wide root's folder list short.
Folders below the walk root are probed before their readdir (#516), since the
walk reaches folders the depth-first one never did.
Filtering at the read also clears the false flag on a folder whose only unread
entries were hidden or excluded, and an exactly-full workspace now finishes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the folder field empty, Create New's Browse opens in the cases folder, and New
folder there answered "Use Add Case > New Case". There is no tab of that name (the
label is Create New), and a Create New user is already on it. The refusal now reads
right from Link Existing and from Create New's custom folder. Its test pin, the code
comment, the manual, api-reference, CLAUDE.md and architecture-invariants move with it.
The block() helper in the New folder picker test returned an empty string for a name
that is not in session-ui.js, so .not.toContain passed and a renamed picker dropped out
of the check. It now asserts the name was found.
The manual's path picker list said two places and left out Create New, which the New
folder paragraph names. It now lists every Browse that opens the picker, and that
paragraph is rewrapped to the page's width.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
foreign-sessions.js joins the files the no-id test scans, so the catalog lookup
stays one. The block's rules go where a frontend reader looks, in
src/web/CLAUDE.md, and the architecture note no longer calls the file surfaces
read-only, since File Viewer edit mode writes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The two routes are public under /api/v1, so they get an API reference section.
The overlay gets an architecture note and a CLAUDE.md rule: an adopted session
has session.remote and session.docker undefined and a neutral workingDir, so any
new code that reads workingDir as a workspace must also test isAdopted. That is
the class of bug this round fixed a dozen times.
Records a measured limit: while the view exists an owner's kill-session leaves
the window alive under it until the tab closes.
Comments that still called workingDir the foreign pane's cwd now say what is
true since the wrapper got its neutral folder. Also drops a zh-CN key that
duplicated an existing one, and updates the two structure tests pinned on the
lines this round changed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex now declares a version gate too, so the notes in session.ts and
docker-hosts.ts were wrong, the hooks-config caveat described every local codex
when --no-daemon now covers 0.162 and later, and session-routes left network
access out of the launch defaults. architecture-invariants gains --no-daemon and
the network config in its list of Codex flags. Comments and docs only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The setting told users the sandbox and approvals stay on. With it on, a
command in Codex's sandbox can call Codeman's API, which can start a session
outside the sandbox with no approval prompt. Measured on codex 0.162.0 with
`codex sandbox`: a touch the sandbox refused landed through POST /api/sessions,
and from inside a Codeman pane it landed with a password set too, because a
pane inherits CODEMAN_PASSWORD. Without that password the same call gets 401.
The settings row, the wiki, the skill and the changeset now say so, the wiki
Security page and security-architecture name it, and the docs scope the claims
to what the code does: app-wide, local launches only, not cron jobs, and
--no-daemon is a local launch's flag. Agent-CLIs gains the --no-daemon
trade-off. A test pins that no surface sells the sandbox as still in charge.
Text only: the switch, the clamp and the launch are unchanged.
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>
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>
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>
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>