mirror of
https://github.com/stablyai/orca.git
synced 2026-09-25 16:02:38 +00:00
* 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
97 lines
2.9 KiB
TypeScript
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)
|
|
}
|
|
}
|