🌐 US-Proxy
class="logged-out env-production page-responsive" style="word-wrap: break-word;" >
Skip to content

fix(coderd): give chat message ids an append-order guarantee - #27495

Merged
ibetitsmike merged 3 commits into
mainfrom
mike/chat-message-order-tiebreaker
Jul 29, 2026
Merged

fix(coderd): give chat message ids an append-order guarantee#27495
ibetitsmike merged 3 commits into
mainfrom
mike/chat-message-order-tiebreaker

Conversation

@ibetitsmike

@ibetitsmike ibetitsmike commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator

Chat message ordering was derived from created_at, which is now() and therefore the transaction start time. That makes it unusable as an append-order column for two independent reasons: every row in one InsertChatMessages batch shares a single timestamp, and two concurrent transactions can commit in the opposite order to the one they started in.

This PR gives chat_messages.id a real append-order guarantee and moves the history reads onto it.

Changes

InsertChatMessages had no input-order guarantee. Callers index the returned slice by input position. That only worked because PostgreSQL happens to evaluate the BIGSERIAL default in row order. Ids are now allocated up front and the k-th smallest is assigned to input index k, so the pairing does not depend on where the column default is evaluated. Returned rows are explicitly ORDER BY id.

Three history reads now order by id.

Query Was Now
GetChatMessagesByChatID created_at ASC id ASC
GetChatMessagesByRevisionForStream created_at ASC, id ASC id ASC
GetLastChatMessageByRole created_at DESC, id DESC id DESC

GetChatMessagesByChatID paginated by id while ordering by created_at, which is incoherent on its own terms.

The other two matter because of who consumes them. The stream query supplies incremental updates on the same socket that emits a full GetChatMessagesByChatID snapshot on history reset, so once that snapshot moved to id the two disagreed under timestamp skew. GetLastChatMessageByRole returns an id that is then used as an id cursor, both as AfterID when synthesizing tool cancellations and as chats.last_read_message_id, where a stale anchor leaves later assistant messages permanently unread.

A tie-breaker would not have fixed either one. It only resolves equal timestamps; leading with created_at is the actual defect.

GetLastChatMessageByRole loses its index, so this adds one. ORDER BY created_at DESC, id DESC could take an ordered scan of idx_chat_messages_chat_created. Nothing in the schema can supply ORDER BY id DESC LIMIT 1 for a given chat_id and role, so the planner switches to a backward scan of the primary key and filters every newer row in the table, scanning all of it when the chat has no message in that role, which is the routine case for a fresh chat. Migration 000559 adds (chat_id, role, id DESC) WHERE deleted = false, the same shape as the existing idx_chat_messages_user_prompts. This matters because the query is hot: it runs on every stream connect and disconnect, and once per turn when synthesizing tool cancellations.

GetChatMessagesForPromptByChatID has the same defect and is fixed in the stacked PR, because its compaction boundary change is semantic and deserves a separate review. Auto-archive stays timestamp-based deliberately: it measures activity, not order.

Wrapping the insert in a CTE (needed because INSERT cannot take ORDER BY) makes sqlc synthesize InsertChatMessagesRow. It is structurally identical to ChatMessage, so the call sites use a direct struct conversion that stops compiling if the two ever diverge.

Testing

Behavior tests write created_at values inverted against id order, so a reader that leads with created_at returns the batch backwards. All three queries were verified red by reverting the ORDER BY and regenerating: the stream query returned [3,2,1] for [1,2,3], and GetLastChatMessageByRole picked id 1 instead of id 3.

TestInsertChatMessagesOrderContract asserts against the generated SQL, covering what a behavior test cannot: PostgreSQL evaluates the id default in row order anyway, so a batch still looks ordered once the guarantee is removed.

TestChatMessagesSequenceCacheIsOne guards the cross-batch half of the invariant. Ids follow chat row lock order only while the sequence hands out one value at a time; sequence cache blocks are per session, so with a cache above one a backend holding stale cached values can lock second and still commit lower ids. Bumping a sequence cache is an ordinary throughput tweak, and it would silently corrupt history order.

The index was checked on a 200k row fixture. Without it, the zero-match lookup filters all 200,000 rows over 2763 buffers; with it, the plan is an index scan with both chat_id and role in the index condition, no sort node, and 3 buffers.

Note that the within-batch mapping does not depend on the cache size. It is established by ROW_NUMBER() OVER (ORDER BY id) over the allocated ids, so it holds regardless of nextval evaluation order.

Note on the deleted subagent hand-sort

The subagent history reader's hand-sort stays deleted, but calling it redundant was imprecise. It sorted by created_at then id, so it is only equivalent to id ordering when the two agree. When they disagree the old code selected a different "latest assistant". This is a deliberate behavior change to match the new invariant, not dead-code removal.

Opened by Mux on behalf of Mike.

@ibetitsmike
ibetitsmike marked this pull request as ready for review July 25, 2026 15:30
@ibetitsmike
ibetitsmike force-pushed the mike/chat-message-order-tiebreaker branch from bdc295f to f4c9360 Compare July 28, 2026 16:11
@ibetitsmike ibetitsmike changed the title fix(coderd): make chat message history ordering deterministic fix(coderd/database): correlate chat message ids to batch input order Jul 28, 2026
@ibetitsmike ibetitsmike changed the title fix(coderd/database): correlate chat message ids to batch input order fix(coderd): correlate chat message ids to batch input order Jul 28, 2026
@ibetitsmike
ibetitsmike force-pushed the mike/chat-message-order-tiebreaker branch from f4c9360 to 6ba9454 Compare July 28, 2026 16:50
@ibetitsmike

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Another round soon, please!

Reviewed commit: 6ba945466a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@ibetitsmike ibetitsmike changed the title fix(coderd): correlate chat message ids to batch input order fix(coderd): give chat message ids an append-order guarantee Jul 28, 2026
@ibetitsmike
ibetitsmike force-pushed the mike/chat-message-order-tiebreaker branch from 1e49a5e to 026bb95 Compare July 28, 2026 22:58
@ibetitsmike

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 026bb956ff

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread coderd/database/queries/chats.sql
@ibetitsmike

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep it up!

Reviewed commit: 026bb956ff

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

`InsertChatMessages` relied on PostgreSQL evaluating the `BIGSERIAL`
default in input-array order, and `GetChatMessagesByChatID` ordered by
`created_at` while paginating by `id`. Callers that index the returned
slice positionally, and readers that reconstruct history, therefore had
no guarantee behind them.

Allocate the ids before the insert and assign the k-th smallest to input
index k, then return the rows explicitly ordered by id. Order history
reads by id alone so they agree with the `after_id` cursor: `created_at`
is the transaction start time, so it can disagree with append order when
a transaction takes the chat row lock later than one that started after
it.

Wrapping the insert in a CTE makes sqlc synthesize `InsertChatMessagesRow`,
which converts to `ChatMessage` at the four call sites.
GetChatMessagesByRevisionForStream and GetLastChatMessageByRole led with
created_at, which is the transaction start time and shared by every row in an
insert batch. The stream query disagreed with the id-ordered full history
snapshot the same socket emits on reset, and the last-message id is consumed
as an id cursor by synthetic tool cancellation and by last_read_message_id.

Ordering GetLastChatMessageByRole by id leaves it with no index that can
supply its LIMIT 1 row in index order, so add (chat_id, role, id DESC) where
deleted = false. Without it the planner takes a backward primary key scan and
filters every newer row in the table, scanning all of it when the chat has no
message in that role.
@ibetitsmike
ibetitsmike force-pushed the mike/chat-message-order-tiebreaker branch from 026bb95 to 5d4893f Compare July 29, 2026 07:05
@ibetitsmike

Copy link
Copy Markdown
Collaborator Author

Rebased onto main and renumbered the migration from 000558 to 000559. 000558_audit_oauth2_provider_settings landed on main in #27316 after the last review here, and because stacked PRs target their parent branch, that collision would never have surfaced in this PR's CI.

Nothing else changed: git range-diff against the pre-rebase heads reports every commit identical apart from the two migration filenames.

Revalidated after the rebase: make gen clean (including a forced dump.sql regen), migration up/down tests, and the chat ordering tests.

Mux is working on Mike's behalf.

@ibetitsmike

Copy link
Copy Markdown
Collaborator Author

@codex review

@ibetitsmike
ibetitsmike enabled auto-merge (squash) July 29, 2026 07:09

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5d4893f1af

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread coderd/database/queries/chats.sql
@ibetitsmike
ibetitsmike merged commit e96d864 into main Jul 29, 2026
32 checks passed
@ibetitsmike
ibetitsmike deleted the mike/chat-message-order-tiebreaker branch July 29, 2026 07:17
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 29, 2026
@ibetitsmike

Copy link
Copy Markdown
Collaborator Author

@codex review

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