paperclipai/paperclip · error

OpenCode request ${input.requestId} belongs to a stale turn

Error message

OpenCode request ${input.requestId} belongs to a stale turn

What it means

After finding the pending request, `resolveRuntimeRequest` verifies the caller's `turnId` matches both the turn the request was raised in (`pending.request.turnId`) and the session's currently active turn (`#activeTurnId`). A mismatch means the resolution belongs to an earlier, finished turn and is refused so answers cannot leak across turn boundaries.

Source

Thrown at packages/paperclip-runner/src/drivers/opencode/opencode-server-driver.ts:633

      structuredClone(request),
    );
  }

  async resolveRuntimeRequest(input: {
    requestId: string;
    turnId: string;
    resolution: HarnessRuntimeRequestResolution;
  }): Promise<void> {
    const pending = this.#pendingRuntimeRequests.get(input.requestId);
    if (!pending)
      throw new Error(
        `OpenCode request ${input.requestId} is no longer pending`,
      );
    if (
      pending.request.turnId !== input.turnId ||
      this.#activeTurnId !== input.turnId
    ) {
      throw new Error(
        `OpenCode request ${input.requestId} belongs to a stale turn`,
      );
    }
    const resolution = parseHarnessRuntimeRequestResolution(
      pending.request.requestKind,
      input.resolution,
      pending.request.input,
    );
    if (pending.settling)
      throw new Error(
        `OpenCode request ${input.requestId} is already settling`,
      );
    pending.settling = true;
    const submit = async (operation: Promise<unknown>) => {
      try {
        await operation;
      } catch (error) {
        if (this.#pendingRuntimeRequests.get(input.requestId) === pending)

View on GitHub (pinned to 01ad858492)

Solutions

  1. Read the authoritative turn id from `session.pendingRuntimeRequests()` (`request.turnId`) and pass exactly that value.
  2. Confirm via `await session.snapshot()` that `activeTurnId` still equals the request's turn; if not, abandon the resolution — the request is expired.
  3. On receiving this error, drop the pending answer and surface the request as expired rather than retrying with the same stale turnId.
  4. Ensure the caller's turn id comes from the same event stream instance (`events()`) that raised the request, not a different session handle.

Example fix

// before
await session.resolveRuntimeRequest({ requestId, turnId: cachedTurnId, resolution });

// after
const pending = session.pendingRuntimeRequests().find(r => r.requestId === requestId);
await session.resolveRuntimeRequest({ requestId, turnId: pending.turnId, resolution });
Defensive patterns

Strategy: validation

Validate before calling

const pending = session.pendingRuntimeRequests().find(r => r.requestId === requestId);
if (!pending || pending.turnId !== turnId) throw new StaleTurnError(requestId);

Type guard

function turnMatches(session, requestId, turnId) {
  const p = session.pendingRuntimeRequests().find(r => r.requestId === requestId);
  return !!p && p.turnId === turnId;
}

Try / catch

try {
  await session.resolveRuntimeRequest({ requestId, turnId, resolution });
} catch (e) {
  if (e.message.includes('belongs to a stale turn')) {
    // abandon the answer; surface the request as expired
  } else throw e;
}

Prevention

When it happens

Trigger: Calling `resolveRuntimeRequest` with a `turnId` different from `pending.request.turnId`, or after the active turn advanced past the one owning the request (old turn completed, new turn started, request map not yet cleaned).

Common situations: A caller caches `(requestId, turnId)` pairs and replays them after a turn restart; turn id captured from a stale event before a reconnect re-minted the active turn; mixing requests between two restored session instances.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


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