Skip to content

Commit c78d077

Browse files
abizerclaude
andcommitted
nssh: harden session recovery
- skip prepareRemote when joining an existing session for the host (the original process already wrote the remote session file and seeded the log; redoing it costs an SSH and adds a misleading duplicate session-open event) - record `joined: <pid>` on the local session-start event so the log distinguishes a fresh connect from a join subscriber resilience: - log subscribe-up / subscribe-down events with reconnect gap, so silent failures are visible in `nssh status --tail` - track the most-recent ntfy message id and pass it as ?since=<id> on every reconnect — anything posted while we were asleep / on a broken connection is replayed instead of dropped on the floor liveness + collision UX: - new ping / pong wire kinds and a pingTopic helper (one-shot subscribe + publish + wait for ack) to distinguish "PID is alive and answering" from "PID is alive but wedged" - on a session collision, ping the existing peer and prompt interactively: [J]oin / [R]eplace (SIGTERM + escalate to SIGKILL) / [N]ew topic / [C]ancel. Default is join when alive, replace when not. Non-interactive shells silently join with a stderr warning if the peer didn't answer. - new flags --join / --replace / --new to skip the prompt remote cleanup: - new `nssh sweep [--all|--older <dur>] <host>` subcommand that enumerates mosh-server processes owned by $USER on the remote and offers to kill them. Safe with tmux-inside-mosh: tmux server is its own daemon, so detached sessions survive. follow-ups in #10 (auto-sweep on connect, spare-yourself in --all). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 7af523b commit c78d077

6 files changed

Lines changed: 636 additions & 22 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
/nssh
22
/nssh-linux
33
.remember/
4+
/.claude/

CLAUDE.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ to `infect self`. `infect self` creates symlinks in `~/.local/bin`
4141
pointing at the running nssh binary (darwin: no-op; desktop linux:
4242
refuses without --force to avoid shadowing real xclip/xdg-open).
4343

44+
`nssh sweep <host>` lists `mosh-server` processes owned by $USER on
45+
the remote and offers to kill them. Safe when running tmux-inside-mosh:
46+
killing mosh-server doesn't kill the tmux server, so detached sessions
47+
survive. Use `--all` for unattended cleanup or `--older 168h` to keep
48+
only the last week.
49+
4450
## Architecture
4551

4652
### Dispatch on argv[0]
@@ -82,6 +88,8 @@ JSON envelopes on the ntfy topic. Every message has a `kind` field:
8288
| `clip-write` | remote → local | Write data to the Mac clipboard |
8389
| `clip-read-request` | remote → local | Request the Mac clipboard contents |
8490
| `clip-read-response` | local → remote | Response with clipboard data |
91+
| `ping` | local ↔ local | Liveness probe between two nssh processes sharing a topic |
92+
| `pong` | local ↔ local | Ack for `ping`, echoing the same correlation id |
8593

8694
Small text (≤3KB) is base64-encoded inline in the `body` field. Larger payloads
8795
and images are sent as ntfy attachments (PUT with `Filename` + `X-Message` headers).
@@ -93,6 +101,24 @@ no config required. nssh writes the server + topic to `~/.local/state/nssh/sessi
93101
on the remote before launching the shell (and seeds a `session-open` event into
94102
the JSONL log). The shim reads this file.
95103

104+
### Session collisions
105+
106+
A pidfile per live local nssh is kept at `~/.local/state/nssh/sessions/<pid>.json`.
107+
On startup, nssh looks up the host (canonical short name from `ssh -G`) in that
108+
registry. When an existing session is found nssh sends a `ping` on the topic and
109+
waits ~1.5s for a `pong`, then prompts (in an interactive shell) for one of:
110+
111+
| Choice | Effect |
112+
|--------|--------|
113+
| join | adopt the existing topic; both subscribers see every message |
114+
| replace | SIGTERM the existing PID, then SIGKILL after 1s if it's still up; fresh topic |
115+
| new | fresh topic; existing PID is left running but the remote bridge will follow the new topic |
116+
117+
Default in the prompt is `join` if the peer answered the ping, `replace` if it
118+
didn't. Non-interactive shells silently join (with a warning on the stderr if the
119+
peer was unresponsive). Override with `--join` / `--replace` / `--new` on the
120+
command line.
121+
96122
Optional `~/.config/nssh/config.toml` on either side to pin values:
97123
```toml
98124
server = "https://ntfy.example.com" # default: https://ntfy.sh

0 commit comments

Comments
 (0)