Skip to content

fix(chat): page agent history by compound cursor, not bare timestamp - #785

Merged
1 commit merged into
dataelement:mainfrom
liqingb0220-stack:fix/chat-history-pagination-cursor
Jul 24, 2026
Merged

fix(chat): page agent history by compound cursor, not bare timestamp#785
1 commit merged into
dataelement:mainfrom
liqingb0220-stack:fix/chat-history-pagination-cursor

Conversation

@liqingb0220-stack

Copy link
Copy Markdown
Contributor

Problem

In the agent 1:1 chat view, when a conversation contains a burst of messages that were persisted with an identical created_at (typically a batch of tool_call rows written in one turn) and that batch is larger than HISTORY_PAGE_SIZE (20), scrolling up never loads older messages. The tool count keeps inflating on every scroll (e.g. 23 → 35 → 155 …) and any message older than the batch — including the user's own first message — becomes unreachable. The data is intact; it just can never be paged to.

Root cause

The frontend derived its before pagination cursor from messages[0].created_at alone, dropping the |id component. The backend's before contract is <created_at>|<id> and it already emits a ready-to-use cursor field on every message (_base_message_entry).

For a timestamp-only cursor, _parse_message_cursor falls back to uuid.UUID(int=(1 << 128) - 1) (max UUID). The filter tuple_(created_at, id) < (before_created_at, MAX_UUID) is then true for every row sharing that timestamp, so each page re-returns the same equal-timestamp batch. messages[0].created_at of the next page is the same timestamp again → the cursor never advances below it. The backend comment already notes: "Legacy timestamp-only cursors may duplicate equal-timestamp messages … New clients should round-trip the emitted cursor."

Fix

Round-trip the backend cursor field at all four cursor-set sites in AgentDetailPage.tsx (refreshSessionMessages, session load, loadMoreHistoryMessages, loadMoreChatHistoryMessages), falling back to created_at only when cursor is absent. No backend change.

Verification

Reproduced on a real conversation with 23 same-timestamp tool_call rows:

  • Before: scroll-up loops, tool count balloons to 155+, first user message never appears.
  • After: scroll-up terminates with "已加载全部历史消息", tool count is the correct 23, and the first user message + agent greeting load correctly.

🤖 Generated with Claude Code

The agent chat view derived its "before" pagination cursor from
`messages[0].created_at` alone, dropping the `|id` component. The
backend's `before` contract is `<created_at>|<id>`, and it already
returns a ready-to-use `cursor` field per message.

When a single turn persists a batch of messages that share one
`created_at` (e.g. a burst of tool_call rows) and that batch is larger
than HISTORY_PAGE_SIZE, the timestamp-only cursor never advances: the
backend's legacy-cursor fallback pins the id to the max UUID, so every
page re-returns the same equal-timestamp rows. Pagination loops,
inflating the tool count on each scroll, and any message older than the
batch (including the user's first message) can never be loaded.

Round-trip the backend `cursor` field at all four cursor-set sites,
falling back to `created_at` only when absent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Y1fe1Zh0u added a commit that referenced this pull request Jul 24, 2026
Integrate PR #785 so the test branch round-trips the backend compound cursor instead of repeating equal-timestamp pages.

Constraint: Preserve the PR head unchanged for integration testing
Confidence: high
Scope-risk: narrow
Tested: Merge completed without conflicts
Not-tested: Regression suite pending combined-branch validation
@Y1fe1Zh0u Y1fe1Zh0u closed this pull request by merging all changes into dataelement:main in 5564700 Jul 24, 2026
Y1fe1Zh0u added a commit that referenced this pull request Jul 24, 2026
Waiting-to-cancel transitions can emit distinct delivery receipts from one preserved checkpoint, while legacy group members may no longer resolve an Agent model. Narrow checkpoint uniqueness only for delivery receipts and let shared compaction fall back to the configured Group Compact model when no active Agent model remains.

Constraint: Control-plane cancel must preserve the last authoritative Graph checkpoint

Rejected: Fabricate a cancel checkpoint | violates the existing Runtime checkpoint contract

Confidence: high

Scope-risk: narrow

Reversibility: clean before distinct same-checkpoint delivery rows are written

Directive: Keep delivery idempotency keyed by run_id and idempotency_key

Tested: 110 targeted backend tests, Alembic head validation, Python compileall, scoped Ruff

Not-tested: Live 3010 migration and reconciliation before commit

Related: #785 #786 #787 #788
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants