stablyai/orca · error · Error

Sidecar did not exit after graceful IPC close

Error message

Sidecar did not exit after graceful IPC close

What it means

verifyGracefulClose disconnects the sidecar's IPC channel and expects the sidecar process to exit within PROCESS_EXIT_TIMEOUT_MS. If it does not, the sidecar is hanging on a non-IPC reference (open handle, blocked syscall) instead of reacting to channel close — a graceful-shutdown regression. The helper-exit check (119) runs only after this passes.

Source

Thrown at config/scripts/macos-computer-helper-owner-loss-benchmark.mjs:313

    latencies.push(performance.now() - requestStartedAt)
  }
  const totalMs = performance.now() - startedAt
  return {
    totalMs,
    requestsPerSecond: (ACTIVE_REQUEST_COUNT * 1_000) / totalMs,
    medianLatencyMs: median(latencies),
    p95LatencyMs: percentile(latencies, 0.95),
    maxLatencyMs: Math.max(...latencies)
  }
}

async function verifyGracefulClose() {
  const { sidecar, helper } = await startAuthenticatedSession()
  try {
    const startedAt = performance.now()
    sidecar.child.disconnect()
    if (!(await waitForChildExit(sidecar.child, PROCESS_EXIT_TIMEOUT_MS))) {
      throw new Error('Sidecar did not exit after graceful IPC close')
    }
    const helperExitMs = await waitForProcessExit(helper, PROCESS_EXIT_TIMEOUT_MS)
    if (helperExitMs === null) {
      throw new Error('Helper did not exit after graceful owner close')
    }
    return Math.round(performance.now() - startedAt)
  } finally {
    sidecar.child.kill('SIGKILL')
    await stopProcess(helper)
  }
}

async function runInternalTrial(expectation) {
  let sidecar
  let helper
  let invalidPeer
  let invalidPeerRejected = false
  try {

View on GitHub (pinned to 1136503c6a)

Solutions

  1. Inspect the sidecar for lingering handles (node --trace-warnings / process._getActiveHandles) when run standalone.
  2. Confirm the sidecar's IPC disconnect handler still triggers exit (e.g. calls server.close/process.exit).
  3. If the helper genuinely needs more time, raise PROCESS_EXIT_TIMEOUT_MS — but only after ruling out a real hang.
  4. Check that stdio to the parent is closed/ignored so it does not keep the loop alive.

Example fix

// before
sidecar.child.disconnect()
if (!(await waitForChildExit(sidecar.child, PROCESS_EXIT_TIMEOUT_MS))) {
  throw new Error('Sidecar did not exit after graceful IPC close')
}

// after
sidecar.child.disconnect()
if (!(await waitForChildExit(sidecar.child, PROCESS_EXIT_TIMEOUT_MS))) {
  const handles = await tryGetActiveHandles(sidecar)
  throw new Error(`Sidecar did not exit after graceful IPC close; activeHandles=${JSON.stringify(handles)}`)
}
Defensive patterns

Strategy: retry

Validate before calling

const exited = await waitForChildExit(sidecar.child, PROCESS_EXIT_TIMEOUT_MS)
if (!exited) {
  throw new Error(`Sidecar still alive ${PROCESS_EXIT_TIMEOUT_MS}ms after disconnect; inspect active handles`)
}

Type guard

const hasExited = (child) => child.exitCode !== null || child.signalCode !== null

Try / catch

try {
  await verifyGracefulClose()
} catch (e) {
  if (/Sidecar did not exit/.test(e.message)) { /* SIGTERM fallback then re-check */ sidecar.child.kill('SIGTERM'); await waitForChildExit(sidecar.child, PROCESS_EXIT_TIMEOUT_MS) }
  else throw e
}

Prevention

When it happens

Trigger: The sidecar ignores the IPC 'disconnect' event, holds a keep-alive handle (timer/socket/stdio) that prevents exit, or is blocked in a synchronous operation when disconnect fires.

Common situations: A code change that added an unref'd-but-still-referenced timer, an open file/socket keeping the event loop alive, or a regression in the disconnect handler that previously called process.exit.

Related errors


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