stablyai/orca · error · Error

Expected helper to exit after abrupt authenticated owner los

Error message

Expected helper to exit after abrupt authenticated owner loss

What it means

Thrown by the owner-loss benchmark when running with --expect reaped. After SIGKILL-ing the authenticated sidecar (the helper's owner), the benchmark polls the helper for up to PROCESS_EXIT_TIMEOUT_MS (5s). If the helper is still alive after that window, the owner-loss reaping security contract is violated: a privileged helper must not outlive its authenticated owner.

Source

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

    const activeRequests = await exerciseActiveRequests(sidecar)
    const remainingHoldMs = Math.max(0, OWNER_HOLD_MS - (performance.now() - authenticatedAt))
    await sleep(remainingHoldMs)
    const connected = await sampleProcess(helper.pid)
    const invalidSocketPath = socketPathFromCommand(helper.command)
    invalidPeer = await connectInvalidPeer(invalidSocketPath)
    invalidPeerRejected = true
    const survivedClaimDeadline =
      performance.now() - authenticatedAt >= OWNER_HOLD_MS && isProcessAlive(helper.pid)

    sidecar.child.kill('SIGKILL')
    await waitForChildExit(sidecar.child, PROCESS_EXIT_TIMEOUT_MS)
    const abruptExitMs = await waitForProcessExit(
      helper,
      expectation === 'reaped' ? PROCESS_EXIT_TIMEOUT_MS : RETAIN_PROOF_MS
    )
    const helperExitedAfterAbruptLoss = abruptExitMs !== null
    if (expectation === 'reaped' && !helperExitedAfterAbruptLoss) {
      throw new Error('Expected helper to exit after abrupt authenticated owner loss')
    }
    if (expectation === 'retained' && helperExitedAfterAbruptLoss) {
      throw new Error('Expected baseline helper to remain after abrupt owner loss')
    }
    const postLossRssBytes = helperExitedAfterAbruptLoss ? 0 : processSnapshot(helper.pid).rssBytes
    await stopProcess(helper)
    helper = null
    invalidPeer.destroy()
    invalidPeer = null

    const gracefulExitMs = await verifyGracefulClose()
    return {
      authenticated: session.authenticated,
      survivedClaimDeadline,
      invalidPeerRejectedAndDidNotRetain: invalidPeerRejected && helperExitedAfterAbruptLoss,
      connectedRssBytes: connected.rssBytes,
      connectedCpuMilliseconds: Math.max(
        0,

View on GitHub (pinned to 1136503c6a)

Solutions

  1. Inspect the helper's owner-loss detection: confirm it monitors the sidecar PID and exits within 5s (check kqueue/dispatch source registration or poll interval)
  2. Verify the helper's ppid is the sidecar and it is not detached — run ps -o pid,ppid,pgid,command on the helper mid-trial
  3. Run --expect retained first to confirm setup (sidecar launch, helper spawn, authentication handshake) is correct, isolating the reaping logic from setup failures
  4. Inspect the trial's stderr.log in the launcher temp dir for unhandled helper errors that block clean shutdown

Example fix

// before: helper polls owner every 10s — misses the 5s reaping deadline
setInterval(() => { if (!isOwnerAlive(sidecarPid)) process.exit(0) }, 10_000)

// after: use macOS dispatch source / kqueue for immediate exit notification
const source = watchProcessExit(sidecarPid)
source.on('exit', () => process.exit(0))
Defensive patterns

Strategy: try-catch

Validate before calling

// Before running the benchmark, verify the helper will detect owner loss
const helper = await waitForHelper(sidecarPid)
// confirm helper is a child of the sidecar, not detached
const ps = execFileSync('ps', ['-o', 'pid,ppid,pgid,command', '-p', String(helper.pid)], { encoding: 'utf8' })
if (!ps.includes(String(sidecarPid))) {
  throw new Error('Helper is not a child of the sidecar — reaping will fail')
}

Try / catch

try {
  const result = await runInternalTrial('reaped')
} catch (error) {
  if (error.message.includes('Expected helper to exit')) {
    // owner-loss reaping failed — inspect helper logs, check watchdog implementation
    console.error('Owner-loss reaping contract violated:', error)
  }
  throw error
}

Prevention

When it happens

Trigger: Called from runInternalTrial('reaped') at line 354. The sidecar is killed via sidecar.child.kill('SIGKILL'), then waitForProcessExit(helper, 5000) returns null because processIdentityIsCurrent still matches after 5s of 50ms polling. helperExitedAfterAbruptLoss is false, so the assertion fires.

Common situations: The helper's owner-death watchdog (kqueue/pidfd/poll on the sidecar PID) is broken or its poll interval exceeds the 5s window; the helper was spawned detached or re-parented to launchd so it never observes the sidecar exit; the helper's process group differs from the sidecar's so SIGKILL does not cascade; a regression in the helper lifecycle manager prevents self-termination.

Understand the failure class

Related errors


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