The viewer looked for a Claude transcript under ~/.claude/projects only, and
for the conversation after a /clear in ~/.claude/history.jsonl only. A session
started with its own CLAUDE_CONFIG_DIR (a second account, #255), or any session
on a server that exports one, writes both elsewhere, so the eye button showed
"No response yet" or a terminal dump, and codeman agent read and the Discord
bridge's last message came back empty. This is the third part of the report
in issue 701; the other two are a design choice and a feature request.
lastResponseFor() now asks claudeConversationDirs() for the folder the
session's claude writes to: its own override, then the server's, then
~/.claude, through claudeProjectsDir(), which the relaunch pin and the Edit
history panel already use. The rename's /resume title takes the same lookup
when no hook has named the transcript yet.
- Local sessions only. In a Docker case or on a remote host the override
names a folder over there, and the same path on this host is someone else's.
- A relocated folder is a path somebody chose, so it is probed bounded first
(the rule from issue 516). Anything but a directory reads as no transcript,
never as a reason to look under ~/.claude, where a file of the same name is
another account's conversation.
- The default folder is not probed, so that path is as before.
Not changed: the resume pre-validation at session create and the host-wide
history scans still read the default folder.
Tests: five route cases for the viewer (own folder, the server's, no fallback,
/clear through the relocated history, Docker left alone) and one for the
rename. Each part of the change fails one of them when reverted.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Conflict in skills/codeman/SKILL.md (and its plugin mirror): the list of
modes with neither stop nor blocked gained jcode here and the omp
exception from #798 on the release branch; both are kept.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The four review items of #797, applied at merge.
The editor's name rule lowercases, the data dir's .env pattern did not:
.codeman/.ENV passed both, and on a case-insensitive filesystem (the macOS
default) that is ~/.codeman/.env, the login file codeman attach reads. A
session whose workspace is the home folder could open and save it. The
pattern is case-insensitive now, pinned in the blocklist table and on the
three editor routes.
A save writes a new file and renames it over the old one, so the file took
the server's own group: a 0640 .env owned you:www-data came back you:you
and the web server lost read access. The save now puts the old user and
group back where the account may (best effort), and the wiki says mode
and group instead of permissions.
Tests: an .env in an outside worktree is refused on edit=1 and PUT too,
now that the editor's own gate lets dotenv through; the comment on why
.env.example is blocked for downloads and attachments is back.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The four edits promised in the review of #798:
- An omp alert could clear while omp's dialog was still up. omp items got
the same pane `capture` as Claude's, which the inbox re-reads with
Claude's numbered-dialog parser. omp's dialog has no numbered rows, but an
agent's plan above it does, so the list was parsed as the item's options,
and once it scrolled out of the frame the item was resolved as answered in
the terminal. An approval-only item now takes the frame once as `context`
and gets no `capture`, like a screen-read item (web/screen-blocked.ts), so
the module's `elicitation_complete` is the only thing that clears it. A
route test puts a numbered list above an omp-shaped dialog and fails on
the old code at both steps (options parsed, alert cleared).
- The reboot-restore install is dropped: the restore plan skips every CLI
whose transcript is not `claude-jsonl` (omp's is `omp-jsonl`), so it never
ran, and it sat after the start, when omp has already scanned its hooks.
- The docs that say which CLIs raise `blocked` and open alert-only items now
name omp: api-reference, CLAUDE.md, architecture-invariants, an entry for
`approvalHookFile` in cli-registry, and the skill with its plugin mirror
(verbs.md listed omp among the modes where `blocked` is a 400, one line
above the paragraph that says it works).
- Two comments: the schema's file-name rule is a pattern, not a closed list,
and the marker check takes the marker anywhere in the file.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
An installed model knew one coding agent: the one it was installed with.
The launch check never asked about the agent, only about the endpoint
and the model, so nothing stopped a second one. What was missing is the
list, and a way to get an agent that is not on the Mac yet.
The install view gains `harnesses`: every catalogue harness the model in
use can be run with, each with how it stands (`ready`, `installable`,
`installing`, `setup-needed`), the literal install command, a readiness
mark for scripts, and the actions the server accepts for it. `cli` stays
the default agent, and names one that can be started whenever one can,
so a client that reads only `cli` never starts an agent that is gone.
`POST /api/local-model/agent { cli }` installs one more agent beside an
installed model. It is not a job and not an event of the reducer: the
state stays `installed` whatever the installer does, so a failed npm
install never blocks a session on a model that works. Same install slot
and same compiled stock command as the `agent` step, one at a time, the
outcome in memory only.
The catalogue:
- `pi` is no longer gated. Its two open questions were answered by a run
against a real model (the `name:tag` id reaches the runtime unchanged;
its first request is 2,096 tokens), and Codeman now passes it the
window. Its minimum is 65536 like the others: nothing below was run,
and no model row offers less, so no Mac's pick changes.
- `deepseek` is new. It is a launcher: its install command leaves it
with nothing to launch, so it is never reported installable (read off
`discovery.launcherProfile`, not the id) and a Mac is offered it only
where it was already set up. Its tool round trip was seen on a
scripted endpoint only; the header says how to gate it again.
- a row may carry `readyMark`; `opencode` has the measured one.
A switch no longer refuses for the agent a model was checked with when
another agent that can run it is on the Mac.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The local model window could hold one model. A second install was refused
(409 already-installed), and a model a Remove kept could never be freed
from the window again. The store can now hold several models, and exactly
one of them is the model in use: the one the endpoint record lists, the
runtime is started for and a session may be launched on.
Server
- LocalModelStateFile.library lists every model that passed the check
(id, label, context, digest, size, coding agent). The service writes
the entry when verify passes; the reducer carries it and remove drops
it. An installed file of an earlier build counts its one model.
- POST /api/local-model/install is allowed from installed: it adds a
model, and the earlier one stays. A replaced job no longer deletes a
model of the library, only an unchecked or partial download.
- POST /api/local-model/use switches to a checked model: a job of the
two steps verify and connect. The stored digest is compared before
anything is loaded (model-changed otherwise). Nothing is downloaded.
- DELETE /api/local-model/models deletes one model by its name, through
the runtime's own delete, never by a path, never the model in use.
- Adding and switching answer 409 sessions-open while a live session
uses the endpoint. Until such a job ends installed, a session start is
refused with the new code job-unfinished.
- The view carries library (the state's entries joined with what the
running runtime lists: ready, changed, missing, unverified) and store
(the size of the blob folder). Both are cached and refreshed in the
background, never read on the one-second poll.
- With nothing installed, remove is offered while the store still holds
models an earlier Remove kept.
Window
- The installed screen lists the models on this Mac with the context
each was checked with, its state and its size on disk, with Use and
Delete per row (Delete asks once, in the row) and Add a model.
- In the chooser a model that is on this Mac already says so and is
switched to without a download or a confirm screen.
- A paused or failed job that has a checked model beside it shows the
way back to that model.
Every route is admin only in multi-user mode, like the existing ones.
Not run on a Mac: the tests use fakes and the fake runtime, and the
listing, store size and delete were checked against a real Ollama 0.13.0
on Linux, not the pinned 0.40.15 build for macOS.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Review follow-up. omp declared `hooks: 'always'`, which every consumer reads
as Claude's full hook set, so an omp session picked up behaviour it cannot
back. omp goes back to `hooks: 'none'`; `capabilities.approvalHookFile` is now
a separate, narrower capability with its own predicate,
`approvalHookAvailableForMode()` (local, non-adopted sessions only).
- An explicit `stop` wait on omp is a 400 again; `blocked` still works.
- omp inbox items are alert-only (`answerInTerminal`, no `approvalId` on the
push), and only `permission_prompt` / `elicitation_complete` are accepted for
an approval-only session.
- The module is installed from POST /api/quick-start too (new and existing
cases, not Docker or remote) and on reboot-restore, after the launch
pre-flight on both create paths. Claude's settings.local.json block and the
Docker CLAUDE.md no longer land in omp workspaces.
- The generated module passes Bun's `tls: { rejectUnauthorized: false }` for an
https CODEMAN_API_URL (checked against a `--https` server), clears the alert
only when the last pending toolCallId resolves, sends reports in order,
carries omp's policy `reason` as `message`, and imports HookAPI from
`@oh-my-pi/pi-coding-agent/extensibility/hooks`.
- Every path component and the target file are lstat-checked.
- docs/omp-integration.md, verbs.md and the stock.ts comments updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The plain preview already serves a workspace dotenv file in full to the same user, so refusing the edit protected nothing. The edit gate now passes allowDotenv to the sensitive-path check, which skips only the two dotenv patterns; every other credential pattern, a dotenv file inside a blocked tree and Codeman's own data-dir login file stay blocked. Downloads and attachments are unchanged.
- sync-case-state browser and stale-refresh tests use lastPickedCase (the stale test now fails without its guard)
- saveLastUsedCase runs on the case-sync chain, and the follow is skipped while this device's own pick was not stored
- _caseStateSyncOn tolerates a harness without loadAppSettingsFromStorage
- lastPickedCase added to the config-backup volatile-key test and docs; rename rollback tests cover caseSettings and lastPickedCase
- wiki row moved to System
The fixes#529's review asked for, each with a test.
- No inline handler. The button quoted the path inside
onclick="...decodeURIComponent('<path>')", and encodeURIComponent leaves
' alone, so a file named x'-alert(1)-'.txt ran script in Codeman's
origin and it's.txt was a syntax error. It is now bound in the existing
.file-tree-item loops from the row's escaped data-path.
- Symbolic links are refused, never followed. validateSessionFilePath()
returns the realpath, so deleting AGENTS.md -> CLAUDE.md deleted
CLAUDE.md and left the link dangling. The path as named is lstat'ed,
a link answers 400, and the inode is compared again before the unlink.
- The edit route's confinement whole: validateLocalSessionFilePath()
behind withGuardedOutsideWorktree(), so an absolute path in a followed
outside worktree obeys the same rules, and the sensitive, blocked and
.git checks run on both the realpath and the path as named.
- Adopted sessions are refused like remote ones (the /tmp exception of
the older file surfaces is not copied to a new one); Docker workspaces
are allowed, as on the edit route. The workspace is probed bounded
first (#516). Errors are thrown with their status.
- Deleting from search results re-runs the search, the row buttons stay
visible on touch screens, the confirm and toasts have zh-CN text, and
no button is drawn for remote or adopted sessions.
- Docs: CLAUDE.md, architecture-invariants, api-reference and the
Working With Files wiki page.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jcode (1jehuang/jcode) joins the CLI registry as the `jcode` run mode:
Run menu, phone overview, TUI, quick-start and POST /api/sessions
(`jcodeConfig: { model?, resumeSessionId? }`), local, SSH and Docker
(dedicated agent-image layer), plus codeman doctor and install.sh
through the generated catalogue. Measured on a live jcode 0.98.1 pane.
- Work detection reads jcode's braille status row and `<n>>` composer;
alt screen with mouse strip and PageUp/PageDown paging like opencode.
- Every jcode TUI is a client of one shared per-user daemon whose tools
run with the daemon's env, so no agent skill injection and no JCODE_*
envOverrides prefix.
- New `capabilities.sharedDaemon`: a session kill spares descendants
outside the pane's process group, so closing the tab that started the
daemon no longer cuts every other jcode session off (measured).
- last-response answers an honest empty for an external CLI with no
transcript reader that the pane segmenter cannot read (jcode alone).
- GET /api/jcode/status via the generic registry resolver.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
An absent `agentSkillEnabled` now means on, on the server
(`getAgentSkillEnabled()` reads `!== false`, like `workspaceHooksEnabled`)
and on the page (the App Settings switch loads `!== false`, so a Save of any
other setting no longer writes the old default back). Only an explicit false
turns it off. A partial settings PUT that omits the key leaves it as stored,
since the route merges the request into the file.
The switch's help text says it is on by default, that the copy stays out of
git, and that turning it off removes no copy already written. The shared
route-test mock now defaults to on too, so route tests see what a user sees.
Tests: quick-start against a real server (absent key injects, a partial PUT
keeps it on, an explicit false stays off through a later partial PUT), the
real openAppSettings/saveAppSettings in JSDOM (absent shows on, explicit false
shows off, a Save on a never-touched install sends true and passes the
schema), and the plain create route at the mock's default.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts resolved as on test/desktop-macbook: config/test-suites.ts keeps both browser tests (desktop-connect-page and local-model-ui), and the CLAUDE.md state-files line keeps #778's cli-installed entry with the local model's local-model.json and local-model/ entries.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`GET /api/approvals` re-reads the pane of every pending dialog and resolves
one whose frame shows no dialog, either because its options vanished or
because the frame shows a turn running. `notePrompt` deliberately waits 3 s
before making that same judgement, since the Notification hook can land
before Ink paints the dialog and the working line is still on screen then.
The list did not wait. That rarely mattered while it ran once per page load,
but a client that polls it would resolve a live prompt as answered in the
terminal, clearing the red alert on every device and the push Approve button
with it. `sweepForList` skips a dialog younger than the delay.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only the two session routes asked launchRefusalMessage(). The attach that
finds no pane, the respawn of a dead pane (a click on the tab, a respawn
start, the boot recovery), restartCli() and the reboot-restore rebuild
started an agent on the endpoint unasked. Session gains setLaunchGuard(),
asked right before a NEW agent process is started and never for an attach to
an agent that runs; WebServer.setupSessionListeners() sets it for every
session; a refusal is a LaunchRefusedError that never falls back to a direct
PTY. The reboot-restore route asks before it builds and skips the entry with
its own reason, left on offer.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The connect step records the connection in the state file BEFORE it writes
the endpoint record and the settings flag (the flag's earlier value read by
inspectLocalEndpoint()), so a state write that fails or a server killed
between the two never leaves a record of ours that the state file does not
know; a record under the reserved id that says exactly what ours would say
is ours. changePort() writes the state first and puts the old port back when
the endpoint move is refused.
A job whose end could not be written (a full disk) is kept in memory: the
view shows it, Resume and a new install start from it, the next verb writes
it, and Cancel never answers a 500. A first install on a full disk is a 507
with the sentence. "installing" in the file while this process runs no job
always reads as paused or that failure.
An installed model whose runtime does not start says so (runtime-start-failed
with Start as the next step, and the launch check says it too); the stored
runtime version is started while its folder is unpacked. Stop, Remove and
the boot of anything that is not a running installation end a runtime an
earlier server left behind before any file is deleted. Stop and "Use another
port" answer 409 sessions-open while a live session uses the model. A pull
that failed because the runtime is gone is runtime-stopped, not a network
problem. After "does not fit" only a model that needs less memory is offered.
A replaced job's partial blobs are deleted. Every 400 carries its sentence as
data.message, and the six inline sentences are labels with zh-CN entries.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
POST /api/local-model/install, cancel, resume and runtime, and DELETE
/api/local-model. The GET gains an `install` object the window polls. All of
it is experimental and admin only in multi-user mode.
One job of five steps (runtime, model, verify, agent, connect) driven by a
pure reducer over explicit state, step and transition tables. One job at a
time (409). The consent is bound to the size shown: `acknowledgedBytes` must
be the server's own total (409 size-changed). A job a server restart
interrupted comes back paused and never continues without a click. Every
failure is a code with one next step.
The model is judged against the budget the runtime measured before it is
downloaded. A missing coding agent is installed with its stock command
through the shared runner, audited. Connect writes one endpoint record under
the reserved id through mutateCustomModelHosts() and switches
customModelEndpointsEnabled on through mutateSettingsFile(), remembering the
flag's value. The model's digest is stored and read again at boot and before
every session start on that endpoint; a changed model refuses the launch.
Remove answers 409 with the session names while a live session uses the
endpoint, undoes the two writes first, restores the flag only when this
feature switched it on and no other endpoint is saved, and deletes only
inside local-model/. The coding agent stays.
The server starts the runtime at boot when a model is installed and stops
it at shutdown beside stopDeepSeekWeb().
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The four endpoint routes and the 5-minute discovery sweep each read
custom-model-hosts.json, changed their copy and wrote it back through one
shared temp file name. Two of them at once could tear the file or drop each
other's record: ten creates sent together did not all land.
Every write now goes through mutateCustomModelHosts() (one promise chain, a
random temp name, mode 0600), with each check inside the same step as its
write. A file that does not parse is refused with 409 and never replaced.
A delete of an unknown id no longer rewrites the file. A guard test fails
any other writer in src/.
The id `local-ollama` is reserved for the model Codeman runs on this Mac: a
new endpoint may not take it, and while the record is the managed one it is
not edited, deleted or re-discovered from these routes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Keep the 1.49.0 API meaning: liveWorktree and root=live stay inside the
session folder, and an outside worktree is published as outsideWorktree
and walked only with root=live-any.
Refuse a symlinked .git folder as the main worktree, sensitive files
inside a followed worktree, and any outside worktree while attachment
confinement is on or around a blocked tree. The file routes re-check
the guard settings on every request, and a throwing rule check refuses.
The panel keeps its Back to worktree button while pinned to the session
folder.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#772 made the Files panel follow an agent into a worktree inside the
session's folder. A worktree beside the repository (git worktree add
../feature, or an EnterWorktree into one that already exists there)
stayed unfollowed, since serving it needs a second confinement root.
findOutsideWorktree() answers that case without running git. The
nearest .git above the pane must name <common dir>/worktrees/<id> of the
session's own repository, and that registry entry must name the
candidate back (or the .git folder must be the common dir itself). It
refuses /, the home folder and above, any folder holding workingDir and
Docker case workspaces, and multi-user mode applies the owner's
folderAllowedForUser rule. The answer is liveWorktree with outside: true
and the folder's real path.
The file routes serve an absolute path inside that worktree, and only
while the folder still resolves to the recorded path. A relative path
still resolves against workingDir alone. The browser routes such paths
to the editable preview, and root-relative markdown refs resolve from
the top of the worktree.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The read-only route behind the recommendation, and the one module that reads
the CLI registry for it.
- harness-facts.ts: one fact per catalogue harness row, resolved in the CLI
registry at call time: the registry's label, `installed` from the existing
availability probe (asked once per build), `installable` for a stock CLI
whose install command for this platform needs no administrator rights, and
`acceptsModelId`, the test a custom model launch applies to the model id.
A row whose CLI is unknown, disabled or has no custom model mechanism gets
no fact. No CLI id is compared with a literal.
- routes/local-model-routes.ts: answers { hardware, recommendation,
catalogUpdated } in the usual envelope and reformats nothing. Admin only in
multi-user mode (403), checked before anything is probed. A failed probe is
an answer (200, the hardware's `source` says what was read); a 500 is left
for a fault in the catalogue, the harness facts or recommend(). It writes no
file and reads no setting.
Tests: test/local-model-harness-facts.test.ts,
test/routes/local-model-routes.test.ts (app.inject(), no port; the temp HOME
is byte-identical before and after the requests).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Both checks compared getTotalTokens() against the threshold. That
counter leaves out cache reads, so it stays far below the real context,
and the 500k cap ruled out thresholds on a 1M window.
POST /api/status-telemetry now hands context_window.total_input_tokens
and context_window_size to Session.noteContextReport(), which runs both
checks. The decision is a pure function, decideAutoThreshold(). The
counter stays the fallback when no report has arrived.
After a /compact or /clear the pre-command report is ignored by value,
since it stays the newest number until the command finishes. The first
differing report came later, so a swallowed command fires again instead
of locking the automation off. A counter summed from JSON usage never
drops after a compact, so it counts from its value at the last one. The
pane parser's counter is absolute and only moves past its high, so a
move clears that baseline and it fires as it did before.
Thresholds go up to the reported window, 1M when unknown, and a higher
value stops at the window. MAX_SESSION_TOKENS follows, and the pane
parser's M-suffix guard checks that cap instead of a fixed 0.5.
Refs #585
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
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>
`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>
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>
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 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>
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>
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>
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>
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>