Skip to content

Keep a channel cursor to the microsecond so no channel falls between pages - #689

Merged
davidmckayv merged 3 commits into
CopilotKit:mainfrom
Chebaleomkar:fix/channel-cursor-microseconds
Oct 2, 2026
Merged

davidmckayv merged 3 commits into
CopilotKit:mainfrom
Chebaleomkar:fix/channel-cursor-microseconds

Conversation

@Chebaleomkar

Copy link
Copy Markdown
Contributor

What this changes

ChannelStore.list built its next-page cursor from new Date(last.recency).toISOString(). A Date keeps milliseconds and coalesce(last_message_at, created_at) keeps microseconds, so the cursor named a moment just before its own row. The next page's (pinned, recency, id) < (cursor) then treated every channel later in that same millisecond as newer than the cursor, and those channels were on no page.

The audit reader had exactly this fault and fixed it with a microsecond to_char used for the cursor only (audit.ts, cursorCreatedAt). The channel page query now does the same: recency is selected as to_char(... at time zone 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"'), and the cursor carries that string. The decode side already binds it as text and casts with ::timestamptz, so it keeps the microseconds.

This matters when several channels share a millisecond, which is what a package sync or an import produces, and when a page boundary falls between them.

Where it runs

  • New state that outlives a request? None. The cursor format is unchanged: still a base64url JSON {pinned, recency, id} with a string recency, now with more digits. An old millisecond cursor still parses.
  • What happens on the second replica? The same query.
  • Anything serialised? No.
  • Anything fanned out to a browser? No.
  • New listener, port, or schedule? No.

Boundary and audit

  • Not touched.

Changelog

  • A line under Unreleased.

Proof

New test in channel-activity.integration.test.ts: five channels set to '2026-01-01 00:00:00.123456+00', read with limit: 2, and every page walked.

  • With upstream routes.ts, the walk misses 3 of the 5 channels.
  • With this change, channel-activity.integration and channel-routes pass 108 of 108 against pgvector/pgvector:pg17 with all migrations applied.
  • tsc --noEmit on server/ exits 0, and biome is clean.

…pages

The cursor was built from new Date(last.recency).toISOString(), which drops PostgreSQL's microseconds, so channels later in the same millisecond compared as newer than the cursor and were on no page. The page query now reads the recency as microsecond text for the cursor, the way the audit reader already does for the same fault.
davidmckayv
davidmckayv previously approved these changes Oct 2, 2026
@davidmckayv
davidmckayv enabled auto-merge (squash) October 2, 2026 03:56
auto-merge was automatically disabled October 2, 2026 11:11

Head branch was pushed to by a user without write access

davidmckayv
davidmckayv previously approved these changes Oct 2, 2026

@davidmckayv davidmckayv left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The current head ba277a4 matches the accepted source disposition from the refreshed triage. Required CI must pass before landing.

@davidmckayv
davidmckayv enabled auto-merge (squash) October 2, 2026 16:52
@davidmckayv
davidmckayv merged commit f0929a3 into CopilotKit:main Oct 2, 2026
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