{"record":{"id":"6dd07b17b3baae3a","repo":"bytedance/deer-flow","slug":"thread-history-returned-an-invalid-next-before-seq","errorCode":null,"errorMessage":"Thread history returned an invalid next_before_seq cursor.","messagePattern":"Thread history returned an invalid next_before_seq cursor\\.","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"frontend/src/core/threads/hooks.ts","lineNumber":367,"sourceCode":"  for (const row of data) {\n    const seq =\n      typeof row === \"object\" && row !== null\n        ? Reflect.get(row, \"seq\")\n        : undefined;\n    if (!isValidThreadMessageSeq(seq)) {\n      throw new Error(\"Thread history returned a row with an invalid seq.\");\n    }\n    if (seenSeqs.has(seq)) {\n      throw new Error(\"Thread history returned duplicate seq values.\");\n    }\n    seenSeqs.add(seq);\n  }\n\n  if (\n    (hasMore && !isValidThreadMessageSeq(nextBeforeSeq)) ||\n    (!hasMore && nextBeforeSeq !== null)\n  ) {\n    throw new Error(\n      \"Thread history returned an invalid next_before_seq cursor.\",\n    );\n  }\n\n  return value as ThreadMessagesPageResponse;\n}\n\nexport function getThreadHistoryNextPageParam(\n  lastPage: ThreadMessagesPageResponse,\n): number | undefined {\n  if (!lastPage.has_more) {\n    return undefined;\n  }\n  if (lastPage.next_before_seq === null) {\n    console.warn(\n      \"Thread history returned has_more without next_before_seq; pagination cannot continue.\",\n    );\n    return undefined;","sourceCodeStart":349,"sourceCodeEnd":385,"githubUrl":"https://github.com/bytedance/deer-flow/blob/1dd6ba1acb03700589994b0366c5d1c7d05e2eff/frontend/src/core/threads/hooks.ts#L349-L385","documentation":"Final contract check in parseThreadMessagesPageResponse: the 'next_before_seq' cursor must be internally consistent with 'has_more'. If has_more is true, next_before_seq must be a valid seq (so the next page can be fetched); if has_more is false, it must be exactly null. Any other combination throws.","triggerScenarios":"Backend sends has_more=true with next_before_seq null or 0/invalid (e.g. it computed 'more pages' but never derived the cursor), or sends has_more=false with a leftover integer cursor (e.g. cursor computed unconditionally from the last row).","commonSituations":"Off-by-one pagination logic after a limit change, a serializer that always emits the last row's seq regardless of has_more, or a backend refactor that dropped cursor computation but kept the has_more heuristic.","solutions":["Check the raw JSON for the failing page: what are has_more and next_before_seq?","Fix the backend to emit next_before_seq = (has_more ? last_row_seq : null).","If has_more is derived from len(rows) == limit, verify the cursor variable is set on that same branch.","Add a contract test covering both terminal and non-terminal pages."],"exampleFix":"# before: cursor always emitted\nreturn {\"data\": rows, \"has_more\": has_more, \"next_before_seq\": rows[-1][\"seq\"]}\n\n# after: cursor only when another page exists\nreturn {\"data\": rows, \"has_more\": has_more, \"next_before_seq\": rows[-1][\"seq\"] if has_more else None}","handlingStrategy":"validation","validationCode":"const okCursor = body.has_more ? Number.isInteger(body.next_before_seq) && body.next_before_seq > 0 : body.next_before_seq === null;\nif (!okCursor) { /* reject page before handing it to the infinite query */ }","typeGuard":"function cursorConsistent(hasMore: boolean, next: unknown): boolean {\n  if (hasMore) { return typeof next === \"number\" && Number.isInteger(next) && next > 0; }\n  return next === null;\n}","tryCatchPattern":"catch (e) { if (e.message.includes(\"next_before_seq\")) { stopPaginationAndLog(threadId, e); } throw e; }","preventionTips":["Backend: derive next_before_seq and has_more in one code path so they cannot disagree.","Contract-test both terminal and non-terminal pages in CI.","Treat cursor/has_more divergence as a paging bug, never silently clamp it client-side."],"tags":["validation","pagination","threads","contract-mismatch"],"backgroundTag":null,"analyzedSha":"1dd6ba1acb03700589994b0366c5d1c7d05e2eff","analyzedAt":"2026-08-14T21:20:34.804Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}