feat: add persisted whole-chat summary with background generation (#26657)

Adds a persisted whole-chat summary that backs the chat summary popover.
A new nullable `chats.summary` column is populated in the background
after a successful root-chat turn and pushed to clients via a new
`chat_summary_change` watch event (distinct from `summary_change`, which
is bound to `last_turn_summary`), so the popover reads `chat.summary`
straight off the loaded `Chat` with no extra query.

This is the data source for the popover and per-chat cost UI built in
#26649; the popover can consume `chat.summary` once this lands (the
field is nullable, so merge order does not matter).

## How it works

- **Generation** runs in the existing successful-turn finalize hook,
detached from the request so the user's turn is never blocked. A cadence
gate generates the first summary after one completed turn, then
regenerates every three turns, using the `chats.summary_generated_at`
freshness 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.
- **Staleness** is guarded by `history_version` (mirroring
`last_turn_summary`), so a background write racing a newer turn loses
while worker lifecycle transitions cannot reject a fresh write.
- **Model selection** uses the chat's configured model.

## Deferred to follow-ups

- **Cost accounting**: the `chat_messages.cost_source` discriminator and
summary/title usage recording were removed from this PR so summary
persistence is not blocked by hidden accounting rows advancing
`history_version`. Title usage recording stays on main's
`InsertChatMessages` path.
- **Model override**: deployment-wide summary generation model selection
is split into #26803; the base feature always uses the chat model.

## Notes

- Migration `000540` adds `chats.summary` and
`chats.summary_generated_at`, and recreates `chats_expanded` to expose
the new columns.
- Root chats only; shared viewers pick up the summary on their next
refetch (live watch events are owner-only).

Refs #26649

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jaayden Halko
2026-07-23 16:36:23 +01:00
committed by GitHub
co-authored by Cursor
parent 9ce366414e
commit 3c7a1d33e3
33 changed files with 1473 additions and 120 deletions
+5 -1
View File
@@ -4970,6 +4970,8 @@ type Chat struct {
PlanMode NullChatPlanMode `db:"plan_mode" json:"plan_mode"`
ClientType ChatClientType `db:"client_type" json:"client_type"`
LastTurnSummary sql.NullString `db:"last_turn_summary" json:"last_turn_summary"`
Summary sql.NullString `db:"summary" json:"summary"`
SummaryGeneratedAt sql.NullTime `db:"summary_generated_at" json:"summary_generated_at"`
SnapshotVersion int64 `db:"snapshot_version" json:"snapshot_version"`
HistoryVersion int64 `db:"history_version" json:"history_version"`
QueueVersion int64 `db:"queue_version" json:"queue_version"`
@@ -5209,7 +5211,9 @@ type ChatTable struct {
// Stores the most recent message effort once per-turn selection is wired.
LastReasoningEffort NullChatReasoningEffort `db:"last_reasoning_effort" json:"last_reasoning_effort"`
// Set when the chat owner manually requests a context compaction. One-shot signal: consumed by the compaction commit and cleared whenever the chat leaves running.
CompactionRequestedAt sql.NullTime `db:"compaction_requested_at" json:"compaction_requested_at"`
CompactionRequestedAt sql.NullTime `db:"compaction_requested_at" json:"compaction_requested_at"`
Summary sql.NullString `db:"summary" json:"summary"`
SummaryGeneratedAt sql.NullTime `db:"summary_generated_at" json:"summary_generated_at"`
}
type ChatUsageLimitConfig struct {