fix(chat): page agent history by compound cursor, not bare timestamp - #785
Merged
1 commit merged intoJul 24, 2026
Merged
Conversation
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
5564700
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 oftool_callrows written in one turn) and that batch is larger thanHISTORY_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
beforepagination cursor frommessages[0].created_atalone, dropping the|idcomponent. The backend'sbeforecontract is<created_at>|<id>and it already emits a ready-to-usecursorfield on every message (_base_message_entry).For a timestamp-only cursor,
_parse_message_cursorfalls back touuid.UUID(int=(1 << 128) - 1)(max UUID). The filtertuple_(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_atof 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
cursorfield at all four cursor-set sites inAgentDetailPage.tsx(refreshSessionMessages, session load,loadMoreHistoryMessages,loadMoreChatHistoryMessages), falling back tocreated_atonly whencursoris absent. No backend change.Verification
Reproduced on a real conversation with 23 same-timestamp
tool_callrows:🤖 Generated with Claude Code