Skip to content

feat(remote): fetch CLI session descriptors on the inventory ssh connection - #224

Merged
devsuitup merged 1 commit into
mainfrom
feat/remote-session-descriptors
Sep 8, 2026
Merged

devsuitup merged 1 commit into
mainfrom
feat/remote-session-descriptors

Conversation

@devsuitup

Copy link
Copy Markdown
Owner

Closes #211.

The CLI writes one descriptor per live session to ~/.claude/sessions/<pid>.json
on 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 full
connection. They therefore ride on the single inventory connection
remote-transport.js already opens.

The command now runs the existing projects find, an SOH-framed marker line,
then a bounded read of .claude/sessions:

find .claude/projects -type f -name '*.jsonl' -printf '%T@\t%s\t%P\n';
printf '\001SWITCHBOARD-SESSIONS\001\n';
find .claude/sessions -maxdepth 1 -type f -name '[0-9]*.json' 2>/dev/null |
  LC_ALL=C sort | head -n 200 |
  while IFS= read -r f; do head -c 8192 "$f"; printf '\n'; done

That directory also holds .key files, mode 600. The -name '[0-9]*.json' glob
is 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 sessions directory yields zero descriptors and a
successful 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, not
to us — validating only pid and sessionId before accepting a record, and
never logging descriptor content on a parse failure.

listFiles() returns { files, sessions } instead of a bare array; syncMirror
and remote-index.js thread the descriptors through per host, reachable via a
new 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:

  1. No second ssh. Injecting a second run('ssh', ...) into listFiles turns
    the one-spawn test red.
  2. A .key can never be read. Widening the glob to a bare -type f turns
    the exact-pinning test on LIST_COMMAND red.
  3. A host with no sessions directory still succeeds. Making
    splitListOutput throw on a missing marker instead of degrading turns two
    tests red.

task check: 1112 tests, 1104 passed, 0 failed, 8 pre-existing skips. Lint 0
errors; the 272 warnings are pre-existing and untouched.

Note for the follow-up

status is not a heartbeat — measured on a live host, the field read idle with
a statusUpdatedAt 22 hours old, while a session just interacted with was 26
seconds old. Liveness needs pid and procStart checked against the process,
not the status alone. Recorded on #212, which owns the UI side.

…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
devsuitup force-pushed the feat/remote-session-descriptors branch from 0f127ce to c6610da Compare September 8, 2026 10:58
@devsuitup
devsuitup enabled auto-merge (squash) September 8, 2026 10:59
@devsuitup
devsuitup merged commit abf3048 into main Sep 8, 2026
9 checks passed
@devsuitup
devsuitup deleted the feat/remote-session-descriptors branch September 8, 2026 11:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(remote): read the CLI session descriptors on the inventory connection

1 participant