paperclipai/paperclip · error
codex_history_incomplete
codex_history_incomplete
Error message
codex_history_incomplete: requested turn start is outside the retained runner event window
What it means
When reconstructing thread/items/list, the code walks committed events and requires an explicit turn.started event for the requested turn; item.completed events alone are insufficient. If the retained event window begins mid-turn (start event evicted), the transport throws rather than emit an item list of unknown completeness.
Source
Thrown at packages/paperclip-runner/src/live/runnerd-codex-transport.ts:3368
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 });
}
if (!observedStart) throw new Error("codex_history_incomplete: requested turn start is outside the retained runner event window");
data = [...items.values()];
}
if (params.sortDirection === "desc") data.reverse();
const offset = params.cursor == null ? 0 : Number(params.cursor);
if (!Number.isSafeInteger(offset) || offset < 0 || offset > data.length) throw new Error("codex_history_invalid_cursor");
const limit = typeof params.limit === "number" ? Math.max(1, Math.min(100, params.limit)) : 100;
return { data: data.slice(offset, offset + limit), nextCursor: offset + limit < data.length ? String(offset + limit) : null };
}
if (method === "thread/read") {
if (this.#core === null) {
this.#recoveryTurnBindingPending = true;
await this.#resume();
}
// Ask the authenticated runner for its live provider snapshot rather
// than reading its filesystem. This both supports remote process owners
// and proves any identity restored after PRP event compaction before the
// checkpoint-backed thread is exposed to the driver.
const snapshot = await this.#commandResult("session.snapshot", {});View on GitHub (pinned to 01ad858492)
Solutions
- List items for turns whose start event is still retained; use thread/read to discover which turns qualify.
- Increase the runner's event retention/window size if long turns must remain fully replayable.
- Recover the turn's items from durable storage (state directory) instead of the live event window.
- Re-run the turn if the items must be sourced live and the window cannot be extended.
Example fix
// before
await transport.request('thread/items/list', { threadId, turnId: evictedTurnId });
// after
const turns = (await transport.request('thread/read', { threadId, includeTurns: true })).thread.turns;
const latest = turns.at(-1).id;
await transport.request('thread/items/list', { threadId, turnId: latest }); Defensive patterns
Strategy: fallback
Validate before calling
// ensure the turn start is still observable before listing items
const read = await transport.request('thread/read', { threadId, includeTurns: true });
if (!read.thread.turns.some(t => t.id === turnId)) fallbackToDurableStore(); Try / catch
try {
data = await transport.request('thread/items/list', { threadId, turnId });
} catch (err) {
if ((err as Error).message.startsWith('codex_history_incomplete')) {
data = await loadItemsFromDurableStore(threadId, turnId);
} else throw err;
} Prevention
- Increase event-window retention for long-running turns
- Capture items incrementally as the turn progresses
- Adopt runners before their turns start so start events are retained
When it happens
Trigger: Requesting thread/items/list for the current turn whose turn.started event has already aged out of this.#core.store.state.committedEvents, or where the turn's providerTurnId/turnId in the start event doesn't match params.turnId.
Common situations: Very long turns producing many events that evict the start marker; attaching/adopting a runner mid-turn so the start event predates the retained window; replaying history after event-window trimming.
Understand the failure class
Background: EmptyResultError / "no results found": when an API or scraper succeeds but returns zero rows — this error's family across 9 libraries.
Related errors
- OpenCode runtime request is not resolvable
- run.result.proposed
- codex_history_read_failed
- codex_history_incomplete
- provider_notification_window_exceeded
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/5556a992125d58dd.
Report an issue: GitHub.