feat: dedicated /prompts endpoint for chat history cycle#25083
Conversation
Docs preview📖 View docs preview for |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 34fa90eee2
ℹ️ 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".
|
/coder-agents-review |
34fa90e to
725422a
Compare
|
|
/coder-agents-review |
There was a problem hiding this comment.
Well-scoped PR. The partial index matches the SQL filter exactly, the dbauthz pattern mirrors sibling endpoints, and the frontend integration is clean with appropriate cache invalidation and snapshot-based cycling. The four subtests cover the key dimensions. One P2, four P3s, two P4s.
"The cycle path and the edit path extract different text from the same message." (Hisoka)
1 P2 (legacy scalar content guard), 4 P3 (dead code, comment mismatch, test gap, behavioral divergence), 2 P4 (limit=0 silent accept, empty-result test). The P2 aligns with the existing Codex comment on this PR. The fix is a one-line WHERE clause addition.
🤖 This review was automatically generated with Coder Agents.
725422a to
ab2a893
Compare
|
|
Thanks for the review. Pushed
Local gates green: /coder-agents-review |
ab2a893 to
e96f371
Compare
|
Rebased onto latest /coder-agents-review |
There was a problem hiding this comment.
R1 fixes verified: DEREM-1 (V0 guard), -2 (dead branch), -3 (comment rewrite), -4 (assistant-role test), -6 (limit=0 docs), -7 (empty result test) all confirmed by Netero and 4-reviewer panel. Solid follow-through.
DEREM-5 (whitespace divergence): panel accepts the author's defense that the edit path's appendText is the pre-existing quirk and the SQL behavior is more faithful. However, the divergence is new (this PR switched the cycle path away from the shared extraction), there is no linked ticket, and a deferral without a ticket is a drop. This needs a human decision: file a ticket for the appendText alignment or explicitly accept the gap.
One new P3: the jsonb_typeof guard that prevents the P2 crash on V0 rows has no regression test. Two reviewers converged on this independently.
🤖 This review was automatically generated with Coder Agents.
There was a problem hiding this comment.
Review blocked. DEREM-10 (P3, no regression test for the V0 jsonb_typeof guard) was raised in R2 and has no response. The R3 push was a rebase only (migration renumbered to 000491, no code changes).
Open items before further review:
- DEREM-10 (P3): Insert a V0-style scalar content row in
NewestFirstFilteringand assert the endpoint returns without error and excludes the V0 row. Two reviewers converged on this independently in R2. - DEREM-5 (P3, panel-deferred): The whitespace divergence between cycle and edit paths needs a human decision: file a ticket for the
appendTextalignment, or explicitly accept the gap.
🤖 This review was automatically generated with Coder Agents.
e96f371 to
648c5d1
Compare
|
R2 follow-ups on
/coder-agents-review |
There was a problem hiding this comment.
All findings resolved. 8 fixed across two commits (ab2a893, 648c5d1), 1 deferred with ticket (#25125), 1 dropped. Netero and 3-reviewer panel verified all fixes in R4. No new findings across R2-R4.
Four rounds, 10 findings, clean exit. The partial index, V0 scalar guard, authorization model, cache invalidation strategy, and test suite are solid.
🤖 This review was automatically generated with Coder Agents.
648c5d1 to
93b6f45
Compare
|
Came back and addressed DEREM-5 within this PR. /coder-agents-review |
There was a problem hiding this comment.
All 10 findings resolved. DEREM-5 (the last open item) fixed in 93b6f45: getEditableUserMessagePayload now concatenates text parts verbatim, matching the SQL string_agg. Unit test added, #25125 closed. Netero and 2-reviewer panel verified the fix with no new findings.
Five rounds, 10 findings, all addressed. Clean.
🤖 This review was automatically generated with Coder Agents.
93b6f45 to
f1c2dc3
Compare
ibetitsmike
left a comment
There was a problem hiding this comment.
Nice cleanup after the earlier review. The dedicated endpoint is well-scoped, the legacy content guard and empty-array behavior are covered, and I did not find any blocking issues. Left two small TypeScript consistency nits.
Mux reviewed this on behalf of Mike.
6348014 to
0819a5f
Compare
The merged CODAGT-319 (#25004) cycles only through messages already loaded in the in-memory chat store, capped at 50 per page. Long chats and chats whose oldest turns have rolled out of the page can no longer recall their full prompt history from the composer's up/down arrow cycle. This change adds GET /api/experimental/chats/{chat}/prompts, a small newest-first projection of user-authored prompts that the composer pulls into a dedicated React Query. The query is filtered and ordered server-side, falls within a 1..2000 limit window, and is backed by a partial index that turns the planner's bitmap+sort into a presorted index scan. Frontend: ChatPageContent.tsx now reads the cycle source from useQuery(chatPromptsQuery(chatId)) instead of deriving from the in-memory store. The composer's existing cycleHistorySnapshotRef anchors the in-flight cycle so refetches caused by send/edit mutations cannot shift the indexed prompt out from under the user. Cache invalidation hooks live in the createChatMessage, editChatMessage, and useChatStore.upsertCacheMessages paths.
0819a5f to
0149639
Compare
Follow-up to #25004. The merged change cycles only through messages already loaded in the in-memory chat store (page size 50). Long chats and chats whose oldest turns have rolled out of the page lose access to their earlier prompts in the composer's up/down arrow cycle. This PR adds a dedicated server endpoint that returns the full prompt history, newest first, and rewires the composer to use it.
What changed
Endpoint
GET /api/experimental/chats/{chat}/prompts?limit=Nlimit:0..2000.0(the default) is treated as the server-side default of 500; out-of-range values return400. Negative values are rejected by the SDK'sPositiveInt32parser before reaching the handler.dbauthz, mirroringGetChatMessagesByChatID.role='user',deleted=false,visibility IN ('user','both'), guards the lateral withjsonb_typeof(content) = 'array'so legacy V0 scalar-string rows are silently skipped, then unrollscontentJSONB withWITH ORDINALITYand concatenates onlytype='text'parts in original order viastring_agg(... ORDER BY ordinality). Messages whose joined text is whitespace-only are dropped viaHAVING ... ~ '\S'so cycling never lands on a blank entry.Partial index (migration
000494)The partial WHERE matches the query's filter exactly and the key order matches
ORDER BY id DESC, so the planner gets both the filter and the ordering from the index without a sort step.EXPLAIN ANALYZEon a synthetic 51-chat × 5,000-message dataset (≈260k rows, 10k user prompts in the target chat,random_page_cost=1.1):Index Scan Backward using chat_messages_pkey, 250,848 rows removed by filterIndex Scan using idx_chat_messages_user_prompts, no filter≈25× faster, 175× fewer buffer hits.
Frontend
chatPromptsKey/chatPromptsQueryfactories insite/src/api/queries/chats.ts(staleTime: 30s,enabled: chatId !== "", asks the server for 500 prompts).ChatPageContent.tsxreplaces the in-memory derivation withuseQuery(chatPromptsQuery(chatId ?? "")). The composer's existingcycleHistorySnapshotRefanchors the in-flight cycle so a refetch arriving mid-cycle cannot shift the indexed prompt out from under the user.getEditableUserMessagePayloadnow concatenates user-message text parts verbatim, mirroring the server'sstring_agg(part->>'text', '' ORDER BY ordinality), instead of routing through the streaming-orientedparseMessageContent/appendTextpipeline (which drops whitespace-only chunks — correct for assistant streams, wrong for a user's persisted message). This keeps the cycle and the edit path in agreement on the same message. File blocks are still pulled separately viaparseMessageContent(...).blocks.filter(isEditableUserMessageFileBlock).createChatMessage.onSuccess,editChatMessage.onSettled, anduseChatStore.upsertCacheMessages(only when an upserted message hasrole === "user").chatPromptsKey(CHAT_ID)from the samemessagesDatato keep them offline.Tests
TestGetChatUserPromptsincoderd/exp_chats_test.gowith five subtests:NewestFirstFiltering— multi-part concatenation, non-text parts skipped, whitespace-only filtered, soft-deleted excluded,model-only visibility excluded, assistant-role excluded bycm.role = 'user', legacy V0 scalar row silently excluded by thejsonb_typeofguard, ordering newest first.LimitClampsResults— explicitlimit=2returns the two newest prompts.InvalidLimitRejected—limit=5000is400 Bad Request.NotFoundForOtherUsers— a separate user in the same org gets404, not the prompts.EmptyResultIsJSONArray— zero-message chat and assistant-only chat both returnPrompts: [](non-nil, empty).messageParsing.test.tsasserting thatgetEditableUserMessagePayload(["hello", " ", "world"])returns"hello world", locking in the agreement with the SQLstring_agg.dbauthz_test.goadds theMethodTestSuite.TestChats/GetChatUserPromptsByChatIDentry, asserting parent-chatpolicy.ActionRead.pnpm test src/pages/AgentsPage— 1159 passed, 2 skipped.make genproduces no diff.Manual verification
Seeded a dev chat with Claude Sonnet 4.6 via the aibridge Anthropic provider and posted 20 user prompts end-to-end. Verified that the
/promptsendpoint returns 20 rows newest-first, thatlimit=10clamps correctly, thatlimit=0uses the server default of 500, and that the up/down keyboard cycle in the composer walks the same sequence (and reverses correctly back to the empty draft).Out of scope
Implementation plan
CODAGT-319 Follow-up — Dedicated
/promptsendpointContext
The merged feature (#25004 / d32842f) cycles only through messages already loaded in the in-memory chat store, which is capped at the first 50 messages of the current page. Long chats and chats whose oldest turns have rolled out of the page can no longer recall their full prompt history. This follow-up exposes a dedicated server endpoint that returns the user-authored prompts in a chat, newest first, and rewires the composer to use it.
Design
Endpoint
GET /api/experimental/chats/{chat}/prompts?limit=NReturns:
limit:0..2000.0(the default) → server-side default of 500. The wire-level default is encoded in SQL asCOALESCE(NULLIF($limit, 0), 500). Negatives are rejected upstream byPositiveInt32; the handler only caps the upper bound.dbauthz, mirroringGetChatMessagesByChatID.SQL
The query lives in
coderd/database/queries/chats.sqlasGetChatUserPromptsByChatID:role='user',deleted=false,visibility IN ('user','both')to mirror the composer's "what the user actually typed and can re-send" contract.jsonb_typeof(content) = 'array'so legacy V0 rows whose content is a scalar JSON string (predates migration000434) are silently excluded instead of raising"cannot extract elements from a scalar".contentJSONB withjsonb_array_elements WITH ORDINALITYand concatenates onlytype='text'parts, preserving original order viastring_agg(... ORDER BY ordinality).textso sqlc emits astringfield instead of[]byte.HAVING string_agg(...) ~ '\S'so cycling never lands on a blank entry.cm.id DESC(idis a sequence, so this is "newest first" without relying oncreated_at).Index
New partial index added in migration
000494:The partial WHERE clause matches the query's filter exactly, so the planner can use the index for both filtering and ordering without a sort step.
Frontend
chatPromptsKey(chatId)andchatPromptsQuery(chatId)factories insite/src/api/queries/chats.ts.staleTime: 30s,enabled: chatId !== "". Asks the server for 500 prompts (well below the 2000 max, plenty for the cycle).ChatPageContent.tsxreplaces the in-memory derivation withuseQuery(chatPromptsQuery(chatId ?? "")). The composer'scycleHistorySnapshotRefalready takes a stable snapshot at cycle entry, so a refetch arriving mid-cycle cannot shift the indexed prompt out from under the user.getEditableUserMessagePayloadextracts the edit-path text from raw user-message parts (filtertype === "text", join verbatim) instead of going throughparseMessageContent/appendText, which is built for assistant streams and intentionally drops whitespace-only chunks. Without this, cycling and clicking Edit on the same message could produce different draft text for messages with whitespace-only interleaved text parts.createChatMessage.onSuccess,editChatMessage.onSettled, anduseChatStore.upsertCacheMessages(when at least one upserted message hasrole === "user") all invalidatechatPromptsKey(chatId).Tests
TestGetChatUserPrompts(coderd/exp_chats_test.go) covers:NewestFirstFiltering— multi-part concatenation, non-text parts skipped, whitespace-only filtered, soft-deleted excluded,model-only visibility excluded, assistant-role excluded bycm.role = 'user', legacy V0 scalar row silently excluded by thejsonb_typeofguard, ordering newest first.LimitClampsResults— explicitlimit=2returns the two newest prompts.InvalidLimitRejected—limit=5000is400 Bad Request.NotFoundForOtherUsers— a separate user in the same org gets404, not the prompts.EmptyResultIsJSONArray— zero-message chat and assistant-only chat both returnPrompts: [](non-nil, empty).messageParsing.test.tsadds a unit test asserting thatgetEditableUserMessagePayload(["hello", " ", "world"])returns"hello world", locking in the agreement with the SQLstring_agg.dbauthz_test.goadds theMethodTestSuite.TestChats/GetChatUserPromptsByChatIDentry, asserting the parent-chatpolicy.ActionRead.Out of scope
coder-agents-review history
Four review rounds, eight unique findings, all addressed in this PR (approved twice). Rebased onto
maintwice after R4: first to pick up new migrations000491/000492, then again for000493_idx_chat_diff_statuses_url_lower. The prompts-index migration was renumbered000491 → 000493 → 000494viacoderd/database/migrations/fix_migration_numbers.sh; no other diff changes.725422abCOMMENTED— 7 findings (DEREM-1..7)ab2a8936COMMENTED— 1 new (DEREM-10) + 1 reraised (DEREM-5)648c5d1fAPPROVED— 7 fixed, DEREM-5 deferred via #2512593b6f450APPROVED— DEREM-5 also fixed in-PR, #25125 closedchats.sqljsonb_typeof(content) = 'array'guard against V0 scalar rowsexp_chats.golimit < 0branch (SDK rejects upstream)useChatStore.tsexp_chats_test.goNewestFirstFilteringnow inserts an assistant-role message so therole='user'filter is exercised end-to-endmessageParsing.tsgetEditableUserMessagePayloadto concatenate text parts verbatim, mirroring the SQLstring_aggexp_chats.goexp_chats_test.goEmptyResultIsJSONArraysubtestexp_chats_test.goNewestFirstFilteringnow inserts a raw V0 scalar-content row; verified locally that removing the guard makes the test failThis PR was created on behalf of @ibetitsmike by Coder Agents.