bytedance/deer-flow · error

Thread history returned an invalid next_before_seq cursor.

Error message

Thread history returned an invalid next_before_seq cursor.

What it means

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.

Source

Thrown at frontend/src/core/threads/hooks.ts:367

  for (const row of data) {
    const seq =
      typeof row === "object" && row !== null
        ? Reflect.get(row, "seq")
        : undefined;
    if (!isValidThreadMessageSeq(seq)) {
      throw new Error("Thread history returned a row with an invalid seq.");
    }
    if (seenSeqs.has(seq)) {
      throw new Error("Thread history returned duplicate seq values.");
    }
    seenSeqs.add(seq);
  }

  if (
    (hasMore && !isValidThreadMessageSeq(nextBeforeSeq)) ||
    (!hasMore && nextBeforeSeq !== null)
  ) {
    throw new Error(
      "Thread history returned an invalid next_before_seq cursor.",
    );
  }

  return value as ThreadMessagesPageResponse;
}

export function getThreadHistoryNextPageParam(
  lastPage: ThreadMessagesPageResponse,
): number | undefined {
  if (!lastPage.has_more) {
    return undefined;
  }
  if (lastPage.next_before_seq === null) {
    console.warn(
      "Thread history returned has_more without next_before_seq; pagination cannot continue.",
    );
    return undefined;

View on GitHub (pinned to 1dd6ba1acb)

Solutions

  1. Check the raw JSON for the failing page: what are has_more and next_before_seq?
  2. Fix the backend to emit next_before_seq = (has_more ? last_row_seq : null).
  3. If has_more is derived from len(rows) == limit, verify the cursor variable is set on that same branch.
  4. Add a contract test covering both terminal and non-terminal pages.

Example fix

# before: cursor always emitted
return {"data": rows, "has_more": has_more, "next_before_seq": rows[-1]["seq"]}

# after: cursor only when another page exists
return {"data": rows, "has_more": has_more, "next_before_seq": rows[-1]["seq"] if has_more else None}
Defensive patterns

Strategy: validation

Validate before calling

const okCursor = body.has_more ? Number.isInteger(body.next_before_seq) && body.next_before_seq > 0 : body.next_before_seq === null;
if (!okCursor) { /* reject page before handing it to the infinite query */ }

Type guard

function cursorConsistent(hasMore: boolean, next: unknown): boolean {
  if (hasMore) { return typeof next === "number" && Number.isInteger(next) && next > 0; }
  return next === null;
}

Try / catch

catch (e) { if (e.message.includes("next_before_seq")) { stopPaginationAndLog(threadId, e); } throw e; }

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


AI-assisted analysis of bytedance/deer-flow@1dd6ba1acb (2026-08-14). Data as JSON: /api/errors/6dd07b17b3baae3a. Report an issue: GitHub.