{"record":{"id":"85a2e644637bb688","repo":"jackwener/OpenCLI","slug":"twitter-collection-repeated-cursor","errorCode":"twitter_collection_repeated_cursor","errorMessage":"twitter_collection_repeated_cursor: pagination cannot prove completion","messagePattern":"twitter_collection_repeated_cursor: pagination cannot prove completion","errorType":"error_code","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/twitter/collection.js","lineNumber":289,"sourceCode":"            }\n            const createdAt = parseCreatedAt(post);\n            if (!oldestSeenAt || createdAt < oldestSeenAt) oldestSeenAt = createdAt;\n            posts.push(post);\n            if (createdAt <= until) {\n                return {\n                    posts,\n                    receipt: completedReceipt('time_boundary_reached', until, pageIndex + 1, oldestSeenAt),\n                };\n            }\n        }\n        if (!nextCursor) {\n            return {\n                posts,\n                receipt: completedReceipt('cursor_exhausted', until, pageIndex + 1, oldestSeenAt),\n            };\n        }\n        if (nextCursor === cursor || seenCursors.has(nextCursor)) {\n            throw new CommandExecutionError('twitter_collection_repeated_cursor: pagination cannot prove completion');\n        }\n        if (posts.length >= limit) {\n            throw new CommandExecutionError('twitter_collection_limit_reached: pagination cannot prove completion');\n        }\n        seenCursors.add(nextCursor);\n        cursor = nextCursor;\n    }\n    throw new CommandExecutionError('twitter_collection_page_guard_hit: pagination cannot prove completion');\n}\n\ncli({\n    site: 'twitter',\n    name: 'collection',\n    access: 'read',\n    description: 'Fetch a user timeline with relationship facts and a bounded completion receipt.',\n    domain: 'x.com',\n    strategy: Strategy.COOKIE,\n    browser: true,","sourceCodeStart":271,"sourceCodeEnd":307,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/twitter/collection.js#L271-L307","documentation":"paginateCollection tracks every cursor it has seen; if the next cursor equals the current one or was already visited, X is serving a pagination loop. Since a loop means the run could cycle forever without proving it reached 'until', the library throws this error immediately.","triggerScenarios":"UserTweets returns the same bottom_cursor on consecutive pages — cursor state expired server-side, X degraded/rate-limited responses repeating a page, or repeated entries in timeline instructions causing the extractor to reuse a stale cursor.","commonSituations":"Very old timelines where X truncates history and repeats the last cursor; rate-limited sessions receiving cached pages; X experiment modes breaking bottom_cursor advancement; scraping accounts with huge gaps causing empty cursor-advancing pages.","solutions":["Back off and retry later — cursor loops are frequently transient under rate limiting.","Refresh authentication/session; degraded sessions often get stale cursors.","Stop at the last good page and record the oldest post seen, treating further history as unavailable.","Update the library if X changed cursor encoding (cursor extraction no longer matches the new value format).","Try a fresh browser profile to bypass server-side cache stickiness."],"exampleFix":"// before\nif (nextCursor === cursor || seenCursors.has(nextCursor)) {\n  throw new CommandExecutionError('twitter_collection_repeated_cursor: ...');\n}\n// after\nif (nextCursor === cursor || seenCursors.has(nextCursor)) {\n  console.warn('cursor loop detected; returning posts collected so far');\n  return { posts, receipt: completedReceipt('cursor_loop', until, pageIndex + 1, oldestSeenAt) };\n}","handlingStrategy":"try-catch","validationCode":"if (nextCursor && seenCursors.has(nextCursor)) {\n  console.warn('cursor already seen before pagination call — cursor loop likely');\n}","typeGuard":null,"tryCatchPattern":"try {\n  await paginateCollection(...);\n} catch (err) {\n  if (String(err.message).startsWith('twitter_collection_repeated_cursor')) {\n    // keep posts collected so far; back off and retry or stop at cursor_exhausted\n  } else throw err;\n}","preventionTips":["Back off and retry — cursor loops are often transient rate-limit artifacts.","Refresh the session/browser profile before deep pagination runs.","Detect repeated cursors early in your own wrapper and stop gracefully.","Update the library if X changed cursor encoding.","Record oldestSeenAt so partial results remain usable."],"tags":["twitter","pagination","cursor","loop"],"backgroundTag":"pagination-cursor-loop","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}