(remote): push liveness over a persistent watch channel - #241
Merged
Conversation
The periodic mirror pull (floored at 60s) stays as the reconciliation path, but a live host no longer waits up to 5 minutes to show up: one ssh -tt inotifywait child per alias now pushes a coalesced signal into remoteIndexer.refreshHostNow(), the same refresh path the timer takes, narrowed to the one host that changed. -tt is required to avoid leaking an orphaned inotifywait on the remote host once the local ssh is killed. A host missing inotifywait is marked unwatchable after one attempt and left to the periodic cycle, never retried in a loop.
Every coalesced signal opens a full ssh cycle for the host. At a one-second window a continuously writing remote session would drive roughly one connection per second — 3600 handshakes an hour against a host that gets 12 today, the opposite of what the watch channel is for. The leading edge still fires at once, so an idle-to-active transition is visible immediately; sustained activity now costs at most four connections a minute.
This was referenced Sep 10, 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 #240.
A remote session's state could be five minutes stale, and lowering the poll
interval only multiplies handshakes — the OpenSSH shipped with Windows has no
ControlMaster, so every cycle is a full connect plus authentication, measuredat 416–585 ms whatever the command.
This adds a persistent push channel per host alongside the periodic cycle. The
cycle stays exactly as it was: it remains the reconciliation pass, and the only
thing covering a host the channel cannot watch.
Shape
remote-watch.js— one long-livedssh -ttchild per alias runninginotifywait, one line per filesystem event, restarting on exit with the samebackoff
remote-index.jsalready uses for host failures (hoisted and shared,not a second policy).
Events are told apart at the source:
.claude/sessionsmeans a descriptor wasrewritten,
.claude/projectsmeans a transcript grew. Paths are checked withthe transport's existing
isSafeRelPath; anything else is dropped rather thanguessed at.
remote-index.jsgainsrefreshHostNow(alias)— the narrowest possible entrypoint, guarded so an event-driven refresh and the periodic cycle can never run
on the same host at once. The signal lands in the existing notify throttle; no
second path to the renderer.
Verified against the live host, with a real session
The whole chain was run against the declared host — not only unit-tested. A
real Claude session was started on it in tmux and asked a question, with the
shipped
buildSshArgsandparseWatchLinedriving the channel:The liveness signal arrives at once instead of waiting up to five minutes.
Orphans: measured twice, zero left behind.
-ttis load-bearing — without aTTY the remote
inotifywaitsurvives the local ssh being killed, one orphan perrestart, forever. A stdin-guard wrapper was tried and still leaked. With two
backgrounded watchers behind a
wait,-ttstill takes the whole process groupdown.
Parser against real traffic: transcript and descriptor paths parse;
inotifywait's own startup chatter and any../traversal are dropped. Notethat with
-ttthe remote stderr merges into stdout, so that chatter does reachthe parser — it is dropped, by design.
One correction on top of the original commit
The coalescing window was one second. Since every coalesced signal opens a full
ssh cycle, a continuously writing session would have driven roughly one
connection per second — 3600 an hour against a host that gets 12 today, the
opposite of the point. It is now 15 s. The leading edge still fires at once, so
an idle-to-active transition is immediate; sustained activity costs at most four
connections a minute.
Degradation
inotifywaitmissing on a host is not a host failure: the channel marks thathost unwatchable, logs it once, and stops retrying. The periodic cycle already
covers it.
inotify-toolswas installed on the declared host for this work(4.23.9.0); no other host is assumed to have it.
Mutation proofs
-ttdropped; the unwatchable guard removed; the coalescing gate disabled; thepath check removed; the backoff replaced with a linear formula; the
in-flight guard removed. Each turns its own test red and nothing else. The
backoff test had to chain three exits — a single exit cannot tell exponential
from linear at the first failure.
task check: 1184 pass, 0 fail, 8 pre-existing skips, lint 0 errors.