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
- Verify the threadId passed to recovery is the live id from the current session, not a cached value from before an app-server restart.
- Restart recovery with a fresh thread read; if the thread no longer exists, recreate the session rather than reconciling.
- Check the app-server version for thread/read misbehavior on unknown thread ids and upgrade.
- 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
- Always source threadId from the live session, never a long-lived cache
- Refresh thread ids after any app-server restart
- Log requested vs returned thread ids on mismatch
- Fail recovery closed rather than reconciling against another thread
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
- thread/items/list returned a different turn
- codex_history_read_failed
- PRP recovery identity does not match the durable session bin
- device-login promotion: the account identifier cannot form a
- codex auth cache: account_id is not a valid account handle
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/5e5337ca54cd4215.
Report an issue: GitHub.