Thanks to visit codestin.com
Credit goes to github.com

Skip to content

fix(coderd): exclude accounting rows from chat history_version - #26689

Closed
jaaydenh wants to merge 11 commits into
mainfrom
fix-chat-history-accounting
Closed

fix(coderd): exclude accounting rows from chat history_version#26689
jaaydenh wants to merge 11 commits into
mainfrom
fix-chat-history-accounting

Conversation

@jaaydenh

Copy link
Copy Markdown
Contributor

Stacked on #26657 (the persisted whole-chat summary backend). Base branch is chat-summary-62j9; review/merge that first.

Fixes a bug where the persisted whole-chat summary (and the existing last_turn_summary sidebar label) was generated after a turn but never saved, so the summary popover always showed "No summary yet."

The summary write is guarded by an optimistic check on chats.history_version (UPDATE chats SET summary=... WHERE history_version = @expected). Background summary and manual title generation record their spend as hidden, soft-deleted chat_messages accounting rows. Two issues made those accounting rows advance history_version and invalidate the guard:

  1. The AFTER STATEMENT history triggers advanced history_version for every message row, including accounting rows.
  2. The accounting rows were inserted via InsertChatMessages with cost_source NULL and only tagged afterward by UpdateChatMessageCostSource, so the insert trigger saw an ordinary row regardless.

This PR:

  • Adds migration 000531 recreating the history triggers so only rows with cost_source IS NULL advance history_version.
  • Adds InsertChatAccountingMessage, which sets cost_source on the initial INSERT so the trigger classifies the row correctly, and switches the summary and title usage writers to it.
  • Drops the now-redundant UpdateChatMessageCostSource query and the last_model_config_id restoration in those accounting paths.

snapshot_version still advances for every change, so history_version trails after an accounting-only write and catches up on the next real message.

Verified against a dev instance: a fresh chat now persists its summary after the first completed turn, with no skipped stale chat summary update.

jaaydenh added 2 commits June 24, 2026 11:23
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.
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.
@jaaydenh

Copy link
Copy Markdown
Contributor Author

/coder-agents-review
@codex review

@coder-agents-review

coder-agents-review Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Chat: Review posted | View chat
Requested: 2026-06-29 15:09 UTC by @jaaydenh
Spend: $91.52 / $100.00

Review history
  • R1 (2026-06-26): 15 reviewers, 3 Nit, 1 Note, 1 P3, 3 P4, COMMENT. Review
  • R2 (2026-06-29): 9 reviewers, 3 Nit, 1 Note, 1 P0, 2 P3, 3 P4, REQUEST_CHANGES. Review
  • R3 (2026-06-29): 7 reviewers, 3 Nit, 1 Note, 1 P0, 2 P3, 3 P4, APPROVE. Review

deep-review v0.9.0 | Round 3 | 877bbf0..785ef29

Last posted: Round 3, 10 findings (1 P0, 2 P3, 3 P4, 3 Nit, 1 Note), APPROVE. Review

Finding inventory

Findings

# Sev Status Location Summary Round Reviewer Posted
CRF-1 P3 Author fixed (6e39e44) chats.sql:921 NULLIF(@cost_source, '') lets empty-string caller silently produce NULL cost_source, defeating the trigger guard R1 Netero Yes
CRF-2 Note Author fixed (b2f6a28 merge) querier_test.go / trigger_test.go DB integration tests fail due to duplicate migration 000530 from stacked PR base branch R1 Netero Yes
CRF-3 Nit Author fixed (b2f6a28 merge) chatd.go:2932 Call-site comment restates InsertChatAccountingMessage doc comment R1 Gon Yes
CRF-4 Nit Author fixed (b2f6a28 merge) chatd.go:5157 Call-site comment restates InsertChatAccountingMessage doc comment R1 Gon Yes
CRF-5 P4 Author fixed (6e39e44) chatd.go:5161 recordChatSummaryUsage has zero mock test coverage for InsertChatAccountingMessage wiring R1 Bisky Yes
CRF-6 P4 Author fixed (b2f6a28 merge) chatd.go:5161 Duplicate 20-field InsertChatAccountingMessageParams struct literal across two callers R1 Zoro Yes
CRF-7 P4 Author accepted R2 (out of scope; CHECK constraint added, UPDATE-to-NULL gap remains application-enforced) 000531_up.sql:22 cost_source discriminator has no schema-level constraint; invariant relies on application discipline R1 Hisoka P4, Meruem Note, Pariston Note Yes
CRF-8 Nit Author accepted R2 (follow-up commit uses correct scope; original is git history) (commit) Commit scope coderd/x/chatd does not contain all changed files R1 Leorio, Mafu-san Yes
CRF-9 P0 Author fixed (785ef29) dbauthz.go:5858 InsertChatAccountingMessage panics with "not implemented" in dbauthz; every production call hits it R2 Netero P0, Mafu-san P1 Yes
CRF-10 P3 Author fixed (785ef29) ARCHITECTURE.md:262 Doc falsely claims snapshot_version advances on accounting inserts; only LockChatAndBumpSnapshotVersion increments it R2 Mafuuu P3, Knuckle P3 Yes

Contested and acknowledged

CRF-7 (P4, 000535_up.sql:32) - cost_source discriminator has no schema-level constraint

  • Finding: The cost_source IS NULL discriminator that the history triggers depend on has no schema-level enforcement preventing it from being cleared to NULL via UPDATE.
  • Author defense: Acknowledged as out of scope. The migration now adds CHECK (cost_source IN ('summary', 'title')) which constrains valid values, partially addressing the concern. The remaining gap (preventing UPDATE to NULL on existing rows) is a separate change.
  • Author accepted R2: The CHECK constraint prevents invalid non-NULL values. The UPDATE-to-NULL gap remains application-enforced. Author noted the corrective action for future reference.

CRF-8 (Nit, commit) - Commit scope too narrow

  • Finding: Commit scope coderd/x/chatd does not contain all changed files; AGENTS.md requires scope to contain every changed file.
  • Author defense: The follow-up review-fix commit uses fix(coderd): ... with the correct scope. The original commit is part of existing git history.
  • Author accepted R2: Correct scope used on new commits. Original history preserved.

Round log

Round 1

Panel. Netero + 15 reviewers (bisky, chopper, ging-go, gon, hisoka, kite, knuckle, komugi, leorio, mafu-san, mafuuu, meruem, pariston, knov, zoro). 1 P3, 2 Nit, 3 P4, 1 Note. Reviewed against 0bc176d..9ad21cb.

Round 2

Churn guard: PROCEED. 6 addressed, 2 acknowledged, 0 silent. Panel (bisky, hisoka, mafu-san, mafuuu, pariston, kite, knuckle, komugi, knov). 1 P0, 1 P3 new. Reviewed against 877bbf0..2cf46f3.

Round 3

Churn guard: PROCEED. 2 addressed, 0 silent. Panel (bisky, hisoka, mafu-san, mafuuu, pariston, kite, knov). 0 new findings. All 10 findings resolved or acknowledged. CI green. Reviewed against 877bbf0..785ef29.

About deep-review

CRF = Coder Review Finding (P0-P4, Nit, Note)

Reviewer Focus
Bisky tests
Chopper ops/errors
Churn-guard change verification
Ging language modernization
Gon naming
Hisoka edge cases
Killua perf
Kite change integrity
Knov contracts
Knuckle SQL
Komugi flake/determinism
Kurapika security
Law decomposition
Leorio docs
Luffy product
Mafu-san process
Mafuuu contracts
Melody dispatch/pairing
Meruem structural
Nami frontend
Netero mechanical checks
Pariston premise testing
Pen-botter product gaps
Razor verification
Robin duplication
Ryosuke Go arch
Takumi concurrency
Zoro shape

🤖 Managed by Coder Agents.

@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: 9ad21cb1e2

ℹ️ 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 coder-agents-review Bot 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.

Clean, well-targeted fix. The root cause analysis is correct: accounting rows were advancing history_version because (1) the AFTER STATEMENT triggers had no filter for non-history rows and (2) the two-step insert made the trigger blind to cost_source. The fix addresses both: trigger filter on cost_source IS NULL and atomic insert via InsertChatAccountingMessage. The removal of the last_model_config_id restoration is a structural simplification, not cosmetic.

Severity breakdown: 1 P3, 3 P4, 2 Nit, 1 Note.

Knuckle confirmed the migration is safe for online deployment: CREATE OR REPLACE FUNCTION rewrites pg_proc without acquiring ACCESS EXCLUSIVE on chat_messages or chats. No table rewrite, no lock queue.

Pariston traced the full causal chain and evaluated four alternative framings (trigger filter gap, two-step atomicity, guard scope mismatch, separate accounting table). The chosen approach (framings 1+2) is proportional. Pariston: "I tried to build a case against this and couldn't."

The trigger test (TestAccountingMessageDoesNotAdvanceHistoryVersion) is well-structured with three independent phases and a positive control. Both mock tests pass. DB integration tests cannot run due to the duplicate migration 000530 from the stacked base branch (#26657); this resolves when that PR renumbers before merge.

Leorio, on the migration header comment: "A developer touching this trigger a year from now gets the full diagnosis without the investigation. Every migration and query comment in this codebase should read like these."

🤖 This review was automatically generated with Coder Agents.

Comment thread coderd/database/queries/chats.sql Outdated
Comment thread coderd/x/chatd/chatd.go Outdated
Comment thread coderd/x/chatd/chatd.go Outdated
Comment thread coderd/x/chatd/chatd.go Outdated
Comment thread coderd/x/chatd/chatd.go Outdated
Comment thread coderd/database/querier_test.go Outdated
Comment thread coderd/database/dbauthz/dbauthz.go Outdated
jaaydenh added 3 commits June 29, 2026 06:59
# 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.
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.

Copy link
Copy Markdown
Contributor Author

Merge conflict resolved + review feedback addressed

Conflict resolution (merge b2f6a28): chat-summary-62j9 had extracted a shared recordHiddenUsageMessageTx helper while this branch rewrote the same call sites to use the atomic InsertChatAccountingMessage. Resolved by keeping the shared helper and reimplementing its body on top of InsertChatAccountingMessage (dropping the removed UpdateChatMessageCostSource two-step and the now-unnecessary last_model_config_id restore). Both recordManualTitleUsage and recordChatSummaryUsage delegate to it. The accounting migration was renumbered 000531 -> 000535 so it runs after 000534_chat_summary (which adds the cost_source column the recreated triggers reference).

Review findings (fixes in 6e39e44):

Finding Resolution
P1 (Codex) docs Documented the accounting-row history_version exception in ARCHITECTURE.md
CRF-1 (P3) NULLIF cost_source stored verbatim; empty value now fails the CHECK; added DB assertion
CRF-2 (Note) migration dup Renumbered to 000535; both DB tests now run and pass locally
CRF-3 / CRF-4 (Nit) comments Removed via the shared helper; rationale lives on the helper + query
CRF-5 (P4) summary test Added TestRecordChatSummaryUsage_InsertsAccountingRow
CRF-6 (P4) duplication Single struct literal now owned by recordHiddenUsageMessageTx
CRF-7 (P4) schema guard Acknowledged, left out of scope (noted on the thread)
CRF-8 (Nit) commit scope Review-fix commit uses coderd scope

make gen is clean and make pre-commit passes; targeted mock + Postgres tests pass locally.

jaaydenh and others added 3 commits June 29, 2026 10:02
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]>
Stack on the simplified chat-summary branch that defers cost accounting.
This PR adds cost_source, InsertChatAccountingMessage, and migration 000535
recreating history triggers so only ordinary turn rows advance
history_version. Summary and title usage writers record spend via the
atomic accounting insert so background summary persistence is not
invalidated by its own usage row.

Co-authored-by: Cursor <[email protected]>
@jaaydenh
jaaydenh force-pushed the fix-chat-history-accounting branch from b13db9f to 2cf46f3 Compare June 29, 2026 12:04
@jaaydenh

Copy link
Copy Markdown
Contributor Author

/coder-agents-review
@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: 2cf46f3266

ℹ️ 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/dbauthz/dbauthz.go Outdated

@coder-agents-review coder-agents-review Bot 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.

All R1 findings addressed or acknowledged. Good work on the shared recordHiddenUsageMessageTx helper (resolves CRF-3, CRF-4, CRF-6), the CHECK constraint on cost_source (resolves CRF-1), and the new summary path test (resolves CRF-5).

This round found one P0 and one P3.

The base branch merge overwrote the InsertChatAccountingMessage dbauthz implementation with a panic("not implemented") stub (CRF-9). The dbauthz test is also missing. This affects every production call path. The mock tests pass because they bypass dbauthz.

Note: the PR description's "verified against a dev instance" claim likely did not exercise the cost attribution path. If the dev model returned zero usage, the if usage != (fantasy.Usage{}) guard at chatd.go:5022 skips recordChatSummaryUsage entirely, so the panic was never hit. After fixing the dbauthz implementation, consider verifying that usage recording is exercised end-to-end.

Pariston: "I tried to build a case against this change and could not. The problem is correctly diagnosed. The two root causes (trigger not filtering, cost_source set after INSERT) are both real and both addressed."

🤖 This review was automatically generated with Coder Agents.

Comment thread coderd/database/dbauthz/dbauthz.go
Comment thread coderd/x/chatd/ARCHITECTURE.md Outdated
jaaydenh added a commit that referenced this pull request Jun 29, 2026
…nscript

The detached background summary goroutine re-reads the chat and transcript
fresh, but resolveChatSummaryModel still used the ModelBuildOptions captured
from the turn that launched the goroutine. In AI Gateway deployments that
ActiveAPIKeyID can be stale or belong to a different turn once a later turn
commits first, so the summary call could fail with a missing key or be
attributed to the wrong key.

Derive modelBuildOptionsFromMessages(messages) after loading the transcript
and thread the active turn API key onto the context, mirroring
deriveFinalTurnRunResult and the generation path. With this, runResult is no
longer needed by the summary helpers, so drop the parameter.

Also fix the generateAndStoreChatSummary doc comment (cost recording is
deferred to #26689) and add a shouldGenerateChatSummary subtest that pins the
countCompletedTurnsSince time-filter guard against pre-marker turns.
The base-branch merge overwrote the InsertChatAccountingMessage dbauthz
method with a panic("not implemented") stub. Production paths are
dbauthz-wrapped, so recordChatSummaryUsage and recordManualTitleUsage
would panic at runtime. Restore the authorization (authorize update on
the parent chat, delegating to the underlying store) and the dbauthz
test.

Also correct the snapshot_version note in ARCHITECTURE.md and the 000535
migration comment: accounting inserts do not bump snapshot_version, so an
accounting-only write leaves both version columns unchanged.

Copy link
Copy Markdown
Contributor Author

/coder-agents-review

Copy link
Copy Markdown
Contributor 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: 785ef29099

ℹ️ 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/x/chatd/chatd.go
}

model, _, ok := p.resolveChatSummaryModel(authCtx, chat, runResult, logger)
model, modelConfig, ok := p.resolveChatSummaryModel(authCtx, chat, runResult, logger)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Derive summary key from the transcript before calling

In AI Gateway chats with rapid back-to-back turns, this resolves the summary model with runResult.ModelBuildOptions from the turn that scheduled the goroutine, while the new accounting code records usage against the active key found in the freshly loaded messages. If a newer turn committed before this goroutine ran, the summary request is sent with the old delegated key but the usage row is attributed to the new key, which can fail routing or misattribute spend. Derive the model options from the same loaded transcript and use that key for both the model and recordChatSummaryUsage.

Useful? React with 👍 / 👎.

@coder-agents-review coder-agents-review Bot 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.

All 10 findings from rounds 1 and 2 are resolved. CI is green. No new findings from Netero or the R3 panel (7 reviewers).

CRF-9 (P0, dbauthz panic) verified fixed: InsertChatAccountingMessage now authorizes ActionUpdate on the parent chat, matching the established pattern. CRF-10 (P3, ARCHITECTURE.md inaccuracy) verified fixed: doc no longer claims snapshot_version advances on accounting inserts.

Mafu-san: "Fixed siblings without being asked. CRF-10 flagged a false snapshot_version claim in ARCHITECTURE.md. The R3 fix corrected it in both ARCHITECTURE.md and the migration comment, which contained the same false claim but was not called out separately."

This is a clean, well-targeted fix. The root cause is correctly diagnosed, the solution is proportional, and the test coverage spans all three layers (trigger, query, application). The recordHiddenUsageMessageTx helper is a structural improvement over the old duplicated 4-step pattern.

🤖 This review was automatically generated with Coder Agents.

Trim the verbose, duplicated rationale comments added for the chat
accounting work down to short, single-purpose comments. The full
explanation of why accounting rows must not advance history_version now
lives once in coderd/x/chatd/ARCHITECTURE.md; the code, SQL, migration,
and test comments keep only the non-obvious essentials.
@github-actions github-actions Bot added the stale This issue is like stale bread. label Jul 8, 2026
@github-actions github-actions Bot closed this Jul 12, 2026
@jaaydenh jaaydenh reopened this Jul 12, 2026
@github-actions github-actions Bot removed the stale This issue is like stale bread. label Jul 13, 2026
@github-actions github-actions Bot added the stale This issue is like stale bread. label Jul 20, 2026
Base automatically changed from chat-summary-62j9 to main July 23, 2026 15:36
@github-actions github-actions Bot removed the stale This issue is like stale bread. label Jul 24, 2026
@github-actions github-actions Bot added the stale This issue is like stale bread. label Jul 31, 2026
@github-actions github-actions Bot closed this Aug 3, 2026
@jaaydenh jaaydenh reopened this Aug 3, 2026
@github-actions github-actions Bot removed the stale This issue is like stale bread. label Aug 4, 2026
@github-actions github-actions Bot added the stale This issue is like stale bread. label Aug 18, 2026
@github-actions github-actions Bot closed this Aug 21, 2026
@jaaydenh jaaydenh reopened this Aug 22, 2026
@github-actions github-actions Bot removed the stale This issue is like stale bread. label Aug 23, 2026
@github-actions github-actions Bot added the stale This issue is like stale bread. label Aug 30, 2026
@github-actions github-actions Bot closed this Sep 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stale This issue is like stale bread.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant