paperclipai/paperclip · error

codex_history_identity_mismatch

codex_history_identity_mismatch

Error message

codex_history_identity_mismatch

What it means

The thread/turns/list and thread/items/list handlers only serve history for the thread this transport instance is currently bound to (#threadId). Requesting history for any other threadId throws, since the retained runner event window belongs to a different thread.

Source

Thrown at packages/paperclip-runner/src/live/runnerd-codex-transport.ts:3349

          : undefined;
      await this.#command(
        "turn.steer",
        {
          text,
          turnId: this.#durableTurnId,
          providerTurnId: expectedTurnId,
          ...(correlationId ? { correlationId } : {}),
        },
        correlationId,
      );
      return {};
    }
    if (method === "turn/interrupt") {
      await this.#command("turn.interrupt", params);
      return {};
    }
    if (method === "thread/turns/list" || method === "thread/items/list") {
      if (params.threadId !== this.#threadId) throw new Error("codex_history_identity_mismatch");
      const snapshot = await this.request("thread/read", { threadId: this.#threadId, includeTurns: false });
      const turns = record(snapshot.thread).turns as Array<Record<string, unknown>>;
      let data: Array<Record<string, unknown>>;
      if (method === "thread/turns/list") {
        data = turns.map(turn => ({ ...turn, items: [], itemsView: "notLoaded" }));
      } else {
        if (params.turnId !== this.#turnId) throw new Error("codex_history_unavailable: requested turn is outside the retained runner event window");
        const items = new Map<string, Record<string, unknown>>();
        let observedTurn = "";
        let observedStart = false;
        for (const event of this.#core?.store.state.committedEvents ?? []) {
          const payload = record(record(event.envelope.payload).payload);
          if (event.eventType === "turn.started") observedTurn = String(payload.providerTurnId ?? payload.turnId ?? record(payload.turn).id ?? "");
          if (event.eventType === "turn.started" && observedTurn === params.turnId) observedStart = true;
          if (event.eventType !== "item.completed" || observedTurn !== params.turnId) continue;
          const item = record(rehydrateRunnerdItemNotification(payload, this.#threadId, observedTurn).item);
          if (typeof item.id === "string") items.set(item.id, { turnId: observedTurn, item });
        }

View on GitHub (pinned to 01ad858492)

Solutions

  1. Read the current threadId (e.g. via thread/read or transport state) and request history for that id.
  2. Create a new transport/session bound to the desired thread before listing its turns/items.
  3. Fix the client to use the threadId returned from thread/start rather than a cached value.
  4. Normalize thread ids before comparison if formatting differs.

Example fix

// before
await transport.request('thread/turns/list', { threadId: oldThreadId });
// after
const { thread } = await transport.request('thread/read', { threadId: transport.currentThreadId });
await transport.request('thread/turns/list', { threadId: transport.currentThreadId });
Defensive patterns

Strategy: validation

Validate before calling

const current = transport.currentThreadId ?? (await transport.request('thread/read', {})).thread?.id;
if (threadId !== current) throw new Error(`transport bound to ${current}, not ${threadId}`);

Try / catch

try {
  await transport.request('thread/items/list', { threadId, turnId });
} catch (err) {
  if ((err as Error).message === 'codex_history_identity_mismatch') {
    threadId = await resolveBoundThreadId(transport);
    await transport.request('thread/items/list', { threadId, turnId });
  } else throw err;
}

Prevention

When it happens

Trigger: Calling request('thread/turns/list' | 'thread/items/list', { threadId: X }) where X !== this.#threadId — e.g. a stale client handle, a thread id from a previous session, or a mismatched id format.

Common situations: Client cache holding an old threadId after reconnect; reading another company/thread's history through the same transport; id normalization differences (prefix vs full id); adopting a runner whose bound thread differs from the client's expectation.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


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