feat: add persisted whole-chat summary with background generation - #26657
Conversation
Add a persisted chats.summary populated by a background generator after successful root-chat turns, delivered live via a new chat_summary_change watch event, with per-feature cost attribution and a deployment-wide summary-generation model override.
Docs previewCheck off each page once it's been reviewed. If a page changes in a later push, its checkbox clears automatically so it gets a fresh look. Pages not yet wired into the docs navigation aren't listed here. |
The UnknownContextReturns400 subtest hardcodes the valid override context list in its expected error. Adding the summary_generation context changed the message, so update both assertions.
|
/coder-agents-review |
|
Chat: Review posted | View chat Review historydeep-review v0.9.0 | Round 4 | Last posted: Round 4, 25 findings (1 P0, 2 P2, 10 P3, 5 P4, 6 Nit, 1 Note), COMMENT. Review Finding inventoryFindings
Contested and acknowledgedCRF-14 (P3, coderd/x/chatd/chatd.go:4993) - No orchestration-level test for generateAndStoreChatSummary
CRF-18 (P4, coderd/x/chatd/chatd.go:4903) - First-turn summary threshold=1
CRF-20 (Note, codersdk/chats.go:1775) - Event naming inversion
CRF-21 (P3, coderd/x/chatd/chatd.go:4961) - Premature full message load before cadence gate
Law analysisEffective LOC: 1704 additions, 68 deletions (31 files). Head SHA: 0bc176d. Verdict: Don't split. Enforcement: Advisory. Concerns: 8 (schema, queries, cost attribution, background generation, model override, SDK/API, frontend, infrastructure plumbing). All serve one feature with a linear dependency chain. Effective human-reviewed content ~1285 lines at 38.7% test density. Round logRound 1Panel (26 reviewers). 1 P0, 2 P2, 7 P3, 3 P4, 5 Nit, 4 Note. Reviewed against 637a801..0bc176d. Round 2Churn guard: PROCEED (17 addressed, 1 acknowledged, 3 contested, 0 deferred, 0 silent). Author pushed 313f8b7 addressing all P0/P2/P3 findings except CRF-14 (contested), CRF-21 (contested). CRF-18 (P4, contested). CRF-20 (Note, acknowledged). Panel closed CRF-14 (5/6), CRF-18 (6/6), CRF-21 (6/6). 1 new P4 (CRF-23). Round 3Churn guard: PROCEED (1 addressed). CRF-23 fixed in 5d0a775. All 23 findings resolved. Reviewed against c782cbc..5d0a775. Round 4Churn guard: PROCEED (0 open from prior). Panel (7 reviewers). PR restructured: model override split to #26803, usage recording deferred to #26689, read ordering fixed, goInflight approach changed. 2 new P3s (CRF-24, CRF-25). Reviewed against c782cbc..7a57ac0. About deep-reviewCRF = Coder Review Finding (P0-P4, Nit, Note)
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0bc176d8c4
ℹ️ 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".
There was a problem hiding this comment.
Deep review of PR #26657
Well-structured feature addition. The optimistic concurrency control, cost attribution, and background generation patterns are solid. Three findings worth addressing before merge.
🔴 Migration version collision
000530_chat_summary collides with 000530_relay_host_nats_port which already exists on main. golang-migrate/iofs returns source.ErrDuplicateMigration for duplicate version numbers, blocking all deployments. Renumber to 000531 (or whatever the next free slot is at merge time).
🟡 Stale chat struct in cadence gate
generateAndStoreChatSummary receives the chat struct snapshotted when the turn finished, but messages are re-read from DB. If two consecutive turns complete quickly, both background goroutines see the same stale chat.Summary.Valid / chat.SummaryGeneratedAt and both pass the cadence gate. Both generate a summary and call updateChatSummary. Since UpdateChatSummary checks history_version but does not update it, both writes can succeed; the second silently overwrites the first. The summaries are likely identical (same input), so data is not corrupted, but one LLM call is wasted.
A cheap fix: re-read the chat row at the top of generateAndStoreChatSummary:
chat, err = p.db.GetChatByID(authCtx, chat.ID)🟡 p.inflight.Go bypasses shutdown admission gate
maybeGenerateChatSummaryAsync calls p.inflight.Go(func(){...}) directly instead of p.goInflight(func(){...}). The comment explains why (deadlock avoidance with drainInflight). The goroutine IS tracked in p.inflight so it cannot outlive the server. But combined with context.WithoutCancel(ctx) and chatSummaryWorkTimeout = 120s, a summary launched right before shutdown could block Close() for up to 2 minutes. This creates an asymmetry: finalizeSuccessfulTurnStatusLabelAndPush uses goInflight and is silently dropped during shutdown, but summary generation runs regardless.
If the 120s shutdown delay is acceptable, this is fine. If fast shutdown matters, consider adding an early select on p.ctx.Done() inside generateAndStoreChatSummary before the model call.
There was a problem hiding this comment.
Solid feature addition. The optimistic concurrency via history_version, cost attribution model, and background generation pipeline are well-designed. The test suite covers the critical paths: cadence gate, transcript rendering, validation, override resolution, SQL staleness race, cost source tagging, and frontend event merging. Six reviewers independently confirmed the event isolation pattern works correctly.
1 P0, 2 P2, 7 P3, 3 P4, 5 Nit.
The P0 (migration collision) blocks all CI. The two P2s (frontend cross-clobber between event types, shutdown admission bypass) are the substantive findings requiring fixes.
"The goroutine IS tracked in p.inflight so it can't outlive the server, but combined with context.WithoutCancel(ctx) and chatSummaryWorkTimeout = 120s, a summary launched right before shutdown could block Close() for up to 2 minutes." (Hisoka)
🤖 This review was automatically generated with Coder Agents.
# Conflicts: # docs/admin/security/audit-logs.md
Resolve the actionable findings from the deep review of #26657: - P0/CRF-2: renumber the chat summary migration from 000530 to 000534 to avoid colliding with 000530_relay_host_nats_port on main. - CRF-3: scope summary_change/chat_summary_change merges to their own field so an equal-timestamp event cannot clobber the other summary; add the equal-timestamp regression tests. - CRF-4/CRF-13: bail out of background summary generation when shutdown has begun so Close() is not blocked, and fix the stale comment. - CRF-5: re-read the chat before the cadence gate so rapid turns do not both pass against a stale snapshot. - CRF-7: add a CHECK constraint on chat_messages.cost_source. - CRF-8: use a dedicated chatSummaryWriteTimeout for the summary write. - CRF-17: only count sentence terminators at a word boundary so dotted identifiers do not inflate the count. - CRF-1/CRF-6: deduplicate the usage-recording and model-override resolution paths shared by title and summary generation. - CRF-15/CRF-16: cover ChatMessageVisibilityUser and model-only user messages in the summary tests. - CRF-9/10/11/12/19/22: comment cleanups, errors.AsType, and drop the popover reference from the settings description.
|
Addressed the deep-review feedback in Blocking
P2
P3
P4 / Nit
Responses (not changed) — left inline replies on CRF-14, CRF-18, CRF-20, and CRF-21 with rationale. |
|
/coder-agents-review |
There was a problem hiding this comment.
All P0/P2/P3 findings from round 1 are addressed. The fixes are well-executed: the deduplication extractions (CRF-1, CRF-6) are genuine generalizations, the frontend cross-clobber fix (CRF-3) correctly scopes each field to its own event type with regression tests, the shutdown admission guard (CRF-4) avoids the deadlock concern while preventing 120s drain delays, and the cadence gate re-read (CRF-5) eliminates redundant LLM calls.
Contested findings closed by panel:
- CRF-14 (P3, no orchestration test): 5/6 accept. Components are well-tested; orchestration is a linear pipeline with no conditional branching the unit tests miss.
- CRF-18 (P4, first-turn threshold): 6/6 accept. Product decision.
- CRF-21 (P3, premature full load): 6/6 accept. Messages are needed for both the cadence gate and the transcript renderer.
1 new P4 below.
"The code held up. The
recordHiddenUsageMessageTxrefactor is clean, theUpdateChatMessageCostSourcetwo-step insert-then-tag avoids threading cost_source through the batch insert, and the history_version staleness guard mirrors the provenUpdateChatLastTurnSummarypattern." (Hisoka)
🤖 This review was automatically generated with Coder Agents.
CRF-23: add TestCountSentenceTerminators so the CRF-17 fix is covered. Without it, removing the word-boundary guard in countSentenceTerminators would not fail any test. The test asserts periods inside dotted identifiers are not counted and that a dotted-identifier-dense summary stays under summaryMaxSentences.
|
/coder-agents-review |
|
@jaaydenh ⛔ This review has reached its per-chat spend limit ($119.20 / $100.00). Further review rounds are paused. To raise the limit and continue, comment: This is a per-chat budget, separate from any account-level usage limit.
|
|
/coder-agents-review set-spend-limit:150 |
There was a problem hiding this comment.
All 23 findings resolved. CRF-23 (P4, test pin for dotted-identifier sentence counting) addressed in 5d0a775 with TestCountSentenceTerminators. The test asserts dotted identifiers (auth.rbac.Policy, file.go) are not counted as sentence boundaries and that a dotted-identifier-dense summary passes validation. Removing the unicode.IsSpace boundary guard now fails the test.
CI: 25 passed, 2 failing (UI Review, UI Tests). The UI failures appear unrelated to the changes in this PR.
No further findings.
🤖 This review was automatically generated with Coder Agents.
Move summary generation model overrides out of the persisted summary PR so the base feature always uses the chat model. Co-authored-by: Cursor <[email protected]>
Remove cost_source, UpdateChatMessageCostSource, and summary usage recording from the whole-chat summary feature so summary persistence is not blocked by hidden accounting rows advancing history_version. Title usage recording reverts to main's InsertChatMessages path. Co-authored-by: Cursor <[email protected]>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 877bbf0ffb
ℹ️ 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".
…races Addresses two issues from the codex review of background whole-chat summary generation: - Read the chat (and its history_version) before loading the transcript so a turn committing between the two reads leaves the captured history_version behind the transcript. UpdateChatSummary then rejects the write instead of persisting a summary that omits the just-committed turn and advancing the cadence marker past it. - Launch the background summary goroutine through goInflight instead of p.inflight.Go. goInflight serializes the WaitGroup Add with drainInflight under inflightMu and drops the launch once shutdown has begun, matching the sibling finalize and last-turn-summary helpers. The previous direct Add raced drainInflight's Wait and could delay Close.
|
Addressed the latest codex review feedback in 7a57ac0:
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7a57ac09c5
ℹ️ 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 |
This already does share some functionality with title generation. Most of the new code is from adding a new endpoint and tests. |
…00541 Main added 000540_workspace_build_orchestrations, which collided with this branch's 000540_chat_summary and made golang-migrate fail with "duplicate migration file" in every Postgres-backed test. Renumber to the next free slot. The migration content is unchanged and the chats_expanded recreation still matches the current schema; workspace_build_orchestrations does not touch the chats table.
| require.NoError(t, err) | ||
| db := database.New(sqlDB) | ||
|
|
||
| ctx := testutil.Context(t, testutil.WaitMedium) |
|
|
||
| // If a turn commits after this read, the stale history_version makes the | ||
| // eventual summary write lose instead of omitting that newer turn. | ||
| chat, err := p.db.GetChatByID(authCtx, chat.ID) |
There was a problem hiding this comment.
Why do we need authCtx to fetch the chat, but we can updateChatSummary using the original ctx?
Edit: Answered this myself by reading further. It seems updateChatSummary escalates privileges by itself.
This needs a complete rethink in how it's structured IMO. The only consumer of the unauthenticated ctx seems to be generateChatSummary, and we pay the prices of juggling contexts containing auth for a single call. The mixture of self-escalating methods and non- also adds to make it harder to grasp what the authenticated path is.
mafredri
left a comment
There was a problem hiding this comment.
Suggestions for the remaining mechanical fixes. The complete suggestion set was applied together in an isolated worktree and the affected package tests passed.
🤖 This review was automatically generated with Coder Agents.
Adds a persisted whole-chat summary that backs the chat summary popover. A new nullable
chats.summarycolumn is populated in the background after a successful root-chat turn and pushed to clients via a newchat_summary_changewatch event (distinct fromsummary_change, which is bound tolast_turn_summary), so the popover readschat.summarystraight off the loadedChatwith no extra query.This is the data source for the popover and per-chat cost UI built in #26649; the popover can consume
chat.summaryonce this lands (the field is nullable, so merge order does not matter).How it works
chats.summary_generated_atfreshness marker. Generation reads compaction-aware history, renders it to a bounded plain-text transcript (short transcripts are skipped), and asks for a 1-3 sentence summary via structured output. Failures never clear an existing summary.history_version(mirroringlast_turn_summary), so a background write racing a newer turn loses while worker lifecycle transitions cannot reject a fresh write.Deferred to follow-ups
chat_messages.cost_sourcediscriminator and summary/title usage recording were removed from this PR so summary persistence is not blocked by hidden accounting rows advancinghistory_version. Title usage recording stays on main'sInsertChatMessagespath.Notes
000540addschats.summaryandchats.summary_generated_at, and recreateschats_expandedto expose the new columns.Refs #26649