Skip to content

feat: add after_id pagination for chat messages#24531

Merged
david-fraley merged 5 commits intomainfrom
davidfraley/codagt-206-support-after_id-pagination-for-messages
Apr 28, 2026
Merged

feat: add after_id pagination for chat messages#24531
david-fraley merged 5 commits intomainfrom
davidfraley/codagt-206-support-after_id-pagination-for-messages

Conversation

@david-fraley
Copy link
Copy Markdown
Collaborator

Adds an optional after_id query parameter to
GET /api/experimental/chats/{chat}/messages alongside the existing
before_id, so callers can poll for messages newer than a known ID
without opening a websocket. queued_messages is now only returned
when neither cursor is set so polling callers do not receive the
snapshot on every request.

The SQL query gains an additive CASE WHEN @after_id > 0 THEN id > @after_id
bound, preserving current behavior at the sentinel 0. The SDK
ChatMessagesPaginationOptions exposes the new field.

Refs: CODAGT-206

Implementation plan

Problem

Messages endpoint supported only before_id for backward infinite-scroll.
The stream endpoint already accepts after_id for reconnect skipping.
API users polling for new messages had no equivalent for the messages
endpoint.

Requirements

  • Accept an optional after_id int64 query parameter.
  • Preserve existing before_id semantics when after_id is absent.
  • When only after_id is set: return id > after_id, DESC-ordered.
  • When both are set: return the open range after_id < id < before_id.
  • Suppress queued_messages whenever any cursor is set.
  • Validate after_id with the same path as before_id (400 on negative/non-numeric).

Implementation

Area Change
coderd/database/queries/chats.sql Extend GetChatMessagesByChatIDDescPaginated with @after_id::bigint bound.
coderd/exp_chats.go Parse after_id, forward to query, gate queued messages on both cursors being unset.
codersdk/chats.go Add AfterID int64 to ChatMessagesPaginationOptions, encode when > 0.
coderd/x/chatd/chatd.go Explicit AfterID: 0 next to existing BeforeID: 0 for readability.
docs/ai-coder/agents/chats-api.md Expand the "Get chat messages" section with a query-parameter table.

Generated via make gen: queries.sql.go, typesGenerated.ts.

Risk

Low. SQL change is additive (CASE ... WHEN @after_id > 0), sentinel
0 preserves current behavior for existing callers. SDK field addition
is backward-compatible (zero value = no filter). The queued-messages
guard becomes tighter (beforeID == 0 && afterID == 0 instead of
beforeID == 0), but that is indistinguishable for any client that
does not send after_id.

Tests

TestGetChatMessages_Pagination covers:

  • No cursor returns all messages DESC plus queued snapshot.
  • before_id returns older messages, suppresses queued.
  • after_id returns newer messages DESC, suppresses queued.
  • after_id + before_id returns the open range, suppresses queued.
  • limit + after_id sets has_more=true when more rows exist.
  • Negative and non-numeric after_id return 400.

This PR was prepared by Coder Agents on behalf of the issue owner. Please review before merging.

Adds an optional after_id query parameter to
GET /api/experimental/chats/{chat}/messages alongside the existing
before_id, so callers can poll for messages newer than a known ID
without opening a websocket. Queued messages are now only returned
when neither cursor is set.

SQL picks up the new bound via an additive CASE clause. The SDK
ChatMessagesPaginationOptions gains an AfterID field. The docs
section is updated with a query-parameter table.

Refs: CODAGT-206

Co-authored-by: Coder Agents <noreply@coder.com>
@david-fraley david-fraley changed the title feat(coderd/exp_chats): add after_id pagination for chat messages feat: add after_id pagination for chat messages Apr 20, 2026
Keeps the TypeScript client in sync with the SDK change to
ChatMessagesPaginationOptions added in the parent commit.
Existing callers (infinite-scroll) continue to work unchanged.
@david-fraley
Copy link
Copy Markdown
Collaborator Author

/coder-agents-review

Copy link
Copy Markdown

@coder-agents-review coder-agents-review Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean, well-scoped addition. The SQL is symmetric with the existing before_id clause, the SDK field is additive, tests cover the six semantic cells plus validation. CI passes. The queued-messages guard tightening is well-motivated and the comment rewrite earns its place.

Severity count: 1 P2, 2 P3, 7 Nit.

The P2 drew convergence from 10 of 17 reviewers (severities ranging P1 to P3). The core concern: after_id on a DESC-ordered, limit-capped query is the wrong shape for the polling use case the PR advertises. A burst larger than limit silently drops the oldest new messages, and the recovery protocol (dual-cursor walk) is neither documented nor tested. GetChatMessagesByChatIDAscPaginated already exists in chats.sql:243 and would eliminate the hazard for the polling case.

Panelist quote worth preserving, from Hisoka: "Shall I show you the race? The naive poller's next tick sets after_id=N+60 (the newest they just saw). [N+1..N+10] are lost forever. No error, no warning, the queue simply skipped ten messages."

Two pre-existing notes surfaced but not posted as findings: (1) the stream handler at exp_chats.go:2568 uses raw strconv.ParseInt for the same after_id parameter name while this handler uses PositiveInt64, creating two validation regimes on adjacent routes (Knov, Zoro); (2) PostgreSQL sequence allocation outside transactions means cursor-based polling can skip messages that commit out of order, currently mitigated by the single-writer-per-chat invariant (Knuckle). Neither is introduced by this PR.

Commit scope coderd/exp_chats is not a real filesystem path per AGENTS.md and the change spans 8 files across multiple directories. Cross-cutting changes should omit the scope or use a broader path (Leorio).


coderd/x/chatd/chatd_internal_test.go:386

Nit [DEREM-2] The PR adds explicit AfterID: 0 in chatd.go:2255 "for readability" next to BeforeID: 0. The expectation struct here (and at line 549, and dbauthz_test.go:765) still sets only BeforeID: 0 without the matching AfterID: 0. Either apply the readability rationale consistently or skip it everywhere. (Netero)

🤖

🤖 This review was automatically generated with Coder Agents.

Comment thread coderd/exp_chats.go Outdated
Comment thread coderd/exp_chats.go
Comment thread coderd/exp_chats_test.go
Comment thread docs/ai-coder/agents/chats-api.md
Comment thread coderd/exp_chats_test.go
Comment thread codersdk/chats.go Outdated
Comment thread coderd/exp_chats_test.go Outdated
Comment thread coderd/exp_chats_test.go Outdated
Comment thread coderd/exp_chats_test.go
@david-fraley david-fraley marked this pull request as ready for review April 24, 2026 15:23
david-fraley and others added 2 commits April 24, 2026 10:23
…date bounds

Polling callers that set only after_id now receive messages in
ASCENDING id order by dispatching to GetChatMessagesByChatIDAscPaginated.
The previous DESC behavior silently dropped the oldest rows between
polls when a burst exceeded `limit`: a caller that advanced
after_id = max(returned) would permanently miss messages in the gap.

The DESC path still serves the initial load (no cursor), before_id
history walks, and the two-cursor open range.

Also rejects the transposed-cursor case (after_id >= before_id) with
a 400 rather than returning an empty page indistinguishable from
"no messages in this range."

Other review follow-ups:
- AfterID godoc explains the ASC-on-polling behavior.
- Docs note the order difference and the transposed-cursor error.
- New subtests cover polling burst walks, steady-state empty pages,
  and the 400 guard.
- Replace manual range-and-break with slices.ContainsFunc; use
  for i := range count; Message assertion symmetry across 400 tests.
- Drop the stray AfterID: 0 from chatd.go regenerate call; go idiom
  omits zero-value fields.
Copy link
Copy Markdown
Collaborator Author

Thanks for the review. Pushed f634fb1 addressing the P2, both P3s, and most of the nits. Summary:

Finding Action
DEREM-3 (P2) polling correctness Dispatch to GetChatMessagesByChatIDAscPaginated when only after_id is set so the cursor is monotonic (after_id = max(returned_ids)). DESC path still serves initial load, before_id history walks, and the two-cursor open range. New subtest AfterIDPollingWalksBurstWithoutGaps seeds a 60-row burst and walks with limit=25 to confirm no gap.
DEREM-4 (P3) transposed cursors 400 when after_id >= before_id with message "after_id must be less than before_id." New subtest AfterIDGreaterThanOrEqualBeforeIDReturns400 (Transposed + Equal cases).
DEREM-5 (P3) steady-state empty New subtest AfterIDAtOrAboveMaxReturnsEmpty asserts empty page + no queued repeat.
DEREM-1 (P3) en-dash Replaced 1–200 with 1 to 200. lint/emdash passes.
DEREM-7 (Nit) AfterID godoc Added — propagates into typesGenerated.ts too.
DEREM-2 (Nit) mock consistency Reversed the direction of consistency: dropped the stray AfterID: 0 in chatd.go so zero-value fields are omitted everywhere.
DEREM-6 (Nit) assertion symmetry Added Message equality to NonNumericAfterIDReturns400.
DEREM-8 (Nit) range loop for i := range count.
DEREM-9 (Nit) slices.ContainsFunc Applied at both validation 400 subtests.
DEREM-10 (Nit) vacuous Empty LimitCapsAfterIDPageToOldestAndSetsHasMore now seeds a queued message so the suppression assertion is meaningful.

Not actioned: the two pre-existing notes about the stream handler's raw strconv.ParseInt validation and PG sequence ordering are real but out of scope for this PR.

This response was prepared by Coder Agents on behalf of the PR author.

Copy link
Copy Markdown

@coder-agents-review coder-agents-review Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All 10 R1 findings addressed in f634fb1. Netero verified every fix mechanically. The panel (Mafuuu, Meruem, Bisky, Knov) found no new issues and confirmed the fixes address root causes, not symptoms.

The ASC dispatch for the polling path (DEREM-3) is particularly well-executed: the handler switch partitions cursor states cleanly, the ASC params struct lacks BeforeID entirely (making misuse impossible), and the burst-walk test proves gap-free coverage over 60 rows in pages of 25.

Severity count: 0 new findings. CI green (25 passed, 6 skipped).

Bisky's summary: "Ten subtests, all genuine. Every assertion proves behavior. None are costume jewelry."

🤖 This review was automatically generated with Coder Agents.

Copy link
Copy Markdown
Member

@johnstcn johnstcn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking, but one observation for follow-up.

Comment thread coderd/exp_chats.go Outdated
Comment thread coderd/exp_chats.go Outdated
Comment on lines +269 to +272
AND CASE
WHEN @after_id::bigint > 0 THEN id > @after_id::bigint
ELSE true
END
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We now have a divergence between the GetChatMessagesByChatIDDescPaginated and GetChatMessagesByChatIDAscPaginated queries where one accepts both before_id and after_id and the other only accepts after_id.

Potential follow-up: could these two queries be combined into one e.g.

ORDER BY CASE WHEN @asc THEN id ASC
         ELSE id DESC END

Address review feedback to make the cursor-validation and dispatch
comments less verbose while keeping the core rationale.
@david-fraley david-fraley merged commit 5222db8 into main Apr 28, 2026
30 of 31 checks passed
@david-fraley david-fraley deleted the davidfraley/codagt-206-support-after_id-pagination-for-messages branch April 28, 2026 13:31
@github-actions github-actions Bot locked and limited conversation to collaborators Apr 28, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants