Files
orca/src/main/ipc/filesystem-watcher-remote-controller.ts
T
Neil 7104056984 fix(watcher): route relay watch-root capacity refusals off the fast ladder (#17950)
* fix(ssh): stop two unrecoverable relay refusal loops

A relay refusal that is a pure function of state the client cannot change was
being retried forever, on two different paths.

- pty.openClient: a superseded owner proof is refuted evidence, not a transient
  fault. The client kept re-presenting the identical proof, so every reconnect
  reproduced the same refusal until the relay was redeployed (#12895, #12931).
  It is now dropped exactly as a stale lease already is, and the claim re-asked
  without it.
- fs.watch: the relay's watch-root capacity refusal was classified 'unavailable'
  and retried at 1 Hz per root for 60s, re-armed indefinitely. A folder
  workspace with more repos than the cap turns that into a permanent install
  storm scaled by the excess root count (#11196). It is now its own 'capacity'
  result that goes straight to the existing dormant backoff, mirroring what the
  local watcher path already does.

* fix(watcher): route relay watch-root capacity refusals off the fast ladder

A full watch-root cap is a decision, not a fault, so a 1 Hz reinstall per refused
root only bills the relay the load that keeps the cap busy (#11196). Capacity
refusals now go straight to the dormant backoff.

The relay side no longer refuses on a slot it is about to hand back: an over-cap
caused by roots still unsubscribing waits once on the teardowns settling — the
release event, mirroring WatcherSupervisorCapacityWait — before it answers. A
parked waiter is excluded from the accounting so it cannot take a slot from the
root already reclaiming one.

Drops the SSH owner-recovery half of this branch. Its premise — that a -32043
SUPERSEDED refusal is permanent — is false: the refusal fires only while the
incumbent is 'active', and assertPtyConsumerOwnerRecovery explicitly admits the
identical lower-generation proof once the incumbent flips to 'disconnected'
(relay-pty-consumer-owner-displacement.test.ts proves it). The remedy could not
work either: the proofless re-ask routes into refuseHeldPtyConsumerOwner, which
is declared `: never` and, with sameClient true by construction, always throws.
It would have traded one refusal loop for another, minus the checkpoints and
minus the proof that resumes the claim once the relay reaps the incumbent.

* fix(i18n): restore the activity-options key the rebase dropped

* fix(i18n): union en.json with main so the rebase cannot drop keys
2026-09-02 15:14:21 -07:00

97 lines
2.9 KiB
TypeScript

import type { WebContents } from 'electron'
import type { RemoteWatcherInstallToken } from './filesystem-watcher-lifecycle-state'
import { watcherLifecycleState } from './filesystem-watcher-lifecycle-state'
import {
installRemoteWatcherCore,
type RemoteWatcherTerminalErrorHandler
} from './filesystem-watcher-remote-install'
import { requestRemoteWatcherResync } from './filesystem-watcher-remote-resync'
import { scheduleRemoteWatcherRetryCore } from './filesystem-watcher-remote-retry'
import { scheduleDormantRemoteWatcherRearmCore } from './filesystem-watcher-remote-dormant'
import { reinstallRemoteWatchersForConnectionCore } from './filesystem-watcher-remote-provider-rearm'
export function installRemoteWatcher(
sender: WebContents,
connectionId: string,
worktreePath: string,
generation = watcherLifecycleState.remoteWatcherLifecycleGeneration
) {
return installRemoteWatcherCore(
sender,
connectionId,
worktreePath,
handleRemoteWatcherTerminalError,
generation
)
}
export function scheduleRemoteWatcherRetry(
sender: WebContents,
connectionId: string,
worktreePath: string,
startedAt = Date.now(),
resyncOnInstall = false
): void {
scheduleRemoteWatcherRetryCore(
sender,
connectionId,
worktreePath,
{
install: installRemoteWatcher,
requestResync: requestRemoteWatcherResync,
scheduleDormant: scheduleDormantRemoteWatcherRearm
},
startedAt,
resyncOnInstall
)
}
export function scheduleDormantRemoteWatcherRearm(
connectionId: string,
worktreePath: string,
delayMs?: number
): void {
scheduleDormantRemoteWatcherRearmCore(
connectionId,
worktreePath,
{ install: installRemoteWatcher, requestResync: requestRemoteWatcherResync },
delayMs
)
}
export function reinstallRemoteWatchersForConnection(connectionId: string): void {
reinstallRemoteWatchersForConnectionCore(connectionId, {
install: installRemoteWatcher,
requestResync: requestRemoteWatcherResync,
scheduleRetry: scheduleRemoteWatcherRetry,
scheduleDormant: scheduleDormantRemoteWatcherRearm
})
}
const handleRemoteWatcherTerminalError: RemoteWatcherTerminalErrorHandler = (
key: string,
connectionId: string,
worktreePath: string,
installToken: RemoteWatcherInstallToken,
error: Error
): void => {
installToken.terminalError = error
const state = watcherLifecycleState.remoteWatchers.get(key)
if (!state || state.installToken !== installToken) {
return
}
watcherLifecycleState.remoteWatchers.delete(key)
state.batch.close()
if (
watcherLifecycleState.remoteWatchersClosed ||
watcherLifecycleState.suspendedRemoteWatcherListeners.has(key)
) {
return
}
console.warn(`[filesystem-watcher] SSH watcher terminated for ${key}:`, error)
const startedAt = Date.now()
for (const listener of state.listeners.values()) {
scheduleRemoteWatcherRetry(listener, connectionId, worktreePath, startedAt, true)
}
}