Hmbown/CodeWhale · error · Error

Runtime predecessor cursor does not match.

Error message

Runtime predecessor cursor does not match.

What it means

Runtime records optionally carry previous_seq, the cursor position of the immediately preceding record. If a record declares a previous_seq that doesn't equal the client's current cursor, a journal record was lost or skipped between the cursor and this record (sequence gaps are allowed, but the declared predecessor must match). followRuntime treats this as a data-loss violation and aborts.

Solutions

  1. Restart the follow from cursor 0 (drop sinceSeq) so the cursor tracks the journal from its true beginning.
  2. Check the Runtime server logs for journal rotation, compaction, or multiple writers on the same thread.
  3. If resuming from a saved cursor, re-derive the cursor from the last fully persisted record rather than an in-memory stale value.
  4. Upgrade Runtime if the server version has a known predecessor-ordering bug.

Example fix

// before: stale resumed cursor
followRuntime({ threadId, sinceSeq: savedCursor });
// after: re-derive from journal start when predecessor mismatch is suspected
followRuntime({ threadId, sinceSeq: 0 }); // or validate savedCursor against the journal's first record before resuming
Defensive patterns

Strategy: validation

Validate before calling

function predecessorMatches(record, cursor) {
  return record.previous_seq === undefined || record.previous_seq === cursor;
}
// before resuming: verify saved cursor against the journal's first record

Type guard

const hasConsistentPredecessor = (r, cursor) =>
  r.previous_seq === undefined || r.previous_seq === cursor;

Try / catch

try {
  await followRuntime({ baseUrl, threadId, sinceSeq: savedCursor });
} catch (err) {
  if (err.message === 'Runtime predecessor cursor does not match.') {
    // journal changed under us: restart from the beginning
    await followRuntime({ baseUrl, threadId, sinceSeq: 0 });
  } else throw err;
}

Prevention

When it happens

Trigger: A record arrives with record.previous_seq defined and !== cursor — e.g. the server skipped or compacted records since the client's sinceSeq position, the client resumed with a stale/partial cursor, or concurrent writers interleaved records.

Common situations: Resuming a follow with an old cursor against a journal that has been rotated/truncated; a server bug emitting records out of predecessor order; multiple Runtime processes writing the same thread journal concurrently; replay starting mid-stream after records were pruned.

Understand the failure class

Background: Checksum mismatch errors: "checksum verification failed", "digest mismatch", "expected vs actual checksum" — what they mean and how to fix them — this error's family across 41 libraries.

Related errors


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

Appendix: source

Thrown at pet/scripts/lib/pet-runtime.mjs:64

        // while its idle body is still being read through the replacement below.
        const body = response.body.pipeThrough(new TransformStream({ transform(chunk, controller) { refresh(); controller.enqueue(chunk); } }), { signal });
        return new Response(body, { status: response.status, headers: response.headers });
      };
      try {
        for await (const record of client.threadEvents(threadId, { sinceSeq: cursor, signal, includeProgress: true })) {
          if (record?.event === 'stream.progress') {
            if (record.thread_id !== threadId || record.seq !== cursor || !['live', 'replaying'].includes(record.state))
              throw new Error('Invalid Runtime replay progress.');
            connected = record.state === 'live';
            continue;
          }
          if (!isCodewhaleRuntimeRecord(record) || record.thread_id !== threadId || !Number.isSafeInteger(record.seq) || record.seq < 0)
            throw new Error('Invalid Runtime envelope.');
          if (record.seq <= cursor) continue;
          // Sequence numbers belong to Runtime, and need not be consecutive.
          // Its predecessor cursor detects loss without inventing a new counter.
          if (record.previous_seq !== undefined && record.previous_seq !== cursor)
            throw new Error('Runtime predecessor cursor does not match.');
          if (record.event !== 'item.delta') {
            // The existing importer retains unfinished lifetimes and a recent
            // recurrence window, not a second copy of the entire raw journal.
            if (revision % 256 === 0) trace.prune(Date.now() - 16_000);
            try { trace.append([redact(record)]); }
            catch (error) { fatal = true; throw error; }
            revision++;
          }
          cursor = record.seq; backoff = 250;
        }
      } catch (error) {
        if ([400, 401, 403, 404, 405, 501].includes(error.status)) fatal = true;
        if (!shutdown.signal.aborted) report(fatal
          ? 'Runtime input stopped: check the thread, authentication, SDK/Runtime replay-progress support, or retained input limit. Recording remains unobserved.'
          : 'Runtime input interrupted; recording unobserved gaps while reconnecting from the last cursor.');
      } finally {
        connected = false; clearTimeout(idleTimer); client.fetchImpl = fetchImpl;
      }

View on GitHub (pinned to 433685b202)