bytedance/deer-flow · error
Thread history returned duplicate seq values.
Error message
Thread history returned duplicate seq values.
What it means
The thread history parser tracks every row's seq in a Set to guarantee ordering uniqueness within one page. If two rows in the same response carry the same seq, pagination cursors would become ambiguous, so the parser throws rather than render a duplicated history.
Source
Thrown at frontend/src/core/threads/hooks.ts:358
const data = Reflect.get(value, "data");
const hasMore = Reflect.get(value, "has_more");
const nextBeforeSeq = Reflect.get(value, "next_before_seq");
if (!Array.isArray(data) || typeof hasMore !== "boolean") {
throw new Error("Thread history returned an invalid response.");
}
const seenSeqs = new Set<number>();
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,View on GitHub (pinned to 1dd6ba1acb)
Solutions
- Reproduce the failing page (threadId + before cursor) via curl and confirm which seq repeats.
- Inspect the backend history query for fan-out (add DISTINCT on the message primary key, or aggregate joined child rows into arrays).
- If a data migration produced duplicate seq values, run a repair migration that reassigns unique monotonically increasing seqs per thread.
- Add a backend test asserting unique, strictly ordered seqs per page.
Example fix
-- before: join fan-out duplicates message rows SELECT m.* FROM messages m JOIN attachments a ON a.message_id = m.id WHERE m.thread_id = :tid; -- after: dedupe on the message primary key SELECT DISTINCT ON (m.id) m.* FROM messages m LEFT JOIN attachments a ON a.message_id = m.id WHERE m.thread_id = :tid ORDER BY m.id, m.seq;
Defensive patterns
Strategy: validation
Validate before calling
const seqs = rows.map((r) => r.seq);
if (new Set(seqs).size !== seqs.length) { /* duplicate seq in page: log and request the page again / fall back to full reload */ } Type guard
function hasUniqueSeqs(rows: Array<{ seq: number }>): boolean {
return new Set(rows.map((r) => r.seq)).size === rows.length;
} Try / catch
catch (e) { if (e.message === "Thread history returned duplicate seq values.") { await queryClient.invalidateQueries({ queryKey: threadHistoryKey }); return; } throw e; } Prevention
- Use DISTINCT or aggregate joins in the history SQL to avoid fan-out duplicates.
- Enforce UNIQUE(thread_id, seq) at the DB level so duplicates cannot be written.
- Add pagination tests that exercise multi-row pages with joined child data.
When it happens
Trigger: The backend returns the same message row twice in one page — typically a JOIN fan-out in the SQL query (e.g. joining messages to attachments or roles produces one row per pair), or a UNION of overlapping ranges, or a retry/merge bug in the query builder.
Common situations: A new message-to-attachment 1:N join added to the history query without DISTINCT or proper aggregation; migration that backfills seq values with collisions; two shards merging pages with overlapping seq ranges.
Related errors
- Thread history returned a row with an invalid seq.
- Thread history returned an invalid next_before_seq cursor.
- Thread history returned an invalid response.
- reuse_thread requires thread_id
- cursor must be >= 0 and limit must be >= 1
AI-assisted analysis of bytedance/deer-flow@1dd6ba1acb (2026-08-14).
Data as JSON: /api/errors/602fb7d9d96dcf49.
Report an issue: GitHub.