Hmbown/CodeWhale · error · Error

Runtime import contains multiple threads. Export one thread…

Error message

Runtime import contains multiple threads. Export one thread before importing.

What it means

Codewhale runtime event files (NDJSON records) are expected to contain events for exactly one conversation thread. During import, each record's thread_id is compared against the thread established by the first record, and any mismatch aborts the import. The library refuses a partial or misleading import rather than silently merging threads.

Solutions

  1. Export exactly one thread from the runtime and re-import that single file.
  2. Split the input by thread_id (e.g. jq -s or a line-by-line filter grouping on .thread_id) and import each group separately.
  3. Find the offending line(s) whose thread_id differs from the first record and remove or route them to their own file.
  4. Check whether upstream logging or file rotation is merging thread streams and fix the exporter.

Example fix

// before
cat thread-a.jsonl thread-b.jsonl > combined.jsonl
importTrace(fromCodewhaleRuntime(readJsonl('combined.jsonl')))
// after
importTrace(fromCodewhaleRuntime(readJsonl('thread-a.jsonl'), 'thread-a'))
importTrace(fromCodewhaleRuntime(readJsonl('thread-b.jsonl'), 'thread-b'))
Defensive patterns

Strategy: validation

Validate before calling

function isSingleThread(records) {
  const ids = new Set(records.filter(r => r && typeof r === 'object').map(r => r.thread_id));
  return ids.size <= 1;
}
if (!isSingleThread(records)) throw new Error('input spans multiple thread_id values; split before import');

Type guard

const isRuntimeRecord = (r) => !!r && typeof r === 'object' && typeof (r).thread_id === 'string' && typeof (r).event === 'string';

Try / catch

try {
  const trace = fromCodewhaleRuntime(records, filename);
} catch (e) {
  if (e.message.includes('multiple threads')) {
    const byThread = new Map();
    for (const r of records) byThread.set(r.thread_id, [...(byThread.get(r.thread_id) ?? []), r]);
    for (const [tid, recs] of byThread) importTrace(fromCodewhaleRuntime(recs, tid));
  } else throw e;
}

Prevention

When it happens

Trigger: Calling fromCodewhaleRuntime (or CodewhaleRuntimeTrace.append) with a records array or file that concatenates events from two or more thread_ids; typically a hand-merged export or a log aggregator that combined multiple sessions into one file.

Common situations: Concatenating several exported .jsonl session files with cat; a log shipper writing multiple threads to one file; exporting a whole session store instead of a single thread; a tool that rotates files mid-thread so imports accidentally span files.

Understand the failure class

Background: "is not a compatible type" / "cannot merge" errors: when a value's type doesn't match what the library requires — this error's family across 65 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15). Data as JSON: /api/errors/dd034d566dcc62da. Report an issue: GitHub.

Appendix: source

Thrown at pet/src/core/codewhale.ts:392

  }
  append(records: unknown[]): void {
    if (!records.length) return;
    const { events, open, requests } = this;
    const threadId = this.threadId ?? str(obj(records[0]).thread_id) ?? this.filename;
    this.threadId = threadId;
    let { origin, model, skippedDeltas } = this;
    let threadName = this.threadName ?? threadId;
    const stamp = (rec: Obj): number => {
      const t = parseTime(rec.timestamp);
      if (t === undefined) throw new Error(`Runtime event seq ${rec.seq} is missing a usable timestamp.`);
      if (origin === undefined) origin = t;
      return t - origin;
    };
    for (const raw of records) {
      if (!isCodewhaleRuntimeRecord(raw)) throw new Error('Runtime import cancelled: a line is not a Codewhale runtime event record. No rows were skipped.');
      this.recordCount++;
      const rec = obj(raw);
      if (rec.thread_id !== threadId) throw new Error('Runtime import contains multiple threads. Export one thread before importing.');
      const eventName = rec.event as string;
      if (eventName === 'item.delta') { skippedDeltas++; continue; }
      const payload = obj(rec.payload);
      const item = obj(payload.item);
      const turn = obj(payload.turn);
      const thread = obj(payload.thread);
      const relative = stamp(rec);
      const turnId = str(rec.turn_id) ?? str(payload.turn_id);
      const itemId = str(rec.item_id) ?? str(item.id);
      const agentId = 'parent';
      if (str(thread.model)) model = str(thread.model);
      if (str(turn.model)) model = str(turn.model) ?? model;

      if (eventName === 'thread.started') {
        model = str(thread.model) ?? model;
        threadName = str(thread.id) ?? threadId;
        this.push({
          schemaVersion: 1, id: `thread:${threadId}`, traceId: threadId,

View on GitHub (pinned to 433685b202)