Repository navigation
Description
Follow-up from #11.
Problem
Today the local→remote rendezvous is a single file: ~/.local/state/nssh/session (TOML with server + topic). Every nssh <host> writes that file, every shim invocation reads it. Consequence: two nssh hostA invocations stomp each other, the last writer wins, and any earlier subscriber gets silently orphaned. #11 papered over this with a per-host pidfile registry + join/replace/new UX, but the root cause is the shared file.
What's available to piggyback on
Honest survey:
| Convention | Per-session? | ssh | mosh | tmux-survivable | Linux-only |
|---|---|---|---|---|---|
SSH_CONNECTION (sshd) |
yes (client port unique per conn) | ✓ | ⚠ stale (refers to bootstrap ssh) | ⚠ tmux strips unless update-environment lists it |
no |
SSH_TTY (sshd) |
yes | ✓ | ✗ | ⚠ tmux strips | no |
XDG_SESSION_ID (pam_systemd) |
yes | ✓ | ⚠ depends on PAM stack | ⚠ tmux strips | yes |
MOSH_KEY (mosh-server) |
yes | ✗ | ✓ | ✓ inherited | no |
loginctl list-sessions |
yes (query) | ✓ | ⚠ same | n/a | yes |
None is both portable across ssh+mosh and stable through tmux detach/reattach.
Proposal: inject our own NSSH_SESSION
- Each
nssh <host>mints a sid (generateTopic()-style random) and writes~/.local/state/nssh/sessions/<sid>.tomlon the remote with that session'sserver+topic. The legacy single-filesessionis dropped. - Launch the interactive shell with the sid in env:
- ssh:
ssh -t host 'NSSH_SESSION=<sid> exec $SHELL -l' - mosh:
mosh host -- env NSSH_SESSION=<sid> $SHELL -l
- ssh:
- Shim reads
$NSSH_SESSIONand looks up~/.local/state/nssh/sessions/$NSSH_SESSION.toml. $NSSH_SESSIONsurvives tmux because tmux inherits its initial env from the shell that started it. Firsttmux(ortmux attach) from inside the nssh-spawned shell bakes the sid into the tmux server's env permanently; every pane and window inherits it forever.
Each nssh invocation becomes a fully independent bridge. No collisions. The --join/--replace/--new flags from #11 become unnecessary (and should probably be removed once this lands, otherwise they're vestigial).
Failure mode this doesn't cover
Local nssh dies, tmux server keeps running on the remote with the dead session's NSSH_SESSION baked in. New nssh hostA gets a fresh sid; tmux attach drops you into the old tmux server whose env still references the dead sid. Shim publishes to a dead topic.
Three reasonable handlings:
- Accept it. Document: "if your local nssh dies,
tmux kill-server(ortmux source-filea script that re-exports NSSH_SESSION in panes) and start fresh." Simplest, slight rough edge. - Re-discover in the shim. If
~/.local/state/nssh/sessions/$NSSH_SESSION.tomlis missing/stale, walk the sessions dir on the remote, pick the newest. Almost always Does The Right Thing in a single-user setup. - Reclaim from new nssh. New nssh, on startup, lists remote sessions/, identifies orphans (no live local owner), and offers to take over a specific orphan by overwriting that file's content with the new topic. tmux env still references the same sid, file content swapped under it. Clean from the user's POV.
Recommendation: ship 1+2 together. Cheap. Worst case is "you tmux kill-server once". Option 3 is bigger and only worth it if 1+2 prove painful in practice.
Implementation notes
- The shim's TOML lookup is one line different — just substitute
sessionforsessions/<sid>. - prepareRemote's heredoc already writes the session file; just change the path to include the sid.
- Need to think about how
nssh statusdiscovers remote sessions: currently it reads the single file; would need to globsessions/*.tomlinstead, both locally and on the remote. - Env-injection means
ssh hostbecomesssh -t host '<cmd>', which is a behavioral change for users who relied on runningnssh host -- <plain ssh cmd>. Need to either gate this on "is it interactive", or accept the change.
Out of scope here
- The "key sessions by full SSH target identity" Codex feedback from nssh: harden session recovery #11 — once each session has its own file, that bug just goes away (no shared key to disambiguate).
Activity
nssh: harden session recovery (#11)