{"record":{"id":"e961f1091def3bc8","repo":"jackwener/OpenCLI","slug":"xiaoyuzhou-history-repeated-eid-episode-eid-th","errorCode":null,"errorMessage":"Xiaoyuzhou history repeated eid ${episode.eid}; the pagination snapshot may have changed and event-vs-overlap semantics are ambiguous","messagePattern":"Xiaoyuzhou history repeated eid (.+?); the pagination snapshot may have changed and event-vs-overlap semantics are ambiguous","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/xiaoyuzhou/history.js","lineNumber":172,"sourceCode":"    let credentials = loadXiaoyuzhouCredentials();\n    const seenEpisodeIds = new Set();\n    const seenCursors = new Set();\n    const rows = [];\n    let loadMoreKey = null;\n    let exhausted = false;\n\n    for (let pageNumber = 1; pageNumber <= maxPages; pageNumber += 1) {\n        const historyResponse = await requestXiaoyuzhouJson(HISTORY_ENDPOINT, {\n            method: 'POST',\n            body: loadMoreKey === null ? {} : { loadMoreKey },\n            credentials,\n        });\n        credentials = historyResponse.credentials;\n        const page = parseHistoryPage(historyResponse);\n        const episodes = page.entries.map((entry, index) => parseHistoryEpisode(entry, index + 1));\n        for (const episode of episodes) {\n            if (seenEpisodeIds.has(episode.eid)) {\n                throw new CommandExecutionError(\n                    `Xiaoyuzhou history repeated eid ${episode.eid}; the pagination snapshot may have changed and event-vs-overlap semantics are ambiguous`,\n                );\n            }\n            seenEpisodeIds.add(episode.eid);\n        }\n\n        const remaining = fetchAll ? episodes.length : limit - rows.length;\n        const selected = episodes.slice(0, remaining);\n        if (selected.length > 0) {\n            const progressResponse = await requestXiaoyuzhouJson(PROGRESS_ENDPOINT, {\n                method: 'POST',\n                body: { eids: selected.map((episode) => episode.eid) },\n                credentials,\n            });\n            credentials = progressResponse.credentials;\n            const progressById = parseProgressRows(progressResponse, selected);\n            for (const episode of selected) {\n                const progress = progressById.get(episode.eid);","sourceCodeStart":154,"sourceCodeEnd":190,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/xiaoyuzhou/history.js#L154-L190","documentation":"fetchHistory paginates the history feed and tracks every eid it has seen. If the same eid appears on a later page, the pagination snapshot changed between requests (new items inserted shifted entries across pages), making it ambiguous whether repeats are distinct play events or overlap artifacts. The library throws rather than guessing event-vs-overlap semantics.","triggerScenarios":"New history entries are added server-side while paginating, shifting later entries onto already-fetched positions; cursor-based pagination without snapshot isolation; repeated loadMore cursors returning overlapping data.","commonSituations":"An active account playing episodes during a long --all fetch; APIs without snapshot isolation; clock skew between client and server; very old recorded fixtures replayed with shifting data.","solutions":["Retry the full fetch (ideally with --all) during a quiet period so the server snapshot is stable between pages.","Confirm the API supports a snapshot/anchor parameter and pass it so pages come from one consistent snapshot.","If you only need recent history, use --limit instead of paginating the whole archive, reducing the window for changes.","Reduce time between page requests (avoid long delays/rate-limit backoff that widen the change window)."],"exampleFix":"// before\nawait fetchHistory({ all: true }); // long unattended run, may abort\n// after\nlet rows;\nfor (let attempt = 0; attempt < 3; attempt++) {\n  try { rows = await fetchHistory({ all: true }); break; }\n  catch (e) {\n    if (!/repeated eid/.test(e.message) || attempt === 2) throw e;\n    await new Promise(r => setTimeout(r, 2000));\n  }\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"for (let i = 0; i < 3; i++) {\n  try { return await fetchHistory({ all: true }); }\n  catch (e) {\n    if (/repeated eid/.test(e.message) && i < 2) { await new Promise(r => setTimeout(r, 2000)); continue; }\n    throw e;\n  }\n}","preventionTips":["Run full-archive fetches when the account is idle.","Prefer --limit for recent history to shorten the pagination window.","Minimize delay between page requests.","Use a snapshot/anchor parameter if the API supports one."],"tags":["pagination","consistency","api","race-condition"],"backgroundTag":"pagination-snapshot-changed","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}