{"record":{"id":"177fa5b8d5321f63","repo":"Hmbown/CodeWhale","slug":"runtime-predecessor-cursor-does-not-match","errorCode":null,"errorMessage":"Runtime predecessor cursor does not match.","messagePattern":"Runtime predecessor cursor does not match\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"pet/scripts/lib/pet-runtime.mjs","lineNumber":64,"sourceCode":"        // while its idle body is still being read through the replacement below.\n        const body = response.body.pipeThrough(new TransformStream({ transform(chunk, controller) { refresh(); controller.enqueue(chunk); } }), { signal });\n        return new Response(body, { status: response.status, headers: response.headers });\n      };\n      try {\n        for await (const record of client.threadEvents(threadId, { sinceSeq: cursor, signal, includeProgress: true })) {\n          if (record?.event === 'stream.progress') {\n            if (record.thread_id !== threadId || record.seq !== cursor || !['live', 'replaying'].includes(record.state))\n              throw new Error('Invalid Runtime replay progress.');\n            connected = record.state === 'live';\n            continue;\n          }\n          if (!isCodewhaleRuntimeRecord(record) || record.thread_id !== threadId || !Number.isSafeInteger(record.seq) || record.seq < 0)\n            throw new Error('Invalid Runtime envelope.');\n          if (record.seq <= cursor) continue;\n          // Sequence numbers belong to Runtime, and need not be consecutive.\n          // Its predecessor cursor detects loss without inventing a new counter.\n          if (record.previous_seq !== undefined && record.previous_seq !== cursor)\n            throw new Error('Runtime predecessor cursor does not match.');\n          if (record.event !== 'item.delta') {\n            // The existing importer retains unfinished lifetimes and a recent\n            // recurrence window, not a second copy of the entire raw journal.\n            if (revision % 256 === 0) trace.prune(Date.now() - 16_000);\n            try { trace.append([redact(record)]); }\n            catch (error) { fatal = true; throw error; }\n            revision++;\n          }\n          cursor = record.seq; backoff = 250;\n        }\n      } catch (error) {\n        if ([400, 401, 403, 404, 405, 501].includes(error.status)) fatal = true;\n        if (!shutdown.signal.aborted) report(fatal\n          ? 'Runtime input stopped: check the thread, authentication, SDK/Runtime replay-progress support, or retained input limit. Recording remains unobserved.'\n          : 'Runtime input interrupted; recording unobserved gaps while reconnecting from the last cursor.');\n      } finally {\n        connected = false; clearTimeout(idleTimer); client.fetchImpl = fetchImpl;\n      }","sourceCodeStart":46,"sourceCodeEnd":82,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/pet/scripts/lib/pet-runtime.mjs#L46-L82","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Restart the follow from cursor 0 (drop sinceSeq) so the cursor tracks the journal from its true beginning.","Check the Runtime server logs for journal rotation, compaction, or multiple writers on the same thread.","If resuming from a saved cursor, re-derive the cursor from the last fully persisted record rather than an in-memory stale value.","Upgrade Runtime if the server version has a known predecessor-ordering bug."],"exampleFix":"// before: stale resumed cursor\nfollowRuntime({ threadId, sinceSeq: savedCursor });\n// after: re-derive from journal start when predecessor mismatch is suspected\nfollowRuntime({ threadId, sinceSeq: 0 }); // or validate savedCursor against the journal's first record before resuming","handlingStrategy":"validation","validationCode":"function predecessorMatches(record, cursor) {\n  return record.previous_seq === undefined || record.previous_seq === cursor;\n}\n// before resuming: verify saved cursor against the journal's first record","typeGuard":"const hasConsistentPredecessor = (r, cursor) =>\n  r.previous_seq === undefined || r.previous_seq === cursor;","tryCatchPattern":"try {\n  await followRuntime({ baseUrl, threadId, sinceSeq: savedCursor });\n} catch (err) {\n  if (err.message === 'Runtime predecessor cursor does not match.') {\n    // journal changed under us: restart from the beginning\n    await followRuntime({ baseUrl, threadId, sinceSeq: 0 });\n  } else throw err;\n}","preventionTips":["Re-derive resume cursors from the last persisted record, never from stale in-memory state.","Ensure only one writer per thread journal on the server.","Avoid resuming across journal rotation/compaction; restart from cursor 0 in that case."],"tags":["streaming","data-loss","cursor","runtime"],"backgroundTag":"checksum-mismatch","analyzedSha":"433685b2024e7bc4c99e1e2e326bcad39b4d9d65","analyzedAt":"2026-09-15T12:24:24.634Z","contentChangedAt":"2026-09-15T12:24:24.634Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}