feat(remote): fetch CLI session descriptors on the inventory ssh connection - #224
Merged
Merged
Conversation
devsuitup
force-pushed
the
feat/remote-session-descriptors
branch
from
September 8, 2026 10:35
ead890c to
0f127ce
Compare
…ection
The CLI writes one descriptor per live session to ~/.claude/sessions/<pid>.json
on a remote host. Fetching those required either a second ssh call per cycle
or piggybacking on the single inventory connection remote-transport.js
already opens; the latter is the only option that costs nothing extra, since
the OpenSSH client shipped on Windows has no ControlMaster to amortize a
second connection over.
LIST_COMMAND runs the existing projects find, an SOH-framed marker line, then
a bounded pull of .claude/sessions (name-filtered to [0-9]*.json, and
-type f so a symlink or a directory named like a descriptor is excluded the
same way a .key secret file is; capped at 200 files and 8 KiB each,
comfortably under the existing 8 MiB combined-output cap) — all in one
command, so listFiles() still spawns exactly one ssh. The inventory find is
followed by `|| exit $?` so its failure aborts the whole command with its own
exit status; without it, the trailing while-loop's exit status (always 0)
masked a failed inventory find as "the remote has nothing", which syncMirror
then read as license to delete every locally mirrored file for that host. The
sessions half fails independently and silently (2>/dev/null, degrading to
zero descriptors) without touching that guarantee. Proven with real `sh -c`
execution in test/remote-transport-shell.test.js, including a symlink fixture
that pins -type f specifically (a directory alone doesn't: head -c on one
writes nothing to stdout either way, so only a followed symlink actually
distinguishes the guard being present from absent).
parseSessions() preserves every field the CLI writes verbatim — the schema is
the CLI's, not ours — validating only pid and sessionId before accepting a
descriptor, and never logging descriptor content on a parse failure.
listFiles()'s contract changes from a bare inventory array to
{ files, sessions }; syncMirror() and remote-index.js thread the sessions
array through per host, exposed via a new getRemoteSessions(alias) accessor.
A cycle whose sync() throws clears that host's entry to an empty array rather
than leaving the last successful read in place, because the accessor is a
liveness signal and a false "still alive" after hours of unreachability is a
worse failure mode than a temporary empty result. No IPC, no UI, no attach —
that's issue #212.
Refs #211
devsuitup
force-pushed
the
feat/remote-session-descriptors
branch
from
September 8, 2026 10:58
0f127ce to
c6610da
Compare
devsuitup
enabled auto-merge (squash)
September 8, 2026 10:59
This was referenced Sep 8, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #211.
The CLI writes one descriptor per live session to
~/.claude/sessions/<pid>.jsonon a remote host. It carries the session id, the cwd, the status, the pid and
its start time, the messaging socket path, the bridge session id — and the name
of the multiplexer the session runs under, written by the CLI itself. That last
point is what makes remote support multiplexer-agnostic: Switchboard reads a
field instead of probing a host, and a host running no multiplexer simply has no
such field.
Fetching those descriptors had to cost nothing. The OpenSSH client shipped with
Windows has no
ControlMaster, so a second ssh call per cycle is a second fullconnection. They therefore ride on the single inventory connection
remote-transport.jsalready opens.The command now runs the existing projects
find, an SOH-framed marker line,then a bounded read of
.claude/sessions:That directory also holds
.keyfiles, mode 600. The-name '[0-9]*.json'globis what keeps them out — structurally, not by an exclusion filter someone could
forget to widen. The read is bounded at 200 files of 8 KiB, comfortably under
the existing 8 MiB output cap.
A missing or unreadable
sessionsdirectory yields zero descriptors and asuccessful cycle: the pipeline's exit status is the trailing while-loop's,
not
find's.parseSessions()keeps every field verbatim — the schema belongs to the CLI, notto us — validating only
pidandsessionIdbefore accepting a record, andnever logging descriptor content on a parse failure.
listFiles()returns{ files, sessions }instead of a bare array;syncMirrorand
remote-index.jsthread the descriptors through per host, reachable via anew
getRemoteSessions(alias).No IPC, no UI, no attach, no injection — those are #212 and beyond.
Verification
Three properties, each proven by a mutation applied with the changed line
printed before the run, the file restored from an md5-verified copy afterwards:
run('ssh', ...)intolistFilesturnsthe one-spawn test red.
.keycan never be read. Widening the glob to a bare-type fturnsthe exact-pinning test on
LIST_COMMANDred.sessionsdirectory still succeeds. MakingsplitListOutputthrow on a missing marker instead of degrading turns twotests red.
task check: 1112 tests, 1104 passed, 0 failed, 8 pre-existing skips. Lint 0errors; the 272 warnings are pre-existing and untouched.
Note for the follow-up
statusis not a heartbeat — measured on a live host, the field readidlewitha
statusUpdatedAt22 hours old, while a session just interacted with was 26seconds old. Liveness needs
pidandprocStartchecked against the process,not the status alone. Recorded on #212, which owns the UI side.