stablyai/orca · error

SSH relay providers were not ready for target

Error message

SSH relay providers were not ready for target "${targetId}".

What it means

Thrown by waitForRuntimeSshProviders when the full SSH_PROVIDER_READY_TIMEOUT_MS (10s, polled every 100ms) elapses without both getSshGitProvider(targetId) and getSshFilesystemProvider(targetId) becoming truthy. The relay accepted the connection and connectRegisteredSshTarget returned 'connected', but the provider-registration side never published both providers, so the runtime cannot route git or filesystem operations over the target. The runtime-owned target is then removed to avoid orphaning it.

Solutions

  1. Check that the SSH relay process is alive and that its handler registration path actually ran for this targetId (look for registerSshGitProvider / registerSshFilesystemProvider calls in logs).
  2. Reconnect the SSH target from the UI — a fresh connect attempt re-runs the registration handshake and usually clears a one-off race.
  3. If provider bootstrap is legitimately slow on this host, confirm whether SSH_PROVIDER_READY_TIMEOUT_MS is being exceeded because of system load (fix the load) rather than bumping the budget.
  4. Verify the SSH runtime build matches the host: a host that does not send the provider-ready frames (older firmware / relay version) will never satisfy the wait.

Example fix

// before
await connectRuntimeOwnedSshTarget({ runtimeId, connection })

// after: retry once on 'providers not ready', surface a reconnect prompt if it persists
try {
  await connectRuntimeOwnedSshTarget({ runtimeId, connection })
} catch (error) {
  if (error instanceof Error && error.message.includes('were not ready')) {
    await disconnectRuntimeOwnedSshTarget(targetId)
    throw new Error('SSH relay providers did not register in time. Reconnect the SSH target.')
  }
  throw error
}
Defensive patterns

Strategy: retry

Validate before calling

import { getSshGitProvider, getSshFilesystemProvider } from '../providers/ssh-git-dispatch'
import { getSshFilesystemProvider as getFs } from '../providers/ssh-filesystem-dispatch'

// Pre-check before relying on a connectionId: both providers must be live.
function sshProvidersReady(connectionId: string): boolean {
  return Boolean(getSshGitProvider(connectionId) && getFs(connectionId))
}

Type guard

function isSshProvidersNotReady(error: unknown): boolean {
  return error instanceof Error && error.message.startsWith('SSH relay providers were not ready for target ')
}

Try / catch

async function connectWithRetry(args, attempts = 2): Promise<RuntimeOwnedSshConnectionResult> {
  for (let i = 0; i < attempts; i++) {
    try { return await connectRuntimeOwnedSshTarget(args) }
    catch (error) {
      if (isSshProvidersNotReady(error) && i < attempts - 1) {
        await disconnectRuntimeOwnedSshTarget(undefined) // best-effort
        await new Promise((r) => setTimeout(r, 500))
        continue
      }
      throw error
    }
  }
  throw new Error('unreachable')
}

Prevention

When it happens

Trigger: connectRuntimeOwnedSshTarget connects the SSH target successfully, but the relay/handlers that should call registerSshGitProvider / registerSshFilesystemProvider for targetId are delayed, dropped, or never fire (handler not registered, IPC race, relay crash after connect). After 10s of polling, the loop at ephemeral-vm-runtime-ssh.ts:61 exits and throws at :70.

Common situations: SSH handlers are not registered (getSshConnectionStore returned a store but the provider dispatch wiring is incomplete in the running build); the relay process died after the connect handshake but before provider registration; high system load / slow first exec makes provider bootstrap exceed 10s; a version skew between client and SSH host where the host never sends the frames that trigger registration.

Related errors


AI-assisted analysis of stablyai/orca@1136503c6a (2026-08-12). Data as JSON: /api/errors/a06961f93b56724c. Report an issue: GitHub.

Appendix: source

Thrown at src/main/ephemeral-vm-runtime-ssh.ts:70

export async function removeRuntimeOwnedSshTarget(targetId: string | undefined): Promise<void> {
  if (!targetId) {
    return
  }
  await removeRegisteredSshTarget(targetId)
}

async function waitForRuntimeSshProviders(targetId: string, signal?: AbortSignal): Promise<void> {
  const startedAt = Date.now()
  while (Date.now() - startedAt < SSH_PROVIDER_READY_TIMEOUT_MS) {
    if (signal?.aborted) {
      throw new Error(`SSH provider wait aborted for target "${targetId}".`)
    }
    if (getSshGitProvider(targetId) && getSshFilesystemProvider(targetId)) {
      return
    }
    await new Promise((resolve) => setTimeout(resolve, SSH_PROVIDER_READY_INTERVAL_MS))
  }
  throw new Error(`SSH relay providers were not ready for target "${targetId}".`)
}

View on GitHub (pinned to 1136503c6a)