paperclipai/paperclip · error · Error

runner_prp_command_timeout:${command.commandId}

Error message

runner_prp_command_timeout:${command.commandId}

What it means

queueLiveRunnerPrpCommand queues a control command on the live durable PRP (runner process) authority for a run and polls for its outcome. If the command has not reached a terminal status (completed/failed/rejected) within a hard-coded 30-second deadline, the completion promise rejects with `runner_prp_command_timeout:<commandId>`. This is a deliberate fail-loud signal that the connected runner never acknowledged or finished the command in time.

Source

Thrown at server/src/realtime/runner-prp-ws.ts:223

      while (Date.now() < deadline) {
        const outcome = binding.authority.commandOutcome(command.commandId);
        if (!outcome) {
          throw new Error(`runner_prp_command_missing:${command.commandId}`);
        }
        if (outcome.status === "completed") return outcome.result;
        if (outcome.status === "failed" || outcome.status === "rejected") {
          const message =
            outcome.result && typeof outcome.result.message === "string"
              ? outcome.result.message
              : `runner_prp_command_${outcome.status}:${command.commandId}`;
          throw new Error(message);
        }
        await new Promise<void>((resolve) => {
          const timer = setTimeout(resolve, 10);
          timer.unref();
        });
      }
      throw new Error(`runner_prp_command_timeout:${command.commandId}`);
    })(),
  };
}

export class RunnerPrpRuntimeRequestResolutionError extends Error {
  constructor(
    readonly code:
      | "runner_prp_authority_not_active"
      | "runtime_request_resolution_conflict",
  ) {
    super(code);
    this.name = "RunnerPrpRuntimeRequestResolutionError";
  }
}

/**
 * Queue one turn-bound runtime response on the active durable PRP authority.
 * Identical browser retries reuse the original command; a different answer for

View on GitHub (pinned to 01ad858492)

Solutions

  1. Retry the command with a fresh commandId after confirming the runner is still connected (re-queue; the old commandId is abandoned on timeout).
  2. Check the runner process health/logs for the run (runId from queueLiveRunnerPrpCommand's return) to see whether the command was received at all.
  3. Verify the runner's WS connection to the CONNECT_PATH_PREFIX endpoint is alive; a dead socket means outcomes are never delivered — restart or reconnect the runner.
  4. If the command is inherently long (e.g. a full turn), increase the 30_000ms deadline in server/src/realtime/runner-prp-ws.ts:204 or make the deadline command-type dependent.

Example fix

// before
const res = queueLiveRunnerPrpCommand({ companyId, issueId, agentId, type: 'steer', payload });
await res.completion; // throws runner_prp_command_timeout after 30s
// after
const res = queueLiveRunnerPrpCommand({ companyId, issueId, agentId, type: 'steer', payload });
try {
  await res.completion;
} catch (e) {
  if (e instanceof Error && e.message.startsWith('runner_prp_command_timeout:')) {
    // runner did not finish in 30s: surface to user, optionally re-queue
    throw new Error(`Steer command timed out; runner may be busy (run ${res.runId})`);
  }
  throw e;
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Pre-check that a live authority exists before queueing
const hasLive = currentLiveAuthorities.has(liveAuthorityKey({ companyId, issueId, agentId }));
if (!hasLive) throw new Error('No live runner authority; command would be a no-op or rely on fallback');

Type guard

function isPrpCommandTimeout(e: unknown): e is Error & { commandId: string } {
  const m = e instanceof Error && /^runner_prp_command_timeout:(.+)$/.exec(e.message);
  if (m) (e as any).commandId = m[1];
  return !!m;
}

Try / catch

try {
  await queueLiveRunnerPrpCommand({...}).completion;
} catch (e) {
  if (isPrpCommandTimeout(e)) {
    logger.warn({ commandId: e.commandId }, 'runner PRP command timed out; runner may be stalled');
    // surface to caller / offer retry
  } else throw e;
}

Prevention

When it happens

Trigger: Calling steer() or nativeControl() on an issue/agent whose runner holds the live PRP authority registration, but the runner process is stalled, disconnected mid-command, blocked on a long synchronous turn, or slow to write the command outcome back to the authority; the outcome record exists (never becomes runner_prp_command_missing) but stays pending past 30s.

Common situations: Runner agent wedged in a long-running tool call that ignores control messages; WS connection dropped but registration not yet released; heavily loaded runner host delaying the 10ms-poll loop's observed outcome write; steering during a model stream that never yields.

Understand the failure class

Background: Request timed out: what client-side request timeouts mean across libraries (Request timed out, TIMED_OUT, APITimeoutError) — this error's family across 39 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10). Data as JSON: /api/errors/eaca578f36ba7fda. Report an issue: GitHub.