paperclipai/paperclip · error · HarnessReconciliationError

thread/read returned a different driver session

Error message

thread/read returned a different driver session

What it means

readCodexThreadState calls thread/read with a threadId and verifies the returned snapshot.thread.id matches. When the provider returns a snapshot for a different thread, the library throws a HarnessReconciliationError, because reconciling against the wrong session would produce false conclusions about running work.

Source

Thrown at packages/paperclip-runner/src/drivers/codex/codex-history.ts:68

    }
    cursors.add(next);
    cursor = next;
  }
  throw new HarnessReconciliationError(
    `codex_history_incomplete: ${method} page limit exceeded`,
  );
}

export async function readCodexThreadState(
  transport: Requester,
  threadId: string,
): Promise<Record<string, unknown>> {
  const snapshot = await transport.request("thread/read", {
    threadId,
    includeTurns: false,
  });
  if (text(record(snapshot.thread).id) !== threadId)
    throw new HarnessReconciliationError(
      "thread/read returned a different driver session",
    );
  return snapshot;
}

export function readCodexTurnMetadata(
  transport: Requester,
  threadId: string,
): Promise<Record<string, unknown>[]> {
  return pages(
    transport,
    "thread/turns/list",
    { threadId, sortDirection: "asc", itemsView: "notLoaded" },
    (value) => {
      if (
        ![
          "inProgress",
          "completed",

View on GitHub (pinned to 01ad858492)

Solutions

  1. Verify the threadId passed to recovery is the live id from the current session, not a cached value from before an app-server restart.
  2. Restart recovery with a fresh thread read; if the thread no longer exists, recreate the session rather than reconciling.
  3. Check the app-server version for thread/read misbehavior on unknown thread ids and upgrade.
  4. Log both requested and returned ids to diagnose id recycling or transport muxing.

Example fix

// before
const snapshot = await readCodexThreadState(transport, staleThreadId);
// after
const currentThreadId = session.getThreadId();
const snapshot = await readCodexThreadState(transport, currentThreadId);
Defensive patterns

Strategy: validation

Validate before calling

if (!threadId || typeof threadId !== 'string') throw new Error('threadId required and must be a live session id'); if (threadId !== session.getThreadId()) throw new Error('stale threadId: session was recreated');

Type guard

function isWrongThreadSnapshot(s: unknown, threadId: string): boolean { return (s as any)?.thread?.id !== threadId; }

Try / catch

try { return await readCodexThreadState(transport, threadId); } catch (e) { if (e instanceof HarnessReconciliationError && e.message.includes('different driver session')) { await recreateSession(); return null; } throw e; }

Prevention

When it happens

Trigger: thread/read responds successfully but snapshot.thread.id !== the requested threadId — typically after a stale/closed thread id, a provider that reuses or recycles thread ids, or a transport multiplexing responses incorrectly.

Common situations: Recovery running against a restarted app-server whose thread store was reset; passing an old threadId cached from a previous session; provider bug returning a default/first thread instead of an error for unknown ids.

Understand the failure class

Background: Record Not Found Errors: "not found", RecordNotFound, and "was not found" — what they mean and how to fix them — this error's family across 28 libraries.

Related errors


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