Files
orca/src/main/runtime
Brennan Benson 9acb685816 fix(ssh): re-arm remote watches that die without a reconnect (#10470)
* fix(ssh): re-arm remote file watches when the provider reconnects

Remote file changes stopped being detected over SSH until the file was
reopened. The watch pipeline itself was fine — nothing ever re-established
the subscription after the transport it was made on went away.

Two paths left an editor tab permanently stale:

- A reconnect kills the relay's watch registrations, and the previous
  provider's unwatch handle belongs to the dead transport.
- A connect slower than the 60s retry window made installRemoteWatcher
  give up for good; first deploy to a new host far exceeds that.

Neither recovered, because installRemoteWatcher is only reachable from the
fs:watchWorktree handler, the retry timer, and the removal-restore path,
and the renderer only issues a watch for newly added targets. Reopening
the file just re-read it — the watcher stayed dead.

Give the layer that owns the transport the job of re-arming:
registerSshFilesystemProvider now notifies subscribers, which covers both
establish and reconnect since registerProviders runs on both. The watcher
keeps the intent to watch in a registry that outlives any single
connection, and on registration drops the stale entry (installRemoteWatcher
treats an existing entry as installed and would otherwise hand back a
watcher that can never fire), reinstalls, and emits overflow so consumers
resync the gap. Intent is dropped on unwatch and sender destroy so a
closed tab is never resurrected.

Verified over SSH to a Rocky Linux 10 host: after the reconnect that
previously killed it, a remote append lands in the editor in ~2s with a
live remote watcher process, and a second edit in ~1.5s.

* fix(ssh): drop watch intent when a remote worktree is removed

A removed worktree kept its entry in the intent registry, so a reconnect
landing before the renderer's unwatch would re-watch a deleted path —
60s of retries against the host and then a bogus overflow.

Also covers two reinstall cases: several senders on one connection must
collapse onto a single relay watch (and all of them resync), and a
destroyed renderer must not be reinstalled.

* fix(ssh): resync when a reconnect's watch only lands on a retry

The reinstall emitted the overflow only for listeners whose first install
returned 'installed'. If that attempt failed — relay-watcher.js still
spawning on the fresh transport, a transient fs.watch rejection — the 1s
retry restored the watch but never signalled the gap, so everything that
changed while the transport was down stayed invisible: the STA-2525
symptom reappearing inside the fix for it.

Thread the resync intent through the retry record so the overflow lands
when the retry does. All pre-existing callers default to false, so the
watch/terminal-error/restore paths are unchanged.

* test(ssh): cover the resync merge when a fresh watch claims the retry slot

The `resyncOnInstall ||=` merge was uncovered: removing it left every
existing test green. It is load-bearing — if a second renderer joins the
reinstall's failing install and reaches the retry slot first with
resync=false, the whole chain stays false and the retry restores the
watch without ever signalling the gap.

New sibling file rather than an append: filesystem-watcher.test.ts is
~10 counted lines from the 800-line lint cap, and AGENTS.md forbids a
max-lines disable.

* fix(ssh): re-arm a remote watch that died without a reconnect

The 60s fast-retry window gave up with a single overflow and no further
trigger — provider registration is the only re-arm, and a watch killed by
remote OOM/inotify exhaustion leaves the SSH link perfectly healthy. Back
off from 1min to a 30min ceiling instead, and stand down entirely when the
provider is gone so registration owns that case.

* fix(ssh): re-arm the remote file-explorer watch on reconnect

SshFilesystemProvider.dispose() stops each watch registration without
invoking its terminal callbacks, so a dropped transport left the runtime
file-explorer watch silently dead: the lease's restart only runs from the
failed-removal path, and the renderer's runtime subscription stays open
because it rides a different link. Reinstall on the connection's next
provider registration and emit overflow so clients resync.
2026-07-24 18:18:40 -07:00
..