diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go
index 69411ed4491..1bf1376cfc9 100644
--- a/coderd/apidoc/docs.go
+++ b/coderd/apidoc/docs.go
@@ -17012,6 +17012,10 @@ const docTemplate = `{
"status": {
"$ref": "#/definitions/codersdk.ChatStatus"
},
+ "summary": {
+ "description": "Summary is the persisted whole-chat summary, generated in the background.\nIt is nil until the first summary has been produced.",
+ "type": "string"
+ },
"title": {
"type": "string"
},
@@ -18143,6 +18147,7 @@ const docTemplate = `{
"enum": [
"status_change",
"summary_change",
+ "chat_summary_change",
"title_change",
"created",
"deleted",
@@ -18153,6 +18158,7 @@ const docTemplate = `{
"x-enum-varnames": [
"ChatWatchEventKindStatusChange",
"ChatWatchEventKindSummaryChange",
+ "ChatWatchEventKindChatSummaryChange",
"ChatWatchEventKindTitleChange",
"ChatWatchEventKindCreated",
"ChatWatchEventKindDeleted",
diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json
index 57d188b92b2..eafa2e787cc 100644
--- a/coderd/apidoc/swagger.json
+++ b/coderd/apidoc/swagger.json
@@ -15293,6 +15293,10 @@
"status": {
"$ref": "#/definitions/codersdk.ChatStatus"
},
+ "summary": {
+ "description": "Summary is the persisted whole-chat summary, generated in the background.\nIt is nil until the first summary has been produced.",
+ "type": "string"
+ },
"title": {
"type": "string"
},
@@ -16371,6 +16375,7 @@
"enum": [
"status_change",
"summary_change",
+ "chat_summary_change",
"title_change",
"created",
"deleted",
@@ -16381,6 +16386,7 @@
"x-enum-varnames": [
"ChatWatchEventKindStatusChange",
"ChatWatchEventKindSummaryChange",
+ "ChatWatchEventKindChatSummaryChange",
"ChatWatchEventKindTitleChange",
"ChatWatchEventKindCreated",
"ChatWatchEventKindDeleted",
diff --git a/coderd/database/db2sdk/db2sdk.go b/coderd/database/db2sdk/db2sdk.go
index de2de7586c6..d0f94b71d1b 100644
--- a/coderd/database/db2sdk/db2sdk.go
+++ b/coderd/database/db2sdk/db2sdk.go
@@ -1750,6 +1750,9 @@ func Chat(c database.Chat, diffStatus *database.ChatDiffStatus, files []database
if c.LastTurnSummary.Valid {
chat.LastTurnSummary = &c.LastTurnSummary.String
}
+ if c.Summary.Valid {
+ chat.Summary = &c.Summary.String
+ }
if c.LastReasoningEffort.Valid {
lastReasoningEffort := string(c.LastReasoningEffort.ChatReasoningEffort)
chat.LastReasoningEffort = &lastReasoningEffort
diff --git a/coderd/database/db2sdk/db2sdk_test.go b/coderd/database/db2sdk/db2sdk_test.go
index 44d43442d20..69a1d7c9bac 100644
--- a/coderd/database/db2sdk/db2sdk_test.go
+++ b/coderd/database/db2sdk/db2sdk_test.go
@@ -713,6 +713,7 @@ func TestChat_AllFieldsPopulated(t *testing.T) {
ClientType: database.ChatClientTypeUi,
LastError: pqtype.NullRawMessage{RawMessage: lastErrorRaw, Valid: true},
LastTurnSummary: sql.NullString{String: "turn completed", Valid: true},
+ Summary: sql.NullString{String: "summarized the whole chat", Valid: true},
CreatedAt: now,
UpdatedAt: now,
Archived: true,
diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go
index 3af34d6b365..879e53e799f 100644
--- a/coderd/database/dbauthz/dbauthz.go
+++ b/coderd/database/dbauthz/dbauthz.go
@@ -7452,6 +7452,17 @@ func (q *querier) UpdateChatStatus(ctx context.Context, arg database.UpdateChatS
return q.db.UpdateChatStatus(ctx, arg)
}
+func (q *querier) UpdateChatSummary(ctx context.Context, arg database.UpdateChatSummaryParams) (int64, error) {
+ chat, err := q.db.GetChatByID(ctx, arg.ID)
+ if err != nil {
+ return 0, err
+ }
+ if err := q.authorizeContext(ctx, policy.ActionUpdate, chat); err != nil {
+ return 0, err
+ }
+ return q.db.UpdateChatSummary(ctx, arg)
+}
+
func (q *querier) UpdateChatTitleByID(ctx context.Context, arg database.UpdateChatTitleByIDParams) (database.Chat, error) {
chat, err := q.db.GetChatByID(ctx, arg.ID)
if err != nil {
diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go
index 8f48b870bef..261170057a2 100644
--- a/coderd/database/dbauthz/dbauthz_test.go
+++ b/coderd/database/dbauthz/dbauthz_test.go
@@ -1917,6 +1917,17 @@ func (s *MethodTestSuite) TestChats() {
dbm.EXPECT().UpdateChatLastTurnSummary(gomock.Any(), arg).Return(int64(1), nil).AnyTimes()
check.Args(arg).Asserts(chat, policy.ActionUpdate).Returns(int64(1))
}))
+ s.Run("UpdateChatSummary", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
+ chat := testutil.Fake(s.T(), faker, database.Chat{})
+ arg := database.UpdateChatSummaryParams{
+ ID: chat.ID,
+ ExpectedHistoryVersion: chat.HistoryVersion,
+ Summary: sql.NullString{String: "summarized the whole chat", Valid: true},
+ }
+ dbm.EXPECT().GetChatByID(gomock.Any(), chat.ID).Return(chat, nil).AnyTimes()
+ dbm.EXPECT().UpdateChatSummary(gomock.Any(), arg).Return(int64(1), nil).AnyTimes()
+ check.Args(arg).Asserts(chat, policy.ActionUpdate).Returns(int64(1))
+ }))
s.Run("UpdateChatLastReadMessageID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
chat := testutil.Fake(s.T(), faker, database.Chat{})
arg := database.UpdateChatLastReadMessageIDParams{
diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go
index 16bfc7c80f1..37d8cc9641f 100644
--- a/coderd/database/dbmetrics/querymetrics.go
+++ b/coderd/database/dbmetrics/querymetrics.go
@@ -5305,6 +5305,14 @@ func (m queryMetricsStore) UpdateChatStatus(ctx context.Context, arg database.Up
return r0, r1
}
+func (m queryMetricsStore) UpdateChatSummary(ctx context.Context, arg database.UpdateChatSummaryParams) (int64, error) {
+ start := time.Now()
+ r0, r1 := m.s.UpdateChatSummary(ctx, arg)
+ m.queryLatencies.WithLabelValues("UpdateChatSummary").Observe(time.Since(start).Seconds())
+ m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpdateChatSummary").Inc()
+ return r0, r1
+}
+
func (m queryMetricsStore) UpdateChatTitleByID(ctx context.Context, arg database.UpdateChatTitleByIDParams) (database.Chat, error) {
start := time.Now()
r0, r1 := m.s.UpdateChatTitleByID(ctx, arg)
diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go
index 453a8798dbb..845b3c279d4 100644
--- a/coderd/database/dbmock/dbmock.go
+++ b/coderd/database/dbmock/dbmock.go
@@ -9996,6 +9996,21 @@ func (mr *MockStoreMockRecorder) UpdateChatStatus(ctx, arg any) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateChatStatus", reflect.TypeOf((*MockStore)(nil).UpdateChatStatus), ctx, arg)
}
+// UpdateChatSummary mocks base method.
+func (m *MockStore) UpdateChatSummary(ctx context.Context, arg database.UpdateChatSummaryParams) (int64, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "UpdateChatSummary", ctx, arg)
+ ret0, _ := ret[0].(int64)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// UpdateChatSummary indicates an expected call of UpdateChatSummary.
+func (mr *MockStoreMockRecorder) UpdateChatSummary(ctx, arg any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateChatSummary", reflect.TypeOf((*MockStore)(nil).UpdateChatSummary), ctx, arg)
+}
+
// UpdateChatTitleByID mocks base method.
func (m *MockStore) UpdateChatTitleByID(ctx context.Context, arg database.UpdateChatTitleByIDParams) (database.Chat, error) {
m.ctrl.T.Helper()
diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql
index b7cd44e1dbf..90bf283c179 100644
--- a/coderd/database/dump.sql
+++ b/coderd/database/dump.sql
@@ -2097,6 +2097,8 @@ CREATE TABLE chats (
context_error text DEFAULT ''::text NOT NULL,
last_reasoning_effort chat_reasoning_effort,
compaction_requested_at timestamp with time zone,
+ summary text,
+ summary_generated_at timestamp with time zone,
CONSTRAINT chat_acl_only_on_root_chats CHECK ((((parent_chat_id IS NULL) AND (root_chat_id IS NULL)) OR ((user_acl = '{}'::jsonb) AND (group_acl = '{}'::jsonb)))),
CONSTRAINT chat_group_acl_not_null_jsonb CHECK (((group_acl IS NOT NULL) AND (jsonb_typeof(group_acl) = 'object'::text))),
CONSTRAINT chat_user_acl_not_null_jsonb CHECK (((user_acl IS NOT NULL) AND (jsonb_typeof(user_acl) = 'object'::text))),
@@ -2202,6 +2204,8 @@ CREATE VIEW chats_expanded AS
c.plan_mode,
c.client_type,
c.last_turn_summary,
+ c.summary,
+ c.summary_generated_at,
c.snapshot_version,
c.history_version,
c.queue_version,
diff --git a/coderd/database/migrations/000551_chat_summary.down.sql b/coderd/database/migrations/000551_chat_summary.down.sql
new file mode 100644
index 00000000000..4b56bc9b93a
--- /dev/null
+++ b/coderd/database/migrations/000551_chat_summary.down.sql
@@ -0,0 +1,57 @@
+-- Drop the view before the columns it references, then recreate it without
+-- the summary columns, matching the 000549 chats_expanded definition.
+DROP VIEW IF EXISTS chats_expanded;
+
+ALTER TABLE chats
+ DROP COLUMN summary,
+ DROP COLUMN summary_generated_at;
+
+CREATE VIEW chats_expanded AS
+ SELECT c.id,
+ c.owner_id,
+ c.workspace_id,
+ c.title,
+ c.status,
+ c.worker_id,
+ c.started_at,
+ c.heartbeat_at,
+ c.created_at,
+ c.updated_at,
+ c.parent_chat_id,
+ c.root_chat_id,
+ c.last_model_config_id,
+ c.last_reasoning_effort,
+ c.archived,
+ c.last_error,
+ c.mode,
+ c.mcp_server_ids,
+ c.labels,
+ c.build_id,
+ c.agent_id,
+ c.pin_order,
+ c.last_read_message_id,
+ c.dynamic_tools,
+ c.organization_id,
+ c.plan_mode,
+ c.client_type,
+ c.last_turn_summary,
+ c.snapshot_version,
+ c.history_version,
+ c.queue_version,
+ c.generation_attempt,
+ c.retry_state,
+ c.retry_state_version,
+ c.runner_id,
+ c.requires_action_deadline_at,
+ COALESCE(root.user_acl, c.user_acl) AS user_acl,
+ COALESCE(root.group_acl, c.group_acl) AS group_acl,
+ owner.username AS owner_username,
+ owner.name AS owner_name,
+ c.context_aggregate_hash,
+ c.context_dirty_since,
+ c.context_dirty_resources,
+ c.context_error,
+ c.compaction_requested_at
+ FROM ((chats c
+ LEFT JOIN chats root ON ((root.id = COALESCE(c.root_chat_id, c.parent_chat_id))))
+ JOIN visible_users owner ON ((owner.id = c.owner_id)));
diff --git a/coderd/database/migrations/000551_chat_summary.up.sql b/coderd/database/migrations/000551_chat_summary.up.sql
new file mode 100644
index 00000000000..3b3e8a407ff
--- /dev/null
+++ b/coderd/database/migrations/000551_chat_summary.up.sql
@@ -0,0 +1,60 @@
+-- Persisted whole-chat summary and its freshness marker, distinct from
+-- last_turn_summary (which only reflects the most recent turn).
+ALTER TABLE chats
+ ADD COLUMN summary TEXT,
+ ADD COLUMN summary_generated_at TIMESTAMPTZ;
+
+-- Recreate chats_expanded: its explicit column list hides new columns otherwise.
+DROP VIEW IF EXISTS chats_expanded;
+
+CREATE VIEW chats_expanded AS
+ SELECT c.id,
+ c.owner_id,
+ c.workspace_id,
+ c.title,
+ c.status,
+ c.worker_id,
+ c.started_at,
+ c.heartbeat_at,
+ c.created_at,
+ c.updated_at,
+ c.parent_chat_id,
+ c.root_chat_id,
+ c.last_model_config_id,
+ c.last_reasoning_effort,
+ c.archived,
+ c.last_error,
+ c.mode,
+ c.mcp_server_ids,
+ c.labels,
+ c.build_id,
+ c.agent_id,
+ c.pin_order,
+ c.last_read_message_id,
+ c.dynamic_tools,
+ c.organization_id,
+ c.plan_mode,
+ c.client_type,
+ c.last_turn_summary,
+ c.summary,
+ c.summary_generated_at,
+ c.snapshot_version,
+ c.history_version,
+ c.queue_version,
+ c.generation_attempt,
+ c.retry_state,
+ c.retry_state_version,
+ c.runner_id,
+ c.requires_action_deadline_at,
+ COALESCE(root.user_acl, c.user_acl) AS user_acl,
+ COALESCE(root.group_acl, c.group_acl) AS group_acl,
+ owner.username AS owner_username,
+ owner.name AS owner_name,
+ c.context_aggregate_hash,
+ c.context_dirty_since,
+ c.context_dirty_resources,
+ c.context_error,
+ c.compaction_requested_at
+ FROM ((chats c
+ LEFT JOIN chats root ON ((root.id = COALESCE(c.root_chat_id, c.parent_chat_id))))
+ JOIN visible_users owner ON ((owner.id = c.owner_id)));
diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go
index d0503fc1d97..e7323bd6504 100644
--- a/coderd/database/modelqueries.go
+++ b/coderd/database/modelqueries.go
@@ -826,6 +826,8 @@ func (q *sqlQuerier) GetAuthorizedChats(ctx context.Context, arg GetChatsParams,
&i.Chat.PlanMode,
&i.Chat.ClientType,
&i.Chat.LastTurnSummary,
+ &i.Chat.Summary,
+ &i.Chat.SummaryGeneratedAt,
&i.Chat.SnapshotVersion,
&i.Chat.HistoryVersion,
&i.Chat.QueueVersion,
@@ -906,6 +908,8 @@ func (q *sqlQuerier) GetAuthorizedChatsByChatFileID(ctx context.Context, fileID
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
+ &i.Summary,
+ &i.SummaryGeneratedAt,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
diff --git a/coderd/database/models.go b/coderd/database/models.go
index 7a96121b021..08b71a244ef 100644
--- a/coderd/database/models.go
+++ b/coderd/database/models.go
@@ -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 {
diff --git a/coderd/database/querier.go b/coderd/database/querier.go
index 8e9b909de43..c637c654c7e 100644
--- a/coderd/database/querier.go
+++ b/coderd/database/querier.go
@@ -1426,6 +1426,9 @@ type sqlcQuerier interface {
// assigned by trigger from the current snapshot_version.
UpdateChatRetryState(ctx context.Context, arg UpdateChatRetryStateParams) (Chat, error)
UpdateChatStatus(ctx context.Context, arg UpdateChatStatusParams) (Chat, error)
+ // The history_version fence lets background summary writes ignore worker-only
+ // updates while losing to newer message history.
+ UpdateChatSummary(ctx context.Context, arg UpdateChatSummaryParams) (int64, error)
UpdateChatTitleByID(ctx context.Context, arg UpdateChatTitleByIDParams) (Chat, error)
UpdateChatWorkspaceBinding(ctx context.Context, arg UpdateChatWorkspaceBindingParams) (Chat, error)
UpdateCryptoKeyDeletesAt(ctx context.Context, arg UpdateCryptoKeyDeletesAtParams) (CryptoKey, error)
diff --git a/coderd/database/querier_test.go b/coderd/database/querier_test.go
index d0467a86b3e..305cf43a1cb 100644
--- a/coderd/database/querier_test.go
+++ b/coderd/database/querier_test.go
@@ -14699,6 +14699,128 @@ func TestUpdateChatLastTurnSummary(t *testing.T) {
require.NotEqual(t, chat.HistoryVersion, fetched.HistoryVersion)
}
+func TestUpdateChatSummary(t *testing.T) {
+ t.Parallel()
+ if testing.Short() {
+ t.SkipNow()
+ }
+
+ sqlDB := testSQLDB(t)
+ err := migrations.Up(sqlDB)
+ require.NoError(t, err)
+ db := database.New(sqlDB)
+
+ owner := dbgen.User(t, db, database.User{})
+ org := dbgen.Organization(t, db, database.Organization{})
+ dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: owner.ID, OrganizationID: org.ID})
+
+ dbgen.ChatProvider(t, db, database.ChatProvider{
+ Provider: "openai",
+ DisplayName: "OpenAI",
+ APIKey: "test-key",
+ Enabled: true,
+ CentralApiKeyEnabled: true,
+ })
+
+ ctx := testutil.Context(t, testutil.WaitMedium)
+ modelCfg, err := insertChatModelConfigForTest(ctx, t, db, "openai", database.InsertChatModelConfigParams{
+ Model: "test-model",
+ DisplayName: "Test Model",
+ CreatedBy: uuid.NullUUID{UUID: owner.ID, Valid: true},
+ UpdatedBy: uuid.NullUUID{UUID: owner.ID, Valid: true},
+ Enabled: true,
+ IsDefault: true,
+ ContextLimit: 128000,
+ CompressionThreshold: 80,
+ Options: json.RawMessage(`{}`),
+ })
+ require.NoError(t, err)
+
+ chat, err := db.InsertChat(ctx, database.InsertChatParams{
+ OrganizationID: org.ID,
+ Status: database.ChatStatusWaiting,
+ ClientType: database.ChatClientTypeUi,
+ OwnerID: owner.ID,
+ LastModelConfigID: modelCfg.ID,
+ Title: "summary-chat",
+ })
+ require.NoError(t, err)
+ require.False(t, chat.Summary.Valid)
+ require.False(t, chat.SummaryGeneratedAt.Valid)
+
+ affected, err := db.UpdateChatSummary(ctx, database.UpdateChatSummaryParams{
+ ID: chat.ID,
+ ExpectedHistoryVersion: chat.HistoryVersion,
+ Summary: sql.NullString{String: "Implemented the whole-chat summary feature.", Valid: true},
+ })
+ require.NoError(t, err)
+ require.EqualValues(t, 1, affected)
+
+ fetched, err := db.GetChatByID(ctx, chat.ID)
+ require.NoError(t, err)
+ require.Equal(t, sql.NullString{String: "Implemented the whole-chat summary feature.", Valid: true}, fetched.Summary)
+ require.True(t, fetched.SummaryGeneratedAt.Valid)
+ require.Equal(t, chat.UpdatedAt, fetched.UpdatedAt)
+
+ affected, err = db.UpdateChatSummary(ctx, database.UpdateChatSummaryParams{
+ ID: chat.ID,
+ ExpectedHistoryVersion: chat.HistoryVersion,
+ Summary: sql.NullString{},
+ })
+ require.NoError(t, err)
+ require.EqualValues(t, 1, affected)
+
+ fetched, err = db.GetChatByID(ctx, chat.ID)
+ require.NoError(t, err)
+ require.False(t, fetched.Summary.Valid)
+ require.Equal(t, chat.UpdatedAt, fetched.UpdatedAt)
+
+ // Background summaries generated from stale history must lose to newer turns.
+ affected, err = db.UpdateChatSummary(ctx, database.UpdateChatSummaryParams{
+ ID: chat.ID,
+ ExpectedHistoryVersion: chat.HistoryVersion,
+ Summary: sql.NullString{String: "Fresh whole-chat summary.", Valid: true},
+ })
+ require.NoError(t, err)
+ require.EqualValues(t, 1, affected)
+
+ _, err = db.LockChatAndBumpSnapshotVersion(ctx, chat.ID)
+ require.NoError(t, err)
+ _, err = db.InsertChatMessages(ctx, database.InsertChatMessagesParams{
+ ChatID: chat.ID,
+ CreatedBy: []uuid.UUID{owner.ID},
+ ModelConfigID: []uuid.UUID{modelCfg.ID},
+ Role: []database.ChatMessageRole{database.ChatMessageRoleUser},
+ Content: []string{`[{"type":"text","text":"new request"}]`},
+ ContentVersion: []int16{chatprompt.CurrentContentVersion},
+ Visibility: []database.ChatMessageVisibility{database.ChatMessageVisibilityBoth},
+ InputTokens: []int64{0},
+ OutputTokens: []int64{0},
+ TotalTokens: []int64{0},
+ ReasoningTokens: []int64{0},
+ CacheCreationTokens: []int64{0},
+ CacheReadTokens: []int64{0},
+ ContextLimit: []int64{0},
+ Compressed: []bool{false},
+ TotalCostMicros: []int64{0},
+ RuntimeMs: []int64{0},
+ })
+ require.NoError(t, err)
+
+ affected, err = db.UpdateChatSummary(ctx, database.UpdateChatSummaryParams{
+ ID: chat.ID,
+ ExpectedHistoryVersion: chat.HistoryVersion,
+ Summary: sql.NullString{String: "Stale whole-chat summary.", Valid: true},
+ })
+ require.NoError(t, err)
+ require.Zero(t, affected)
+
+ fetched, err = db.GetChatByID(ctx, chat.ID)
+ require.NoError(t, err)
+ require.Equal(t, sql.NullString{String: "Fresh whole-chat summary.", Valid: true}, fetched.Summary)
+ require.NotEqual(t, chat.HistoryVersion, fetched.HistoryVersion)
+}
+
func TestUpdateChatWorkspaceBindingNoOp(t *testing.T) {
t.Parallel()
if testing.Short() {
diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go
index 76168e90363..b55924928ae 100644
--- a/coderd/database/queries.sql.go
+++ b/coderd/database/queries.sql.go
@@ -6050,7 +6050,7 @@ WITH updated_chats AS (
UPDATE chats
SET archived = true, pin_order = 0, updated_at = NOW()
WHERE id = $1::uuid OR root_chat_id = $1::uuid
- RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at
+ RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at, summary, summary_generated_at
),
chats_expanded AS (
SELECT
@@ -6082,6 +6082,8 @@ chats_expanded AS (
updated_chats.plan_mode,
updated_chats.client_type,
updated_chats.last_turn_summary,
+ updated_chats.summary,
+ updated_chats.summary_generated_at,
updated_chats.snapshot_version,
updated_chats.history_version,
updated_chats.queue_version,
@@ -6104,7 +6106,7 @@ chats_expanded AS (
LEFT JOIN chats root ON root.id = COALESCE(updated_chats.root_chat_id, updated_chats.parent_chat_id)
JOIN visible_users owner ON owner.id = updated_chats.owner_id
)
-SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
+SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, summary, summary_generated_at, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
FROM chats_expanded
ORDER BY (chats_expanded.id = $1::uuid) DESC, chats_expanded.created_at ASC, chats_expanded.id ASC
`
@@ -6147,6 +6149,8 @@ func (q *sqlQuerier) ArchiveChatByID(ctx context.Context, id uuid.UUID) ([]Chat,
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
+ &i.Summary,
+ &i.SummaryGeneratedAt,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
@@ -6214,10 +6218,10 @@ archived AS (
FROM to_archive t
WHERE (c.id = t.id OR c.root_chat_id = t.id) -- cascade to children
AND c.archived = false
- RETURNING c.id, c.owner_id, c.workspace_id, c.title, c.status, c.worker_id, c.started_at, c.heartbeat_at, c.created_at, c.updated_at, c.parent_chat_id, c.root_chat_id, c.last_model_config_id, c.archived, c.last_error, c.mode, c.mcp_server_ids, c.labels, c.build_id, c.agent_id, c.pin_order, c.last_read_message_id, c.dynamic_tools, c.organization_id, c.plan_mode, c.client_type, c.last_turn_summary, c.user_acl, c.group_acl, c.snapshot_version, c.history_version, c.queue_version, c.generation_attempt, c.retry_state, c.retry_state_version, c.runner_id, c.requires_action_deadline_at, c.context_aggregate_hash, c.context_dirty_since, c.context_dirty_resources, c.context_error, c.last_reasoning_effort, c.compaction_requested_at
+ RETURNING c.id, c.owner_id, c.workspace_id, c.title, c.status, c.worker_id, c.started_at, c.heartbeat_at, c.created_at, c.updated_at, c.parent_chat_id, c.root_chat_id, c.last_model_config_id, c.archived, c.last_error, c.mode, c.mcp_server_ids, c.labels, c.build_id, c.agent_id, c.pin_order, c.last_read_message_id, c.dynamic_tools, c.organization_id, c.plan_mode, c.client_type, c.last_turn_summary, c.user_acl, c.group_acl, c.snapshot_version, c.history_version, c.queue_version, c.generation_attempt, c.retry_state, c.retry_state_version, c.runner_id, c.requires_action_deadline_at, c.context_aggregate_hash, c.context_dirty_since, c.context_dirty_resources, c.context_error, c.last_reasoning_effort, c.compaction_requested_at, c.summary, c.summary_generated_at
)
SELECT
- a.id, a.owner_id, a.workspace_id, a.title, a.status, a.worker_id, a.started_at, a.heartbeat_at, a.created_at, a.updated_at, a.parent_chat_id, a.root_chat_id, a.last_model_config_id, a.archived, a.last_error, a.mode, a.mcp_server_ids, a.labels, a.build_id, a.agent_id, a.pin_order, a.last_read_message_id, a.dynamic_tools, a.organization_id, a.plan_mode, a.client_type, a.last_turn_summary, a.user_acl, a.group_acl, a.snapshot_version, a.history_version, a.queue_version, a.generation_attempt, a.retry_state, a.retry_state_version, a.runner_id, a.requires_action_deadline_at, a.context_aggregate_hash, a.context_dirty_since, a.context_dirty_resources, a.context_error, a.last_reasoning_effort, a.compaction_requested_at,
+ a.id, a.owner_id, a.workspace_id, a.title, a.status, a.worker_id, a.started_at, a.heartbeat_at, a.created_at, a.updated_at, a.parent_chat_id, a.root_chat_id, a.last_model_config_id, a.archived, a.last_error, a.mode, a.mcp_server_ids, a.labels, a.build_id, a.agent_id, a.pin_order, a.last_read_message_id, a.dynamic_tools, a.organization_id, a.plan_mode, a.client_type, a.last_turn_summary, a.user_acl, a.group_acl, a.snapshot_version, a.history_version, a.queue_version, a.generation_attempt, a.retry_state, a.retry_state_version, a.runner_id, a.requires_action_deadline_at, a.context_aggregate_hash, a.context_dirty_since, a.context_dirty_resources, a.context_error, a.last_reasoning_effort, a.compaction_requested_at, a.summary, a.summary_generated_at,
-- Children inherit their root's activity so last_activity_at is never null.
COALESCE(
t.last_activity_at,
@@ -6278,6 +6282,8 @@ type AutoArchiveInactiveChatsRow struct {
ContextError string `db:"context_error" json:"context_error"`
LastReasoningEffort NullChatReasoningEffort `db:"last_reasoning_effort" json:"last_reasoning_effort"`
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"`
LastActivityAt time.Time `db:"last_activity_at" json:"last_activity_at"`
}
@@ -6341,6 +6347,8 @@ func (q *sqlQuerier) AutoArchiveInactiveChats(ctx context.Context, arg AutoArchi
&i.ContextError,
&i.LastReasoningEffort,
&i.CompactionRequestedAt,
+ &i.Summary,
+ &i.SummaryGeneratedAt,
&i.LastActivityAt,
); err != nil {
return nil, err
@@ -6647,7 +6655,7 @@ func (q *sqlQuerier) DeleteStaleChatHeartbeats(ctx context.Context, staleSeconds
}
const getActiveChatsByAgentID = `-- name: GetActiveChatsByAgentID :many
-SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
+SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, summary, summary_generated_at, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
FROM chats_expanded
WHERE agent_id = $1::uuid
AND archived = false
@@ -6695,6 +6703,8 @@ func (q *sqlQuerier) GetActiveChatsByAgentID(ctx context.Context, agentID uuid.U
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
+ &i.Summary,
+ &i.SummaryGeneratedAt,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
@@ -6728,7 +6738,7 @@ func (q *sqlQuerier) GetActiveChatsByAgentID(ctx context.Context, agentID uuid.U
const getAutoArchiveInactiveChatCandidates = `-- name: GetAutoArchiveInactiveChatCandidates :many
SELECT
- chats_expanded.id, chats_expanded.owner_id, chats_expanded.workspace_id, chats_expanded.title, chats_expanded.status, chats_expanded.worker_id, chats_expanded.started_at, chats_expanded.heartbeat_at, chats_expanded.created_at, chats_expanded.updated_at, chats_expanded.parent_chat_id, chats_expanded.root_chat_id, chats_expanded.last_model_config_id, chats_expanded.last_reasoning_effort, chats_expanded.archived, chats_expanded.last_error, chats_expanded.mode, chats_expanded.mcp_server_ids, chats_expanded.labels, chats_expanded.build_id, chats_expanded.agent_id, chats_expanded.pin_order, chats_expanded.last_read_message_id, chats_expanded.dynamic_tools, chats_expanded.organization_id, chats_expanded.plan_mode, chats_expanded.client_type, chats_expanded.last_turn_summary, chats_expanded.snapshot_version, chats_expanded.history_version, chats_expanded.queue_version, chats_expanded.generation_attempt, chats_expanded.retry_state, chats_expanded.retry_state_version, chats_expanded.runner_id, chats_expanded.requires_action_deadline_at, chats_expanded.user_acl, chats_expanded.group_acl, chats_expanded.owner_username, chats_expanded.owner_name, chats_expanded.context_aggregate_hash, chats_expanded.context_dirty_since, chats_expanded.context_dirty_resources, chats_expanded.context_error, chats_expanded.compaction_requested_at,
+ chats_expanded.id, chats_expanded.owner_id, chats_expanded.workspace_id, chats_expanded.title, chats_expanded.status, chats_expanded.worker_id, chats_expanded.started_at, chats_expanded.heartbeat_at, chats_expanded.created_at, chats_expanded.updated_at, chats_expanded.parent_chat_id, chats_expanded.root_chat_id, chats_expanded.last_model_config_id, chats_expanded.last_reasoning_effort, chats_expanded.archived, chats_expanded.last_error, chats_expanded.mode, chats_expanded.mcp_server_ids, chats_expanded.labels, chats_expanded.build_id, chats_expanded.agent_id, chats_expanded.pin_order, chats_expanded.last_read_message_id, chats_expanded.dynamic_tools, chats_expanded.organization_id, chats_expanded.plan_mode, chats_expanded.client_type, chats_expanded.last_turn_summary, chats_expanded.summary, chats_expanded.summary_generated_at, chats_expanded.snapshot_version, chats_expanded.history_version, chats_expanded.queue_version, chats_expanded.generation_attempt, chats_expanded.retry_state, chats_expanded.retry_state_version, chats_expanded.runner_id, chats_expanded.requires_action_deadline_at, chats_expanded.user_acl, chats_expanded.group_acl, chats_expanded.owner_username, chats_expanded.owner_name, chats_expanded.context_aggregate_hash, chats_expanded.context_dirty_since, chats_expanded.context_dirty_resources, chats_expanded.context_error, chats_expanded.compaction_requested_at,
COALESCE(activity.last_activity_at, chats_expanded.created_at)::timestamptz AS last_activity_at
FROM chats_expanded
LEFT JOIN LATERAL (
@@ -6787,6 +6797,8 @@ type GetAutoArchiveInactiveChatCandidatesRow 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"`
@@ -6848,6 +6860,8 @@ func (q *sqlQuerier) GetAutoArchiveInactiveChatCandidates(ctx context.Context, a
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
+ &i.Summary,
+ &i.SummaryGeneratedAt,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
@@ -6903,7 +6917,7 @@ func (q *sqlQuerier) GetChatACLByID(ctx context.Context, id uuid.UUID) (GetChatA
}
const getChatByID = `-- name: GetChatByID :one
-SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
+SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, summary, summary_generated_at, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
FROM chats_expanded
WHERE id = $1::uuid
`
@@ -6940,6 +6954,8 @@ func (q *sqlQuerier) GetChatByID(ctx context.Context, id uuid.UUID) (Chat, error
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
+ &i.Summary,
+ &i.SummaryGeneratedAt,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
@@ -6963,7 +6979,7 @@ func (q *sqlQuerier) GetChatByID(ctx context.Context, id uuid.UUID) (Chat, error
const getChatByIDForShare = `-- name: GetChatByIDForShare :one
WITH shared_chat AS (
- SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at
+ SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at, summary, summary_generated_at
FROM chats
WHERE id = $1::uuid
FOR SHARE
@@ -6998,6 +7014,8 @@ chats_expanded AS (
shared_chat.plan_mode,
shared_chat.client_type,
shared_chat.last_turn_summary,
+ shared_chat.summary,
+ shared_chat.summary_generated_at,
shared_chat.snapshot_version,
shared_chat.history_version,
shared_chat.queue_version,
@@ -7020,7 +7038,7 @@ chats_expanded AS (
LEFT JOIN chats root ON root.id = COALESCE(shared_chat.root_chat_id, shared_chat.parent_chat_id)
JOIN visible_users owner ON owner.id = shared_chat.owner_id
)
-SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
+SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, summary, summary_generated_at, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
FROM chats_expanded
`
@@ -7056,6 +7074,8 @@ func (q *sqlQuerier) GetChatByIDForShare(ctx context.Context, id uuid.UUID) (Cha
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
+ &i.Summary,
+ &i.SummaryGeneratedAt,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
@@ -7079,7 +7099,7 @@ func (q *sqlQuerier) GetChatByIDForShare(ctx context.Context, id uuid.UUID) (Cha
const getChatByIDForUpdate = `-- name: GetChatByIDForUpdate :one
WITH locked_chat AS (
- SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at
+ SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at, summary, summary_generated_at
FROM chats
WHERE id = $1::uuid
FOR UPDATE
@@ -7114,6 +7134,8 @@ chats_expanded AS (
locked_chat.plan_mode,
locked_chat.client_type,
locked_chat.last_turn_summary,
+ locked_chat.summary,
+ locked_chat.summary_generated_at,
locked_chat.snapshot_version,
locked_chat.history_version,
locked_chat.queue_version,
@@ -7136,7 +7158,7 @@ chats_expanded AS (
LEFT JOIN chats root ON root.id = COALESCE(locked_chat.root_chat_id, locked_chat.parent_chat_id)
JOIN visible_users owner ON owner.id = locked_chat.owner_id
)
-SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
+SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, summary, summary_generated_at, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
FROM chats_expanded
`
@@ -7172,6 +7194,8 @@ func (q *sqlQuerier) GetChatByIDForUpdate(ctx context.Context, id uuid.UUID) (Ch
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
+ &i.Summary,
+ &i.SummaryGeneratedAt,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
@@ -8647,7 +8671,7 @@ func (q *sqlQuerier) GetChatUserPromptsByChatID(ctx context.Context, arg GetChat
const getChatWorkerAcquisitionCandidates = `-- name: GetChatWorkerAcquisitionCandidates :many
SELECT
- chats_expanded.id, chats_expanded.owner_id, chats_expanded.workspace_id, chats_expanded.title, chats_expanded.status, chats_expanded.worker_id, chats_expanded.started_at, chats_expanded.heartbeat_at, chats_expanded.created_at, chats_expanded.updated_at, chats_expanded.parent_chat_id, chats_expanded.root_chat_id, chats_expanded.last_model_config_id, chats_expanded.last_reasoning_effort, chats_expanded.archived, chats_expanded.last_error, chats_expanded.mode, chats_expanded.mcp_server_ids, chats_expanded.labels, chats_expanded.build_id, chats_expanded.agent_id, chats_expanded.pin_order, chats_expanded.last_read_message_id, chats_expanded.dynamic_tools, chats_expanded.organization_id, chats_expanded.plan_mode, chats_expanded.client_type, chats_expanded.last_turn_summary, chats_expanded.snapshot_version, chats_expanded.history_version, chats_expanded.queue_version, chats_expanded.generation_attempt, chats_expanded.retry_state, chats_expanded.retry_state_version, chats_expanded.runner_id, chats_expanded.requires_action_deadline_at, chats_expanded.user_acl, chats_expanded.group_acl, chats_expanded.owner_username, chats_expanded.owner_name, chats_expanded.context_aggregate_hash, chats_expanded.context_dirty_since, chats_expanded.context_dirty_resources, chats_expanded.context_error, chats_expanded.compaction_requested_at,
+ chats_expanded.id, chats_expanded.owner_id, chats_expanded.workspace_id, chats_expanded.title, chats_expanded.status, chats_expanded.worker_id, chats_expanded.started_at, chats_expanded.heartbeat_at, chats_expanded.created_at, chats_expanded.updated_at, chats_expanded.parent_chat_id, chats_expanded.root_chat_id, chats_expanded.last_model_config_id, chats_expanded.last_reasoning_effort, chats_expanded.archived, chats_expanded.last_error, chats_expanded.mode, chats_expanded.mcp_server_ids, chats_expanded.labels, chats_expanded.build_id, chats_expanded.agent_id, chats_expanded.pin_order, chats_expanded.last_read_message_id, chats_expanded.dynamic_tools, chats_expanded.organization_id, chats_expanded.plan_mode, chats_expanded.client_type, chats_expanded.last_turn_summary, chats_expanded.summary, chats_expanded.summary_generated_at, chats_expanded.snapshot_version, chats_expanded.history_version, chats_expanded.queue_version, chats_expanded.generation_attempt, chats_expanded.retry_state, chats_expanded.retry_state_version, chats_expanded.runner_id, chats_expanded.requires_action_deadline_at, chats_expanded.user_acl, chats_expanded.group_acl, chats_expanded.owner_username, chats_expanded.owner_name, chats_expanded.context_aggregate_hash, chats_expanded.context_dirty_since, chats_expanded.context_dirty_resources, chats_expanded.context_error, chats_expanded.compaction_requested_at,
chat_heartbeats.heartbeat_at AS current_heartbeat_at,
NOT EXISTS (
SELECT 1
@@ -8712,6 +8736,8 @@ type GetChatWorkerAcquisitionCandidatesRow 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"`
@@ -8782,6 +8808,8 @@ func (q *sqlQuerier) GetChatWorkerAcquisitionCandidates(ctx context.Context, arg
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
+ &i.Summary,
+ &i.SummaryGeneratedAt,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
@@ -8825,7 +8853,7 @@ WITH cursor_chat AS (
WHERE id = $7
)
SELECT
- chats_expanded.id, chats_expanded.owner_id, chats_expanded.workspace_id, chats_expanded.title, chats_expanded.status, chats_expanded.worker_id, chats_expanded.started_at, chats_expanded.heartbeat_at, chats_expanded.created_at, chats_expanded.updated_at, chats_expanded.parent_chat_id, chats_expanded.root_chat_id, chats_expanded.last_model_config_id, chats_expanded.last_reasoning_effort, chats_expanded.archived, chats_expanded.last_error, chats_expanded.mode, chats_expanded.mcp_server_ids, chats_expanded.labels, chats_expanded.build_id, chats_expanded.agent_id, chats_expanded.pin_order, chats_expanded.last_read_message_id, chats_expanded.dynamic_tools, chats_expanded.organization_id, chats_expanded.plan_mode, chats_expanded.client_type, chats_expanded.last_turn_summary, chats_expanded.snapshot_version, chats_expanded.history_version, chats_expanded.queue_version, chats_expanded.generation_attempt, chats_expanded.retry_state, chats_expanded.retry_state_version, chats_expanded.runner_id, chats_expanded.requires_action_deadline_at, chats_expanded.user_acl, chats_expanded.group_acl, chats_expanded.owner_username, chats_expanded.owner_name, chats_expanded.context_aggregate_hash, chats_expanded.context_dirty_since, chats_expanded.context_dirty_resources, chats_expanded.context_error, chats_expanded.compaction_requested_at,
+ chats_expanded.id, chats_expanded.owner_id, chats_expanded.workspace_id, chats_expanded.title, chats_expanded.status, chats_expanded.worker_id, chats_expanded.started_at, chats_expanded.heartbeat_at, chats_expanded.created_at, chats_expanded.updated_at, chats_expanded.parent_chat_id, chats_expanded.root_chat_id, chats_expanded.last_model_config_id, chats_expanded.last_reasoning_effort, chats_expanded.archived, chats_expanded.last_error, chats_expanded.mode, chats_expanded.mcp_server_ids, chats_expanded.labels, chats_expanded.build_id, chats_expanded.agent_id, chats_expanded.pin_order, chats_expanded.last_read_message_id, chats_expanded.dynamic_tools, chats_expanded.organization_id, chats_expanded.plan_mode, chats_expanded.client_type, chats_expanded.last_turn_summary, chats_expanded.summary, chats_expanded.summary_generated_at, chats_expanded.snapshot_version, chats_expanded.history_version, chats_expanded.queue_version, chats_expanded.generation_attempt, chats_expanded.retry_state, chats_expanded.retry_state_version, chats_expanded.runner_id, chats_expanded.requires_action_deadline_at, chats_expanded.user_acl, chats_expanded.group_acl, chats_expanded.owner_username, chats_expanded.owner_name, chats_expanded.context_aggregate_hash, chats_expanded.context_dirty_since, chats_expanded.context_dirty_resources, chats_expanded.context_error, chats_expanded.compaction_requested_at,
EXISTS (
SELECT 1 FROM chat_messages cm
WHERE cm.chat_id = chats_expanded.id
@@ -9107,6 +9135,8 @@ func (q *sqlQuerier) GetChats(ctx context.Context, arg GetChatsParams) ([]GetCha
&i.Chat.PlanMode,
&i.Chat.ClientType,
&i.Chat.LastTurnSummary,
+ &i.Chat.Summary,
+ &i.Chat.SummaryGeneratedAt,
&i.Chat.SnapshotVersion,
&i.Chat.HistoryVersion,
&i.Chat.QueueVersion,
@@ -9141,7 +9171,7 @@ func (q *sqlQuerier) GetChats(ctx context.Context, arg GetChatsParams) ([]GetCha
const getChatsByChatFileID = `-- name: GetChatsByChatFileID :many
SELECT
- id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
+ id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, summary, summary_generated_at, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
FROM
chats_expanded
WHERE
@@ -9192,6 +9222,8 @@ func (q *sqlQuerier) GetChatsByChatFileID(ctx context.Context, fileID uuid.UUID)
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
+ &i.Summary,
+ &i.SummaryGeneratedAt,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
@@ -9224,7 +9256,7 @@ func (q *sqlQuerier) GetChatsByChatFileID(ctx context.Context, fileID uuid.UUID)
}
const getChatsByIDsForRunnerSync = `-- name: GetChatsByIDsForRunnerSync :many
-SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
+SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, summary, summary_generated_at, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
FROM chats_expanded
WHERE id = ANY($1::uuid[])
ORDER BY id ASC
@@ -9268,6 +9300,8 @@ func (q *sqlQuerier) GetChatsByIDsForRunnerSync(ctx context.Context, ids []uuid.
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
+ &i.Summary,
+ &i.SummaryGeneratedAt,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
@@ -9300,7 +9334,7 @@ func (q *sqlQuerier) GetChatsByIDsForRunnerSync(ctx context.Context, ids []uuid.
}
const getChatsByWorkspaceIDs = `-- name: GetChatsByWorkspaceIDs :many
-SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
+SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, summary, summary_generated_at, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
FROM chats_expanded
WHERE archived = false
AND workspace_id = ANY($1::uuid[])
@@ -9345,6 +9379,8 @@ func (q *sqlQuerier) GetChatsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
+ &i.Summary,
+ &i.SummaryGeneratedAt,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
@@ -9446,7 +9482,7 @@ func (q *sqlQuerier) GetChatsUpdatedAfter(ctx context.Context, updatedAfter time
const getChildChatsByParentIDs = `-- name: GetChildChatsByParentIDs :many
SELECT
- chats_expanded.id, chats_expanded.owner_id, chats_expanded.workspace_id, chats_expanded.title, chats_expanded.status, chats_expanded.worker_id, chats_expanded.started_at, chats_expanded.heartbeat_at, chats_expanded.created_at, chats_expanded.updated_at, chats_expanded.parent_chat_id, chats_expanded.root_chat_id, chats_expanded.last_model_config_id, chats_expanded.last_reasoning_effort, chats_expanded.archived, chats_expanded.last_error, chats_expanded.mode, chats_expanded.mcp_server_ids, chats_expanded.labels, chats_expanded.build_id, chats_expanded.agent_id, chats_expanded.pin_order, chats_expanded.last_read_message_id, chats_expanded.dynamic_tools, chats_expanded.organization_id, chats_expanded.plan_mode, chats_expanded.client_type, chats_expanded.last_turn_summary, chats_expanded.snapshot_version, chats_expanded.history_version, chats_expanded.queue_version, chats_expanded.generation_attempt, chats_expanded.retry_state, chats_expanded.retry_state_version, chats_expanded.runner_id, chats_expanded.requires_action_deadline_at, chats_expanded.user_acl, chats_expanded.group_acl, chats_expanded.owner_username, chats_expanded.owner_name, chats_expanded.context_aggregate_hash, chats_expanded.context_dirty_since, chats_expanded.context_dirty_resources, chats_expanded.context_error, chats_expanded.compaction_requested_at,
+ chats_expanded.id, chats_expanded.owner_id, chats_expanded.workspace_id, chats_expanded.title, chats_expanded.status, chats_expanded.worker_id, chats_expanded.started_at, chats_expanded.heartbeat_at, chats_expanded.created_at, chats_expanded.updated_at, chats_expanded.parent_chat_id, chats_expanded.root_chat_id, chats_expanded.last_model_config_id, chats_expanded.last_reasoning_effort, chats_expanded.archived, chats_expanded.last_error, chats_expanded.mode, chats_expanded.mcp_server_ids, chats_expanded.labels, chats_expanded.build_id, chats_expanded.agent_id, chats_expanded.pin_order, chats_expanded.last_read_message_id, chats_expanded.dynamic_tools, chats_expanded.organization_id, chats_expanded.plan_mode, chats_expanded.client_type, chats_expanded.last_turn_summary, chats_expanded.summary, chats_expanded.summary_generated_at, chats_expanded.snapshot_version, chats_expanded.history_version, chats_expanded.queue_version, chats_expanded.generation_attempt, chats_expanded.retry_state, chats_expanded.retry_state_version, chats_expanded.runner_id, chats_expanded.requires_action_deadline_at, chats_expanded.user_acl, chats_expanded.group_acl, chats_expanded.owner_username, chats_expanded.owner_name, chats_expanded.context_aggregate_hash, chats_expanded.context_dirty_since, chats_expanded.context_dirty_resources, chats_expanded.context_error, chats_expanded.compaction_requested_at,
EXISTS (
SELECT 1 FROM chat_messages cm
WHERE cm.chat_id = chats_expanded.id
@@ -9519,6 +9555,8 @@ func (q *sqlQuerier) GetChildChatsByParentIDs(ctx context.Context, arg GetChildC
&i.Chat.PlanMode,
&i.Chat.ClientType,
&i.Chat.LastTurnSummary,
+ &i.Chat.Summary,
+ &i.Chat.SummaryGeneratedAt,
&i.Chat.SnapshotVersion,
&i.Chat.HistoryVersion,
&i.Chat.QueueVersion,
@@ -9619,7 +9657,7 @@ func (q *sqlQuerier) GetLastChatMessageByRole(ctx context.Context, arg GetLastCh
const getStaleChats = `-- name: GetStaleChats :many
SELECT
- id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
+ id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, summary, summary_generated_at, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
FROM
chats_expanded
WHERE
@@ -9680,6 +9718,8 @@ func (q *sqlQuerier) GetStaleChats(ctx context.Context, staleThreshold time.Time
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
+ &i.Summary,
+ &i.SummaryGeneratedAt,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
@@ -9925,7 +9965,7 @@ INSERT INTO chats (
$15::jsonb,
$16::chat_client_type
)
-RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at
+RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at, summary, summary_generated_at
),
chats_expanded AS (
SELECT
@@ -9957,6 +9997,8 @@ chats_expanded AS (
inserted_chat.plan_mode,
inserted_chat.client_type,
inserted_chat.last_turn_summary,
+ inserted_chat.summary,
+ inserted_chat.summary_generated_at,
inserted_chat.snapshot_version,
inserted_chat.history_version,
inserted_chat.queue_version,
@@ -9979,7 +10021,7 @@ chats_expanded AS (
LEFT JOIN chats root ON root.id = COALESCE(inserted_chat.root_chat_id, inserted_chat.parent_chat_id)
JOIN visible_users owner ON owner.id = inserted_chat.owner_id
)
-SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
+SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, summary, summary_generated_at, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
FROM chats_expanded
`
@@ -10051,6 +10093,8 @@ func (q *sqlQuerier) InsertChat(ctx context.Context, arg InsertChatParams) (Chat
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
+ &i.Summary,
+ &i.SummaryGeneratedAt,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
@@ -10555,7 +10599,7 @@ WITH bumped_chat AS (
WHERE id = $1::uuid
FOR UPDATE
)
- RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at
+ RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at, summary, summary_generated_at
),
chats_expanded AS (
SELECT
@@ -10587,6 +10631,8 @@ chats_expanded AS (
bumped_chat.plan_mode,
bumped_chat.client_type,
bumped_chat.last_turn_summary,
+ bumped_chat.summary,
+ bumped_chat.summary_generated_at,
bumped_chat.snapshot_version,
bumped_chat.history_version,
bumped_chat.queue_version,
@@ -10608,7 +10654,7 @@ chats_expanded AS (
LEFT JOIN chats root ON root.id = COALESCE(bumped_chat.root_chat_id, bumped_chat.parent_chat_id)
JOIN visible_users owner ON owner.id = bumped_chat.owner_id
)
-SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
+SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, summary, summary_generated_at, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
FROM chats_expanded
`
@@ -10648,6 +10694,8 @@ func (q *sqlQuerier) LockChatAndBumpSnapshotVersion(ctx context.Context, id uuid
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
+ &i.Summary,
+ &i.SummaryGeneratedAt,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
@@ -10995,7 +11043,7 @@ WITH updated_chats AS (
archived = false,
updated_at = NOW()
WHERE id = $1::uuid OR root_chat_id = $1::uuid
- RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at
+ RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at, summary, summary_generated_at
),
chats_expanded AS (
SELECT
@@ -11027,6 +11075,8 @@ chats_expanded AS (
updated_chats.plan_mode,
updated_chats.client_type,
updated_chats.last_turn_summary,
+ updated_chats.summary,
+ updated_chats.summary_generated_at,
updated_chats.snapshot_version,
updated_chats.history_version,
updated_chats.queue_version,
@@ -11049,7 +11099,7 @@ chats_expanded AS (
LEFT JOIN chats root ON root.id = COALESCE(updated_chats.root_chat_id, updated_chats.parent_chat_id)
JOIN visible_users owner ON owner.id = updated_chats.owner_id
)
-SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
+SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, summary, summary_generated_at, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
FROM chats_expanded
ORDER BY (chats_expanded.id = $1::uuid) DESC, chats_expanded.created_at ASC, chats_expanded.id ASC
`
@@ -11096,6 +11146,8 @@ func (q *sqlQuerier) UnarchiveChatByID(ctx context.Context, id uuid.UUID) ([]Cha
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
+ &i.Summary,
+ &i.SummaryGeneratedAt,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
@@ -11215,7 +11267,7 @@ UPDATE chats SET
updated_at = NOW()
WHERE
id = $3::uuid
-RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at
+RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at, summary, summary_generated_at
),
chats_expanded AS (
SELECT
@@ -11247,6 +11299,8 @@ chats_expanded AS (
updated_chat.plan_mode,
updated_chat.client_type,
updated_chat.last_turn_summary,
+ updated_chat.summary,
+ updated_chat.summary_generated_at,
updated_chat.snapshot_version,
updated_chat.history_version,
updated_chat.queue_version,
@@ -11269,7 +11323,7 @@ chats_expanded AS (
LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
JOIN visible_users owner ON owner.id = updated_chat.owner_id
)
-SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
+SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, summary, summary_generated_at, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
FROM chats_expanded
`
@@ -11311,6 +11365,8 @@ func (q *sqlQuerier) UpdateChatBuildAgentBinding(ctx context.Context, arg Update
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
+ &i.Summary,
+ &i.SummaryGeneratedAt,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
@@ -11341,7 +11397,7 @@ SET
updated_at = NOW()
WHERE
id = $2::uuid
-RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at
+RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at, summary, summary_generated_at
),
chats_expanded AS (
SELECT
@@ -11373,6 +11429,8 @@ chats_expanded AS (
updated_chat.plan_mode,
updated_chat.client_type,
updated_chat.last_turn_summary,
+ updated_chat.summary,
+ updated_chat.summary_generated_at,
updated_chat.snapshot_version,
updated_chat.history_version,
updated_chat.queue_version,
@@ -11395,7 +11453,7 @@ chats_expanded AS (
LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
JOIN visible_users owner ON owner.id = updated_chat.owner_id
)
-SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
+SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, summary, summary_generated_at, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
FROM chats_expanded
`
@@ -11436,6 +11494,8 @@ func (q *sqlQuerier) UpdateChatByID(ctx context.Context, arg UpdateChatByIDParam
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
+ &i.Summary,
+ &i.SummaryGeneratedAt,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
@@ -11471,7 +11531,7 @@ WITH updated_chat AS (
pin_order = CASE WHEN $2::boolean THEN 0 ELSE pin_order END,
updated_at = NOW()
WHERE id = $8::uuid
- RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at
+ RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at, summary, summary_generated_at
),
chats_expanded AS (
SELECT
@@ -11503,6 +11563,8 @@ chats_expanded AS (
updated_chat.plan_mode,
updated_chat.client_type,
updated_chat.last_turn_summary,
+ updated_chat.summary,
+ updated_chat.summary_generated_at,
updated_chat.snapshot_version,
updated_chat.history_version,
updated_chat.queue_version,
@@ -11524,7 +11586,7 @@ chats_expanded AS (
LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
JOIN visible_users owner ON owner.id = updated_chat.owner_id
)
-SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
+SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, summary, summary_generated_at, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
FROM chats_expanded
`
@@ -11585,6 +11647,8 @@ func (q *sqlQuerier) UpdateChatExecutionState(ctx context.Context, arg UpdateCha
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
+ &i.Summary,
+ &i.SummaryGeneratedAt,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
@@ -11660,7 +11724,7 @@ SET
updated_at = NOW()
WHERE
id = $2::uuid
-RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at
+RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at, summary, summary_generated_at
),
chats_expanded AS (
SELECT
@@ -11692,6 +11756,8 @@ chats_expanded AS (
updated_chat.plan_mode,
updated_chat.client_type,
updated_chat.last_turn_summary,
+ updated_chat.summary,
+ updated_chat.summary_generated_at,
updated_chat.snapshot_version,
updated_chat.history_version,
updated_chat.queue_version,
@@ -11714,7 +11780,7 @@ chats_expanded AS (
LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
JOIN visible_users owner ON owner.id = updated_chat.owner_id
)
-SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
+SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, summary, summary_generated_at, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
FROM chats_expanded
`
@@ -11755,6 +11821,8 @@ func (q *sqlQuerier) UpdateChatLabelsByID(ctx context.Context, arg UpdateChatLab
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
+ &i.Summary,
+ &i.SummaryGeneratedAt,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
@@ -11785,7 +11853,7 @@ SET
last_model_config_id = $1::uuid
WHERE
id = $2::uuid
-RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at
+RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at, summary, summary_generated_at
),
chats_expanded AS (
SELECT
@@ -11817,6 +11885,8 @@ chats_expanded AS (
updated_chat.plan_mode,
updated_chat.client_type,
updated_chat.last_turn_summary,
+ updated_chat.summary,
+ updated_chat.summary_generated_at,
updated_chat.snapshot_version,
updated_chat.history_version,
updated_chat.queue_version,
@@ -11839,7 +11909,7 @@ chats_expanded AS (
LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
JOIN visible_users owner ON owner.id = updated_chat.owner_id
)
-SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
+SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, summary, summary_generated_at, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
FROM chats_expanded
`
@@ -11880,6 +11950,8 @@ func (q *sqlQuerier) UpdateChatLastModelConfigByID(ctx context.Context, arg Upda
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
+ &i.Summary,
+ &i.SummaryGeneratedAt,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
@@ -11960,7 +12032,7 @@ SET
updated_at = NOW()
WHERE
id = $2::uuid
-RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at
+RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at, summary, summary_generated_at
),
chats_expanded AS (
SELECT
@@ -11992,6 +12064,8 @@ chats_expanded AS (
updated_chat.plan_mode,
updated_chat.client_type,
updated_chat.last_turn_summary,
+ updated_chat.summary,
+ updated_chat.summary_generated_at,
updated_chat.snapshot_version,
updated_chat.history_version,
updated_chat.queue_version,
@@ -12014,7 +12088,7 @@ chats_expanded AS (
LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
JOIN visible_users owner ON owner.id = updated_chat.owner_id
)
-SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
+SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, summary, summary_generated_at, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
FROM chats_expanded
`
@@ -12055,6 +12129,8 @@ func (q *sqlQuerier) UpdateChatMCPServerIDs(ctx context.Context, arg UpdateChatM
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
+ &i.Summary,
+ &i.SummaryGeneratedAt,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
@@ -12156,7 +12232,7 @@ SET
plan_mode = $1::chat_plan_mode
WHERE
id = $2::uuid
-RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at
+RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at, summary, summary_generated_at
),
chats_expanded AS (
SELECT
@@ -12188,6 +12264,8 @@ chats_expanded AS (
updated_chat.plan_mode,
updated_chat.client_type,
updated_chat.last_turn_summary,
+ updated_chat.summary,
+ updated_chat.summary_generated_at,
updated_chat.snapshot_version,
updated_chat.history_version,
updated_chat.queue_version,
@@ -12210,7 +12288,7 @@ chats_expanded AS (
LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
JOIN visible_users owner ON owner.id = updated_chat.owner_id
)
-SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
+SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, summary, summary_generated_at, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
FROM chats_expanded
`
@@ -12251,6 +12329,8 @@ func (q *sqlQuerier) UpdateChatPlanModeByID(ctx context.Context, arg UpdateChatP
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
+ &i.Summary,
+ &i.SummaryGeneratedAt,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
@@ -12279,7 +12359,7 @@ WITH updated_chat AS (
retry_state = $1::jsonb,
updated_at = NOW()
WHERE id = $2::uuid
- RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at
+ RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at, summary, summary_generated_at
),
chats_expanded AS (
SELECT
@@ -12311,6 +12391,8 @@ chats_expanded AS (
updated_chat.plan_mode,
updated_chat.client_type,
updated_chat.last_turn_summary,
+ updated_chat.summary,
+ updated_chat.summary_generated_at,
updated_chat.snapshot_version,
updated_chat.history_version,
updated_chat.queue_version,
@@ -12332,7 +12414,7 @@ chats_expanded AS (
LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
JOIN visible_users owner ON owner.id = updated_chat.owner_id
)
-SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
+SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, summary, summary_generated_at, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
FROM chats_expanded
`
@@ -12375,6 +12457,8 @@ func (q *sqlQuerier) UpdateChatRetryState(ctx context.Context, arg UpdateChatRet
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
+ &i.Summary,
+ &i.SummaryGeneratedAt,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
@@ -12409,7 +12493,7 @@ SET
updated_at = NOW()
WHERE
id = $6::uuid
-RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at
+RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at, summary, summary_generated_at
),
chats_expanded AS (
SELECT
@@ -12441,6 +12525,8 @@ chats_expanded AS (
updated_chat.plan_mode,
updated_chat.client_type,
updated_chat.last_turn_summary,
+ updated_chat.summary,
+ updated_chat.summary_generated_at,
updated_chat.snapshot_version,
updated_chat.history_version,
updated_chat.queue_version,
@@ -12463,7 +12549,7 @@ chats_expanded AS (
LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
JOIN visible_users owner ON owner.id = updated_chat.owner_id
)
-SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
+SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, summary, summary_generated_at, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
FROM chats_expanded
`
@@ -12515,6 +12601,8 @@ func (q *sqlQuerier) UpdateChatStatus(ctx context.Context, arg UpdateChatStatusP
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
+ &i.Summary,
+ &i.SummaryGeneratedAt,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
@@ -12536,6 +12624,32 @@ func (q *sqlQuerier) UpdateChatStatus(ctx context.Context, arg UpdateChatStatusP
return i, err
}
+const updateChatSummary = `-- name: UpdateChatSummary :execrows
+UPDATE chats
+SET
+ summary = $1::text,
+ summary_generated_at = NOW()
+WHERE
+ id = $2::uuid
+ AND history_version = $3::bigint
+`
+
+type UpdateChatSummaryParams struct {
+ Summary sql.NullString `db:"summary" json:"summary"`
+ ID uuid.UUID `db:"id" json:"id"`
+ ExpectedHistoryVersion int64 `db:"expected_history_version" json:"expected_history_version"`
+}
+
+// The history_version fence lets background summary writes ignore worker-only
+// updates while losing to newer message history.
+func (q *sqlQuerier) UpdateChatSummary(ctx context.Context, arg UpdateChatSummaryParams) (int64, error) {
+ result, err := q.db.ExecContext(ctx, updateChatSummary, arg.Summary, arg.ID, arg.ExpectedHistoryVersion)
+ if err != nil {
+ return 0, err
+ }
+ return result.RowsAffected()
+}
+
const updateChatTitleByID = `-- name: UpdateChatTitleByID :one
WITH updated_chat AS (
UPDATE
@@ -12547,7 +12661,7 @@ SET
title = $1::text
WHERE
id = $2::uuid
-RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at
+RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at, summary, summary_generated_at
),
chats_expanded AS (
SELECT
@@ -12579,6 +12693,8 @@ chats_expanded AS (
updated_chat.plan_mode,
updated_chat.client_type,
updated_chat.last_turn_summary,
+ updated_chat.summary,
+ updated_chat.summary_generated_at,
updated_chat.snapshot_version,
updated_chat.history_version,
updated_chat.queue_version,
@@ -12601,7 +12717,7 @@ chats_expanded AS (
LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
JOIN visible_users owner ON owner.id = updated_chat.owner_id
)
-SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
+SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, summary, summary_generated_at, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
FROM chats_expanded
`
@@ -12642,6 +12758,8 @@ func (q *sqlQuerier) UpdateChatTitleByID(ctx context.Context, arg UpdateChatTitl
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
+ &i.Summary,
+ &i.SummaryGeneratedAt,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
@@ -12665,7 +12783,7 @@ func (q *sqlQuerier) UpdateChatTitleByID(ctx context.Context, arg UpdateChatTitl
const updateChatWorkspaceBinding = `-- name: UpdateChatWorkspaceBinding :one
WITH current_chat AS (
- SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at
+ SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at, summary, summary_generated_at
FROM chats
WHERE id = $1::uuid
),
@@ -12684,13 +12802,13 @@ changed_chat AS (
updated_at = NOW()
WHERE id = $1::uuid
AND (SELECT changed FROM binding_changed)
- RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at
+ RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at, summary, summary_generated_at
),
result_chat AS (
- SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at
+ SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at, summary, summary_generated_at
FROM changed_chat
UNION ALL
- SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at
+ SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at, summary, summary_generated_at
FROM current_chat
WHERE NOT (SELECT changed FROM binding_changed)
),
@@ -12724,6 +12842,8 @@ chats_expanded AS (
result_chat.plan_mode,
result_chat.client_type,
result_chat.last_turn_summary,
+ result_chat.summary,
+ result_chat.summary_generated_at,
result_chat.snapshot_version,
result_chat.history_version,
result_chat.queue_version,
@@ -12746,7 +12866,7 @@ chats_expanded AS (
LEFT JOIN chats root ON root.id = COALESCE(result_chat.root_chat_id, result_chat.parent_chat_id)
JOIN visible_users owner ON owner.id = result_chat.owner_id
)
-SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
+SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, summary, summary_generated_at, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
FROM chats_expanded
`
@@ -12794,6 +12914,8 @@ func (q *sqlQuerier) UpdateChatWorkspaceBinding(ctx context.Context, arg UpdateC
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
+ &i.Summary,
+ &i.SummaryGeneratedAt,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
diff --git a/coderd/database/queries/chats.sql b/coderd/database/queries/chats.sql
index d1a796c54c8..2d7389630fb 100644
--- a/coderd/database/queries/chats.sql
+++ b/coderd/database/queries/chats.sql
@@ -35,6 +35,8 @@ chats_expanded AS (
updated_chats.plan_mode,
updated_chats.client_type,
updated_chats.last_turn_summary,
+ updated_chats.summary,
+ updated_chats.summary_generated_at,
updated_chats.snapshot_version,
updated_chats.history_version,
updated_chats.queue_version,
@@ -103,6 +105,8 @@ chats_expanded AS (
updated_chats.plan_mode,
updated_chats.client_type,
updated_chats.last_turn_summary,
+ updated_chats.summary,
+ updated_chats.summary_generated_at,
updated_chats.snapshot_version,
updated_chats.history_version,
updated_chats.queue_version,
@@ -836,6 +840,8 @@ chats_expanded AS (
inserted_chat.plan_mode,
inserted_chat.client_type,
inserted_chat.last_turn_summary,
+ inserted_chat.summary,
+ inserted_chat.summary_generated_at,
inserted_chat.snapshot_version,
inserted_chat.history_version,
inserted_chat.queue_version,
@@ -978,6 +984,8 @@ chats_expanded AS (
updated_chat.plan_mode,
updated_chat.client_type,
updated_chat.last_turn_summary,
+ updated_chat.summary,
+ updated_chat.summary_generated_at,
updated_chat.snapshot_version,
updated_chat.history_version,
updated_chat.queue_version,
@@ -1046,6 +1054,8 @@ chats_expanded AS (
updated_chat.plan_mode,
updated_chat.client_type,
updated_chat.last_turn_summary,
+ updated_chat.summary,
+ updated_chat.summary_generated_at,
updated_chat.snapshot_version,
updated_chat.history_version,
updated_chat.queue_version,
@@ -1112,6 +1122,8 @@ chats_expanded AS (
updated_chat.plan_mode,
updated_chat.client_type,
updated_chat.last_turn_summary,
+ updated_chat.summary,
+ updated_chat.summary_generated_at,
updated_chat.snapshot_version,
updated_chat.history_version,
updated_chat.queue_version,
@@ -1178,6 +1190,8 @@ chats_expanded AS (
updated_chat.plan_mode,
updated_chat.client_type,
updated_chat.last_turn_summary,
+ updated_chat.summary,
+ updated_chat.summary_generated_at,
updated_chat.snapshot_version,
updated_chat.history_version,
updated_chat.queue_version,
@@ -1244,6 +1258,8 @@ chats_expanded AS (
updated_chat.plan_mode,
updated_chat.client_type,
updated_chat.last_turn_summary,
+ updated_chat.summary,
+ updated_chat.summary_generated_at,
updated_chat.snapshot_version,
updated_chat.history_version,
updated_chat.queue_version,
@@ -1330,6 +1346,8 @@ chats_expanded AS (
result_chat.plan_mode,
result_chat.client_type,
result_chat.last_turn_summary,
+ result_chat.summary,
+ result_chat.summary_generated_at,
result_chat.snapshot_version,
result_chat.history_version,
result_chat.queue_version,
@@ -1395,6 +1413,8 @@ chats_expanded AS (
updated_chat.plan_mode,
updated_chat.client_type,
updated_chat.last_turn_summary,
+ updated_chat.summary,
+ updated_chat.summary_generated_at,
updated_chat.snapshot_version,
updated_chat.history_version,
updated_chat.queue_version,
@@ -1437,6 +1457,17 @@ WHERE
id = @id::uuid
AND history_version = @expected_history_version::bigint;
+-- name: UpdateChatSummary :execrows
+-- The history_version fence lets background summary writes ignore worker-only
+-- updates while losing to newer message history.
+UPDATE chats
+SET
+ summary = sqlc.narg('summary')::text,
+ summary_generated_at = NOW()
+WHERE
+ id = @id::uuid
+ AND history_version = @expected_history_version::bigint;
+
-- name: UpdateChatMCPServerIDs :one
WITH updated_chat AS (
UPDATE
@@ -1478,6 +1509,8 @@ chats_expanded AS (
updated_chat.plan_mode,
updated_chat.client_type,
updated_chat.last_turn_summary,
+ updated_chat.summary,
+ updated_chat.summary_generated_at,
updated_chat.snapshot_version,
updated_chat.history_version,
updated_chat.queue_version,
@@ -1686,6 +1719,8 @@ chats_expanded AS (
updated_chat.plan_mode,
updated_chat.client_type,
updated_chat.last_turn_summary,
+ updated_chat.summary,
+ updated_chat.summary_generated_at,
updated_chat.snapshot_version,
updated_chat.history_version,
updated_chat.queue_version,
@@ -1963,6 +1998,8 @@ chats_expanded AS (
locked_chat.plan_mode,
locked_chat.client_type,
locked_chat.last_turn_summary,
+ locked_chat.summary,
+ locked_chat.summary_generated_at,
locked_chat.snapshot_version,
locked_chat.history_version,
locked_chat.queue_version,
@@ -2025,6 +2062,8 @@ chats_expanded AS (
shared_chat.plan_mode,
shared_chat.client_type,
shared_chat.last_turn_summary,
+ shared_chat.summary,
+ shared_chat.summary_generated_at,
shared_chat.snapshot_version,
shared_chat.history_version,
shared_chat.queue_version,
@@ -2700,6 +2739,8 @@ chats_expanded AS (
bumped_chat.plan_mode,
bumped_chat.client_type,
bumped_chat.last_turn_summary,
+ bumped_chat.summary,
+ bumped_chat.summary_generated_at,
bumped_chat.snapshot_version,
bumped_chat.history_version,
bumped_chat.queue_version,
@@ -2775,6 +2816,8 @@ chats_expanded AS (
updated_chat.plan_mode,
updated_chat.client_type,
updated_chat.last_turn_summary,
+ updated_chat.summary,
+ updated_chat.summary_generated_at,
updated_chat.snapshot_version,
updated_chat.history_version,
updated_chat.queue_version,
@@ -2840,6 +2883,8 @@ chats_expanded AS (
updated_chat.plan_mode,
updated_chat.client_type,
updated_chat.last_turn_summary,
+ updated_chat.summary,
+ updated_chat.summary_generated_at,
updated_chat.snapshot_version,
updated_chat.history_version,
updated_chat.queue_version,
diff --git a/coderd/x/chatd/chatd.go b/coderd/x/chatd/chatd.go
index b906cdaa45a..6ca6fa56db2 100644
--- a/coderd/x/chatd/chatd.go
+++ b/coderd/x/chatd/chatd.go
@@ -4387,6 +4387,7 @@ func (p *Server) maybeFinalizeTurnStatusLabelAndPush(
switch status {
case database.ChatStatusWaiting:
p.finalizeSuccessfulTurnStatusLabelAndPush(ctx, chat, status, runResult, logger)
+ p.maybeGenerateChatSummaryAsync(ctx, logger, chat)
case database.ChatStatusError:
p.clearLastTurnSummaryAsync(ctx, chat, logger)
@@ -4614,6 +4615,200 @@ func (p *Server) updateLastTurnSummary(
p.publishChatPubsubEvent(updatedChat, codersdk.ChatWatchEventKindSummaryChange, nil)
}
+const (
+ // Completed user turns before the first summary is generated.
+ summaryInitialTurnThreshold = 1
+ // New completed user turns before the summary is regenerated (since the last summary).
+ summaryStaleTurnThreshold = 3
+ summaryMinTranscriptRunes = 200
+ chatSummaryWorkTimeout = 120 * time.Second
+ chatSummaryGenerateTimeout = 60 * time.Second
+ chatSummaryWriteTimeout = 5 * time.Second
+)
+
+// maybeGenerateChatSummaryAsync launches best-effort whole-chat summary
+// generation in the background for a root chat.
+func (p *Server) maybeGenerateChatSummaryAsync(
+ ctx context.Context,
+ logger slog.Logger,
+ chat database.Chat,
+) {
+ if chat.ParentChatID.Valid {
+ return
+ }
+ ctx, cancel := p.inflightContext(ctx)
+ if err := p.goInflight(func() {
+ defer cancel()
+ p.generateAndStoreChatSummary(ctx, logger, chat)
+ }); err != nil {
+ cancel()
+ logger.Debug(ctx, "skipped chat summary generation",
+ slog.F("chat_id", chat.ID), slog.Error(err))
+ }
+}
+
+// generateAndStoreChatSummary regenerates and persists the whole-chat summary
+// when due. Best-effort; never clears an existing summary on failure.
+func (p *Server) generateAndStoreChatSummary(
+ ctx context.Context,
+ logger slog.Logger,
+ chat database.Chat,
+) {
+ ctx, cancel := context.WithTimeout(ctx, chatSummaryWorkTimeout)
+ defer cancel()
+
+ //nolint:gocritic // Narrow daemon access for best-effort summary generation.
+ ctx = dbauthz.AsChatd(ctx)
+
+ // 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(ctx, chat.ID)
+ if err != nil {
+ logger.Debug(ctx, "failed to re-read chat for summary",
+ slog.F("chat_id", chat.ID), slog.Error(err))
+ return
+ }
+
+ messages, err := p.db.GetChatMessagesForPromptByChatID(ctx, chat.ID)
+ if err != nil {
+ logger.Debug(ctx, "failed to load messages for chat summary",
+ slog.F("chat_id", chat.ID), slog.Error(err))
+ return
+ }
+
+ if !shouldGenerateChatSummary(chat, messages) {
+ return
+ }
+
+ transcript := renderChatSummaryTranscript(messages)
+ if len([]rune(transcript)) < summaryMinTranscriptRunes {
+ logger.Debug(ctx, "skipping chat summary for short transcript",
+ slog.F("chat_id", chat.ID),
+ slog.F("transcript_runes", len([]rune(transcript))),
+ )
+ return
+ }
+
+ // Derive the delegated API key from the chat owner so AI Gateway routing
+ // attributes summary generation to the correct account. This goroutine may
+ // outlive the launching turn, so it cannot rely on that turn's context.
+ apiKeyID, err := p.ensureSyntheticAPIKeyID(ctx, chat.OwnerID)
+ if err != nil {
+ logger.Debug(ctx, "failed to ensure synthetic API key for chat summary",
+ slog.F("chat_id", chat.ID), slog.Error(err))
+ return
+ }
+ modelOpts := modelBuildOptions{ActiveAPIKeyID: apiKeyID}
+
+ model, _, ok := p.resolveChatSummaryModel(ctx, logger, chat, modelOpts)
+ if !ok {
+ return
+ }
+
+ summaryCtx, cancelGen := context.WithTimeout(ctx, chatSummaryGenerateTimeout)
+ defer cancelGen()
+ summary, _, genErr := generateChatSummary(summaryCtx, model, transcript)
+
+ if genErr != nil {
+ logger.Debug(ctx, "failed to generate chat summary",
+ slog.F("chat_id", chat.ID), slog.Error(genErr))
+ return
+ }
+
+ p.updateChatSummary(ctx, logger, chat, chat.HistoryVersion, summary)
+}
+
+func (p *Server) resolveChatSummaryModel(
+ ctx context.Context,
+ logger slog.Logger,
+ chat database.Chat,
+ modelOpts modelBuildOptions,
+) (fantasy.LanguageModel, database.ChatModelConfig, bool) {
+ //nolint:dogsled // resolveChatModel returns rich routing metadata; summary generation only needs the model and its config.
+ model, dbConfig, _, _, _, _, err := p.resolveChatModel(ctx, chat, modelOpts)
+ if err != nil {
+ logger.Debug(ctx, "failed to resolve chat model for summary",
+ slog.F("chat_id", chat.ID), slog.Error(err))
+ return nil, database.ChatModelConfig{}, false
+ }
+ return model, dbConfig, true
+}
+
+func shouldGenerateChatSummary(chat database.Chat, messages []database.ChatMessage) bool {
+ if !chat.Summary.Valid {
+ return countCompletedTurnsSince(messages, time.Time{}) >= summaryInitialTurnThreshold
+ }
+ var marker time.Time
+ if chat.SummaryGeneratedAt.Valid {
+ marker = chat.SummaryGeneratedAt.Time
+ }
+ return countCompletedTurnsSince(messages, marker) >= summaryStaleTurnThreshold
+}
+
+// countCompletedTurnsSince counts visible user messages (one per turn) created
+// after the given time. Model-only user messages (injected context, replayed
+// compaction summary) are not turns; a zero time counts all.
+func countCompletedTurnsSince(messages []database.ChatMessage, after time.Time) int {
+ count := 0
+ for _, message := range messages {
+ if message.Role != database.ChatMessageRoleUser {
+ continue
+ }
+ if message.Visibility != database.ChatMessageVisibilityBoth &&
+ message.Visibility != database.ChatMessageVisibilityUser {
+ continue
+ }
+ if !after.IsZero() && !message.CreatedAt.After(after) {
+ continue
+ }
+ count++
+ }
+ return count
+}
+
+// updateChatSummary persists the whole-chat summary. Best-effort background
+// write (pass a detached context); a blank summary is a no-op, never clearing
+// an existing one.
+func (p *Server) updateChatSummary(
+ ctx context.Context,
+ logger slog.Logger,
+ chat database.Chat,
+ expectedHistoryVersion int64,
+ summary string,
+) {
+ summary = strings.TrimSpace(summary)
+ if summary == "" {
+ return
+ }
+ sqlSummary := sql.NullString{String: summary, Valid: true}
+
+ ctx, cancel := context.WithTimeout(ctx, chatSummaryWriteTimeout)
+ defer cancel()
+
+ affected, err := p.db.UpdateChatSummary(ctx, database.UpdateChatSummaryParams{
+ ID: chat.ID,
+ ExpectedHistoryVersion: expectedHistoryVersion,
+ Summary: sqlSummary,
+ })
+ if err != nil {
+ logger.Warn(ctx, "failed to update chat summary",
+ slog.F("chat_id", chat.ID), slog.Error(err))
+ return
+ }
+ if affected == 0 {
+ logger.Info(ctx, "skipped stale chat summary update",
+ slog.F("chat_id", chat.ID),
+ slog.F("summary_length", len(summary)),
+ slog.F("expected_history_version", expectedHistoryVersion),
+ )
+ return
+ }
+
+ updatedChat := chat
+ updatedChat.Summary = sqlSummary
+ p.publishChatPubsubEvent(updatedChat, codersdk.ChatWatchEventKindChatSummaryChange, nil)
+}
+
func (p *Server) webpushConfigured() bool {
return p.webpushDispatcher != nil && p.webpushDispatcher.PublicKey() != ""
}
diff --git a/coderd/x/chatd/chatd_internal_test.go b/coderd/x/chatd/chatd_internal_test.go
index 84ff5d3a787..f5c7b5c6334 100644
--- a/coderd/x/chatd/chatd_internal_test.go
+++ b/coderd/x/chatd/chatd_internal_test.go
@@ -87,6 +87,115 @@ func (t *testMCPAgentTool) MCPServerConfigID() uuid.UUID {
return t.configID
}
+func TestUpdateChatSummary(t *testing.T) {
+ t.Parallel()
+
+ t.Run("TrimsAndPublishes", func(t *testing.T) {
+ t.Parallel()
+
+ ctrl := gomock.NewController(t)
+ db := dbmock.NewMockStore(ctrl)
+ ps := newRecordingPubsub(dbpubsub.NewInMemory())
+ server := &Server{db: db, pubsub: ps}
+ chat := database.Chat{ID: uuid.New(), OwnerID: uuid.New(), HistoryVersion: 7}
+ logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true})
+ caller := rbac.Subject{
+ ID: chat.OwnerID.String(),
+ Type: rbac.SubjectTypeUser,
+ Roles: rbac.RoleIdentifiers{rbac.RoleMember()},
+ }
+ //nolint:gocritic // Verify updateChatSummary preserves its caller's actor.
+ ctx := dbauthz.As(context.Background(), caller)
+
+ db.EXPECT().UpdateChatSummary(gomock.Any(), database.UpdateChatSummaryParams{
+ ID: chat.ID,
+ ExpectedHistoryVersion: chat.HistoryVersion,
+ Summary: sql.NullString{String: "trimmed summary", Valid: true},
+ }).DoAndReturn(func(ctx context.Context, _ database.UpdateChatSummaryParams) (int64, error) {
+ actor, ok := dbauthz.ActorFromContext(ctx)
+ require.True(t, ok, "summary writes must preserve the caller's actor")
+ require.Equal(t, caller, actor)
+ return 1, nil
+ })
+
+ server.updateChatSummary(ctx, logger, chat, chat.HistoryVersion, " \n trimmed summary\t ")
+
+ events := ps.watchEvents(t)
+ require.Len(t, events, 1)
+ require.Equal(t, codersdk.ChatWatchEventKindChatSummaryChange, events[0].Kind)
+ require.NotNil(t, events[0].Chat.Summary)
+ require.Equal(t, "trimmed summary", *events[0].Chat.Summary)
+ })
+
+ t.Run("SkipsBlankSummary", func(t *testing.T) {
+ t.Parallel()
+
+ ctrl := gomock.NewController(t)
+ db := dbmock.NewMockStore(ctrl)
+ server := &Server{db: db}
+ chat := database.Chat{ID: uuid.New(), OwnerID: uuid.New(), HistoryVersion: 7}
+ logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true})
+
+ server.updateChatSummary(context.Background(), logger, chat, chat.HistoryVersion, " \n\t ")
+ })
+
+ t.Run("SkipsEventOnStaleWrite", func(t *testing.T) {
+ t.Parallel()
+
+ ctrl := gomock.NewController(t)
+ db := dbmock.NewMockStore(ctrl)
+ ps := newRecordingPubsub(dbpubsub.NewInMemory())
+ server := &Server{db: db, pubsub: ps}
+ chat := database.Chat{ID: uuid.New(), OwnerID: uuid.New(), HistoryVersion: 7}
+ logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true})
+
+ db.EXPECT().UpdateChatSummary(gomock.Any(), database.UpdateChatSummaryParams{
+ ID: chat.ID,
+ ExpectedHistoryVersion: chat.HistoryVersion,
+ Summary: sql.NullString{String: "stale summary", Valid: true},
+ }).Return(int64(0), nil)
+
+ server.updateChatSummary(context.Background(), logger, chat, chat.HistoryVersion, "stale summary")
+
+ require.Empty(t, ps.watchEvents(t))
+ })
+}
+
+func TestMaybeGenerateChatSummaryAsync_CloseCancelsInflight(t *testing.T) {
+ t.Parallel()
+
+ ctrl := gomock.NewController(t)
+ db := dbmock.NewMockStore(ctrl)
+ serverCtx, serverCancel := context.WithCancel(context.Background())
+ t.Cleanup(serverCancel)
+ server := &Server{ctx: serverCtx, cancel: serverCancel, db: db}
+ logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true})
+ chat := database.Chat{ID: uuid.New(), OwnerID: uuid.New()}
+ entered := make(chan struct{})
+ db.EXPECT().GetChatByID(gomock.Any(), chat.ID).DoAndReturn(
+ func(readCtx context.Context, _ uuid.UUID) (database.Chat, error) {
+ close(entered)
+ // Hang like an unreachable callee until the context is
+ // canceled; only server shutdown can release this before
+ // chatSummaryWorkTimeout.
+ <-readCtx.Done()
+ return database.Chat{}, readCtx.Err()
+ },
+ )
+
+ ctx := testutil.Context(t, testutil.WaitShort)
+ server.maybeGenerateChatSummaryAsync(ctx, logger, chat)
+
+ testutil.TryReceive(ctx, t, entered)
+
+ closed := make(chan struct{})
+ go func() {
+ defer close(closed)
+ _ = server.Close()
+ }()
+ testutil.TryReceive(ctx, t, closed)
+}
+
func TestComputerUseProviderAndModelFromConfig(t *testing.T) {
t.Parallel()
diff --git a/coderd/x/chatd/quickgen.go b/coderd/x/chatd/quickgen.go
index 36c102a1cac..16e1dea454b 100644
--- a/coderd/x/chatd/quickgen.go
+++ b/coderd/x/chatd/quickgen.go
@@ -9,6 +9,7 @@ import (
"slices"
"strings"
"time"
+ "unicode"
"charm.land/fantasy"
"charm.land/fantasy/object"
@@ -965,6 +966,211 @@ func generateManualTitle(
return title, nil
}
+const chatSummaryGenerationPrompt = "You summarize an AI coding chat for a quick-reference popover. " +
+ "Populate the summary field with 1 to 3 plain sentences describing what the conversation is about and what was accomplished or attempted. " +
+ "Write about the conversation in the third person. " +
+ "Preserve specific identifiers such as PR numbers, repo names, file paths, function names, and error messages. " +
+ "Do not address the user, give instructions, or continue the task. " +
+ "No markdown, lists, headings, code fences, or surrounding quotes."
+
+const (
+ // Bound the transcript so the summary call stays cheap and within context;
+ // long chats keep head and tail turns (see renderChatSummaryTranscript).
+ summaryTranscriptMaxRunes = 16000
+ // Cap a single turn so one long message cannot dominate the budget.
+ summaryTranscriptPerMessageMaxRunes = 4000
+ summaryMaxOutputTokens = 512
+ // Reject pathologically long or verbose summaries, with slack over the
+ // 1-3 sentence target.
+ summaryMaxRunes = 1000
+ summaryMaxSentences = 6
+)
+
+type generatedChatSummary struct {
+ Summary string `json:"summary" description:"1-3 sentence summary of the whole chat"`
+}
+
+// renderChatSummaryTranscript renders chat history as plain text for summary
+// generation. Plain text avoids provider tool-call pairing rules.
+func renderChatSummaryTranscript(messages []database.ChatMessage) string {
+ lines := make([]string, 0, len(messages))
+ for _, message := range messages {
+ var role string
+ switch message.Role {
+ case database.ChatMessageRoleUser:
+ role = "user"
+ case database.ChatMessageRoleAssistant:
+ role = "assistant"
+ default:
+ continue
+ }
+
+ // Keep visible turns plus the compaction summary (model-only but
+ // compressed); skip other model-only messages as noise.
+ visible := message.Visibility == database.ChatMessageVisibilityBoth ||
+ message.Visibility == database.ChatMessageVisibilityUser
+ compactionSummary := message.Visibility == database.ChatMessageVisibilityModel &&
+ message.Compressed
+ if !visible && !compactionSummary {
+ continue
+ }
+
+ parts, err := chatprompt.ParseContent(message)
+ if err != nil {
+ continue
+ }
+ text := strings.TrimSpace(contentBlocksToText(parts))
+ if text == "" {
+ continue
+ }
+ text = truncateRunes(text, summaryTranscriptPerMessageMaxRunes)
+ lines = append(lines, fmt.Sprintf("[%s]: %s", role, text))
+ }
+ return boundTranscriptHeadTail(lines, summaryTranscriptMaxRunes)
+}
+
+// boundTranscriptHeadTail joins lines; if over maxRunes it keeps a head and
+// tail slice with an elision marker between, preserving the chat's start and
+// most recent activity.
+func boundTranscriptHeadTail(lines []string, maxRunes int) string {
+ if len(lines) == 0 {
+ return ""
+ }
+ total := 0
+ for _, line := range lines {
+ total += len([]rune(line)) + 1
+ }
+ if total <= maxRunes {
+ return strings.Join(lines, "\n")
+ }
+
+ half := maxRunes / 2
+ headEnd := 0
+ headRunes := 0
+ for headEnd < len(lines) {
+ n := len([]rune(lines[headEnd])) + 1
+ if headEnd > 0 && headRunes+n > half {
+ break
+ }
+ headRunes += n
+ headEnd++
+ }
+ tailStart := len(lines)
+ tailRunes := 0
+ for tailStart > headEnd {
+ n := len([]rune(lines[tailStart-1])) + 1
+ if tailRunes+n > half {
+ break
+ }
+ tailRunes += n
+ tailStart--
+ }
+
+ var out strings.Builder
+ writeLine := func(line string) {
+ if out.Len() > 0 {
+ _ = out.WriteByte('\n')
+ }
+ _, _ = out.WriteString(line)
+ }
+
+ for _, line := range lines[:headEnd] {
+ writeLine(line)
+ }
+ if tailStart > headEnd {
+ writeLine("[... earlier turns omitted ...]")
+ }
+ for _, line := range lines[tailStart:] {
+ writeLine(line)
+ }
+ return out.String()
+}
+
+// generateChatSummary generates a 1-3 sentence whole-chat summary from a
+// transcript. A blank or invalid result returns an error so callers preserve
+// any existing summary rather than clearing it.
+func generateChatSummary(
+ ctx context.Context,
+ model fantasy.LanguageModel,
+ transcript string,
+) (string, fantasy.Usage, error) {
+ transcript = strings.TrimSpace(transcript)
+ if transcript == "" {
+ return "", fantasy.Usage{}, xerrors.New("chat summary transcript was empty")
+ }
+
+ prompt := fantasy.Prompt{
+ {
+ Role: fantasy.MessageRoleSystem,
+ Content: []fantasy.MessagePart{
+ fantasy.TextPart{Text: chatSummaryGenerationPrompt},
+ },
+ },
+ {
+ Role: fantasy.MessageRoleUser,
+ Content: []fantasy.MessagePart{
+ fantasy.TextPart{Text: transcript},
+ },
+ },
+ }
+
+ maxOutputTokens := int64(summaryMaxOutputTokens)
+ var result *fantasy.ObjectResult[generatedChatSummary]
+ err := chatretry.Retry(ctx, func(retryCtx context.Context) error {
+ var genErr error
+ result, genErr = object.Generate[generatedChatSummary](retryCtx, model, fantasy.ObjectCall{
+ Prompt: prompt,
+ SchemaName: "chat_summary",
+ SchemaDescription: "Summarize the whole chat in 1-3 sentences.",
+ MaxOutputTokens: &maxOutputTokens,
+ })
+ return genErr
+ }, nil)
+ if err != nil {
+ var usage fantasy.Usage
+ if noObjErr, ok := errors.AsType[*fantasy.NoObjectGeneratedError](err); ok {
+ usage = noObjErr.Usage
+ }
+ return "", usage, xerrors.Errorf("generate chat summary: %w", err)
+ }
+
+ summary := normalizeShortTextOutput(result.Object.Summary)
+ if err := validateGeneratedChatSummary(summary); err != nil {
+ return "", result.Usage, err
+ }
+ return summary, result.Usage, nil
+}
+
+func validateGeneratedChatSummary(summary string) error {
+ if summary == "" {
+ return xerrors.New("generated chat summary was empty")
+ }
+ if len([]rune(summary)) > summaryMaxRunes {
+ return xerrors.Errorf("generated chat summary exceeded %d runes", summaryMaxRunes)
+ }
+ if countSentenceTerminators(summary) > summaryMaxSentences {
+ return xerrors.Errorf("generated chat summary exceeded %d sentences", summaryMaxSentences)
+ }
+ return nil
+}
+
+// countSentenceTerminators counts sentence-ending punctuation, but only when
+// followed by whitespace or end-of-text, so periods inside dotted identifiers
+// (pkg.cmd.server, file paths) do not inflate the count.
+func countSentenceTerminators(text string) int {
+ runes := []rune(text)
+ count := 0
+ for i, r := range runes {
+ if r != '.' && r != '!' && r != '?' {
+ continue
+ }
+ if i == len(runes)-1 || unicode.IsSpace(runes[i+1]) {
+ count++
+ }
+ }
+ return count
+}
+
const turnStatusLabelPrompt = "You write compact chat status labels for a sidebar or push notification. " +
"Given a chat title, current chat state, and the agent's latest message, populate the label field with a 2-5 word status label. " +
"Describe the chat's current state, not the agent. " +
diff --git a/coderd/x/chatd/summarygen_internal_test.go b/coderd/x/chatd/summarygen_internal_test.go
new file mode 100644
index 00000000000..d12a108bebe
--- /dev/null
+++ b/coderd/x/chatd/summarygen_internal_test.go
@@ -0,0 +1,246 @@
+package chatd
+
+import (
+ "database/sql"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/x/chatd/chatprompt"
+ "github.com/coder/coder/v2/codersdk"
+)
+
+func summaryTestMessage(
+ t *testing.T,
+ id int64,
+ role database.ChatMessageRole,
+ visibility database.ChatMessageVisibility,
+ parts []codersdk.ChatMessagePart,
+ compressed bool,
+ createdAt time.Time,
+) database.ChatMessage {
+ t.Helper()
+ content, err := chatprompt.MarshalParts(parts)
+ require.NoError(t, err)
+ return database.ChatMessage{
+ ID: id,
+ Role: role,
+ Visibility: visibility,
+ Content: content,
+ ContentVersion: chatprompt.CurrentContentVersion,
+ Compressed: compressed,
+ CreatedAt: createdAt,
+ }
+}
+
+func summaryTextMessage(
+ t *testing.T,
+ id int64,
+ role database.ChatMessageRole,
+ visibility database.ChatMessageVisibility,
+ text string,
+ compressed bool,
+ createdAt time.Time,
+) database.ChatMessage {
+ t.Helper()
+ return summaryTestMessage(t, id, role, visibility,
+ []codersdk.ChatMessagePart{codersdk.ChatMessageText(text)},
+ compressed, createdAt)
+}
+
+func TestRenderChatSummaryTranscript(t *testing.T) {
+ t.Parallel()
+
+ base := time.Date(2026, 6, 24, 0, 0, 0, 0, time.UTC)
+ messages := []database.ChatMessage{
+ summaryTextMessage(t, 1, database.ChatMessageRoleUser, database.ChatMessageVisibilityModel, "you are a helpful agent", false, base),
+ // Compaction summary (model-only but compressed) is kept.
+ summaryTextMessage(t, 2, database.ChatMessageRoleUser, database.ChatMessageVisibilityModel, "earlier work compaction summary", true, base.Add(time.Minute)),
+ // Injected context (model-only, not compressed) is skipped as noise.
+ summaryTextMessage(t, 3, database.ChatMessageRoleUser, database.ChatMessageVisibilityModel, "AGENTS.md injected context", false, base.Add(2*time.Minute)),
+ summaryTextMessage(t, 4, database.ChatMessageRoleUser, database.ChatMessageVisibilityBoth, "fix the bug in foo.go", false, base.Add(3*time.Minute)),
+ summaryTextMessage(t, 8, database.ChatMessageRoleUser, database.ChatMessageVisibilityUser, "and please keep it simple", false, base.Add(3*time.Minute+30*time.Second)),
+ summaryTestMessage(t, 5, database.ChatMessageRoleAssistant, database.ChatMessageVisibilityBoth,
+ []codersdk.ChatMessagePart{codersdk.ChatMessageToolCall("call-1", "bash", []byte(`{"cmd":"go test"}`))},
+ false, base.Add(4*time.Minute)),
+ summaryTextMessage(t, 6, database.ChatMessageRoleTool, database.ChatMessageVisibilityBoth, "tests passed", false, base.Add(5*time.Minute)),
+ summaryTextMessage(t, 7, database.ChatMessageRoleAssistant, database.ChatMessageVisibilityBoth, "fixed the bug and added a test", false, base.Add(6*time.Minute)),
+ }
+
+ transcript := renderChatSummaryTranscript(messages)
+
+ require.Contains(t, transcript, "earlier work compaction summary")
+ require.Contains(t, transcript, "[user]: fix the bug in foo.go")
+ require.Contains(t, transcript, "[user]: and please keep it simple")
+ require.Contains(t, transcript, "[assistant]: fixed the bug and added a test")
+ // System prompt, injected context, tool-call, and tool result are excluded.
+ require.NotContains(t, transcript, "you are a helpful agent")
+ require.NotContains(t, transcript, "AGENTS.md injected context")
+ require.NotContains(t, transcript, "tests passed")
+ require.NotContains(t, transcript, "go test")
+}
+
+func TestBoundTranscriptHeadTail(t *testing.T) {
+ t.Parallel()
+
+ t.Run("UnderBudgetReturnsAll", func(t *testing.T) {
+ t.Parallel()
+ lines := []string{"a", "b", "c"}
+ require.Equal(t, "a\nb\nc", boundTranscriptHeadTail(lines, 1000))
+ })
+
+ t.Run("OverBudgetKeepsHeadAndTail", func(t *testing.T) {
+ t.Parallel()
+ lines := []string{
+ "HEAD-FIRST " + strings.Repeat("x", 40),
+ strings.Repeat("m", 200),
+ strings.Repeat("n", 200),
+ strings.Repeat("o", 200),
+ "TAIL-LAST " + strings.Repeat("y", 40),
+ }
+ out := boundTranscriptHeadTail(lines, 160)
+ require.Contains(t, out, "HEAD-FIRST")
+ require.Contains(t, out, "TAIL-LAST")
+ require.Contains(t, out, "[... earlier turns omitted ...]")
+ })
+}
+
+func TestShouldGenerateChatSummary(t *testing.T) {
+ t.Parallel()
+
+ base := time.Date(2026, 6, 24, 0, 0, 0, 0, time.UTC)
+ userMsg := func(id int64, at time.Time) database.ChatMessage {
+ return database.ChatMessage{
+ ID: id,
+ Role: database.ChatMessageRoleUser,
+ Visibility: database.ChatMessageVisibilityBoth,
+ CreatedAt: at,
+ }
+ }
+ assistantMsg := func(id int64, at time.Time) database.ChatMessage {
+ return database.ChatMessage{
+ ID: id,
+ Role: database.ChatMessageRoleAssistant,
+ Visibility: database.ChatMessageVisibilityBoth,
+ CreatedAt: at,
+ }
+ }
+
+ t.Run("FirstSummaryAtFirstTurn", func(t *testing.T) {
+ t.Parallel()
+ chat := database.Chat{}
+ msgs := []database.ChatMessage{
+ userMsg(1, base),
+ assistantMsg(2, base.Add(time.Minute)),
+ }
+ require.True(t, shouldGenerateChatSummary(chat, msgs))
+ })
+
+ t.Run("FirstSummarySkippedWithNoTurns", func(t *testing.T) {
+ t.Parallel()
+ chat := database.Chat{}
+ require.False(t, shouldGenerateChatSummary(chat, nil))
+ })
+
+ t.Run("MultiStepTurnDoesNotInflateCount", func(t *testing.T) {
+ t.Parallel()
+ marker := base
+ chat := database.Chat{
+ Summary: sql.NullString{String: "existing", Valid: true},
+ SummaryGeneratedAt: sql.NullTime{Time: marker, Valid: true},
+ }
+ // One user turn plus many assistant steps stays below the threshold.
+ msgs := []database.ChatMessage{
+ userMsg(1, marker.Add(time.Minute)),
+ assistantMsg(2, marker.Add(2*time.Minute)),
+ assistantMsg(3, marker.Add(3*time.Minute)),
+ assistantMsg(4, marker.Add(4*time.Minute)),
+ }
+ require.False(t, shouldGenerateChatSummary(chat, msgs))
+ })
+
+ t.Run("ModelOnlyUserMessagesAreNotTurns", func(t *testing.T) {
+ t.Parallel()
+ marker := base
+ chat := database.Chat{
+ Summary: sql.NullString{String: "existing", Valid: true},
+ SummaryGeneratedAt: sql.NullTime{Time: marker, Valid: true},
+ }
+ modelOnlyUserMsg := func(id int64, at time.Time) database.ChatMessage {
+ return database.ChatMessage{
+ ID: id,
+ Role: database.ChatMessageRoleUser,
+ Visibility: database.ChatMessageVisibilityModel,
+ CreatedAt: at,
+ }
+ }
+ // The model-only user message must not count as a turn, else these
+ // three messages would trip the threshold of 3.
+ msgs := []database.ChatMessage{
+ userMsg(1, marker.Add(time.Minute)),
+ modelOnlyUserMsg(2, marker.Add(2*time.Minute)),
+ userMsg(3, marker.Add(3*time.Minute)),
+ }
+ require.False(t, shouldGenerateChatSummary(chat, msgs))
+ })
+
+ t.Run("RefreshAfterThresholdTurns", func(t *testing.T) {
+ t.Parallel()
+ marker := base
+ chat := database.Chat{
+ Summary: sql.NullString{String: "existing", Valid: true},
+ SummaryGeneratedAt: sql.NullTime{Time: marker, Valid: true},
+ }
+ msgs := []database.ChatMessage{
+ // Pre-marker turn is not counted.
+ userMsg(1, marker.Add(-time.Minute)),
+ userMsg(2, marker.Add(time.Minute)),
+ userMsg(3, marker.Add(2*time.Minute)),
+ userMsg(4, marker.Add(3*time.Minute)),
+ }
+ require.True(t, shouldGenerateChatSummary(chat, msgs))
+ })
+
+ t.Run("PreMarkerTurnsAreNotCounted", func(t *testing.T) {
+ t.Parallel()
+ marker := base
+ chat := database.Chat{
+ Summary: sql.NullString{String: "existing", Valid: true},
+ SummaryGeneratedAt: sql.NullTime{Time: marker, Valid: true},
+ }
+ // The pre-marker turn would tip the total to the threshold; this stays
+ // false only because countCompletedTurnsSince excludes pre-marker turns.
+ msgs := []database.ChatMessage{
+ userMsg(1, marker.Add(-time.Minute)),
+ userMsg(2, marker.Add(time.Minute)),
+ userMsg(3, marker.Add(2*time.Minute)),
+ }
+ require.False(t, shouldGenerateChatSummary(chat, msgs))
+ })
+}
+
+func TestValidateGeneratedChatSummary(t *testing.T) {
+ t.Parallel()
+
+ require.Error(t, validateGeneratedChatSummary(""))
+ require.Error(t, validateGeneratedChatSummary(strings.Repeat("a", summaryMaxRunes+1)))
+ require.Error(t, validateGeneratedChatSummary("One. Two. Three. Four. Five. Six. Seven."))
+ require.NoError(t, validateGeneratedChatSummary("Implemented the summary feature. Added tests."))
+}
+
+func TestCountSentenceTerminators(t *testing.T) {
+ t.Parallel()
+
+ // Periods inside dotted identifiers (pkg.cmd.server) are not boundaries.
+ require.Equal(t, 2, countSentenceTerminators("Fixed pkg.cmd.server in file.go. Added a test."))
+ require.Equal(t, 3, countSentenceTerminators("One. Two! Three?"))
+ require.Equal(t, 0, countSentenceTerminators("auth.rbac.Policy"))
+
+ // Dotted identifiers must not push a valid summary over the sentence cap.
+ require.NoError(t, validateGeneratedChatSummary(
+ "Refactored pkg.cmd.server and auth.rbac.Policy in main.go and util.go. Added coverage in foo_test.go.",
+ ))
+}
diff --git a/codersdk/chats.go b/codersdk/chats.go
index c6990e8a776..f72c852b881 100644
--- a/codersdk/chats.go
+++ b/codersdk/chats.go
@@ -105,27 +105,30 @@ const (
// Chat represents a chat session with an AI agent.
type Chat struct {
- ID uuid.UUID `json:"id" format:"uuid"`
- OrganizationID uuid.UUID `json:"organization_id" format:"uuid"`
- OwnerID uuid.UUID `json:"owner_id" format:"uuid"`
- OwnerUsername string `json:"owner_username,omitempty"`
- OwnerName string `json:"owner_name,omitempty"`
- WorkspaceID *uuid.UUID `json:"workspace_id,omitempty" format:"uuid"`
- BuildID *uuid.UUID `json:"build_id,omitempty" format:"uuid"`
- AgentID *uuid.UUID `json:"agent_id,omitempty" format:"uuid"`
- ParentChatID *uuid.UUID `json:"parent_chat_id,omitempty" format:"uuid"`
- RootChatID *uuid.UUID `json:"root_chat_id,omitempty" format:"uuid"`
- LastModelConfigID uuid.UUID `json:"last_model_config_id" format:"uuid"`
- LastReasoningEffort *string `json:"last_reasoning_effort,omitempty"`
- Title string `json:"title"`
- Status ChatStatus `json:"status"`
- PlanMode ChatPlanMode `json:"plan_mode,omitempty"`
- LastError *ChatError `json:"last_error,omitempty"`
- LastTurnSummary *string `json:"last_turn_summary"`
- DiffStatus *ChatDiffStatus `json:"diff_status,omitempty"`
- CreatedAt time.Time `json:"created_at" format:"date-time"`
- UpdatedAt time.Time `json:"updated_at" format:"date-time"`
- Archived bool `json:"archived"`
+ ID uuid.UUID `json:"id" format:"uuid"`
+ OrganizationID uuid.UUID `json:"organization_id" format:"uuid"`
+ OwnerID uuid.UUID `json:"owner_id" format:"uuid"`
+ OwnerUsername string `json:"owner_username,omitempty"`
+ OwnerName string `json:"owner_name,omitempty"`
+ WorkspaceID *uuid.UUID `json:"workspace_id,omitempty" format:"uuid"`
+ BuildID *uuid.UUID `json:"build_id,omitempty" format:"uuid"`
+ AgentID *uuid.UUID `json:"agent_id,omitempty" format:"uuid"`
+ ParentChatID *uuid.UUID `json:"parent_chat_id,omitempty" format:"uuid"`
+ RootChatID *uuid.UUID `json:"root_chat_id,omitempty" format:"uuid"`
+ LastModelConfigID uuid.UUID `json:"last_model_config_id" format:"uuid"`
+ LastReasoningEffort *string `json:"last_reasoning_effort,omitempty"`
+ Title string `json:"title"`
+ Status ChatStatus `json:"status"`
+ PlanMode ChatPlanMode `json:"plan_mode,omitempty"`
+ LastError *ChatError `json:"last_error,omitempty"`
+ LastTurnSummary *string `json:"last_turn_summary"`
+ // Summary is the persisted whole-chat summary, generated in the background.
+ // It is nil until the first summary has been produced.
+ Summary *string `json:"summary"`
+ DiffStatus *ChatDiffStatus `json:"diff_status,omitempty"`
+ CreatedAt time.Time `json:"created_at" format:"date-time"`
+ UpdatedAt time.Time `json:"updated_at" format:"date-time"`
+ Archived bool `json:"archived"`
// Shared is true when this chat's root chat has explicit user or group ACL entries.
Shared bool `json:"shared"`
PinOrder int32 `json:"pin_order"`
@@ -1860,13 +1863,17 @@ func NewDynamicTool[T any](
type ChatWatchEventKind string
const (
- ChatWatchEventKindStatusChange ChatWatchEventKind = "status_change"
- ChatWatchEventKindSummaryChange ChatWatchEventKind = "summary_change"
- ChatWatchEventKindTitleChange ChatWatchEventKind = "title_change"
- ChatWatchEventKindCreated ChatWatchEventKind = "created"
- ChatWatchEventKindDeleted ChatWatchEventKind = "deleted"
- ChatWatchEventKindDiffStatusChange ChatWatchEventKind = "diff_status_change"
- ChatWatchEventKindActionRequired ChatWatchEventKind = "action_required"
+ ChatWatchEventKindStatusChange ChatWatchEventKind = "status_change"
+ ChatWatchEventKindSummaryChange ChatWatchEventKind = "summary_change"
+ // ChatWatchEventKindChatSummaryChange carries the persisted whole-chat
+ // summary. It is distinct from SummaryChange (bound to last_turn_summary) so
+ // the frontend updates one field without disturbing the other.
+ ChatWatchEventKindChatSummaryChange ChatWatchEventKind = "chat_summary_change"
+ ChatWatchEventKindTitleChange ChatWatchEventKind = "title_change"
+ ChatWatchEventKindCreated ChatWatchEventKind = "created"
+ ChatWatchEventKindDeleted ChatWatchEventKind = "deleted"
+ ChatWatchEventKindDiffStatusChange ChatWatchEventKind = "diff_status_change"
+ ChatWatchEventKindActionRequired ChatWatchEventKind = "action_required"
// ChatWatchEventKindContextDirty signals that the chat's pinned
// workspace context changed: it drifted from the agent's latest
// pushed snapshot, or hydration first populated it (a first-turn
diff --git a/docs/admin/security/audit-logs.md b/docs/admin/security/audit-logs.md
index af5967e908a..b83bcdc42ae 100644
--- a/docs/admin/security/audit-logs.md
+++ b/docs/admin/security/audit-logs.md
@@ -13,41 +13,41 @@ We track the following resources:
-| Resource | | |
-|-----------------------------------------------------------------|----------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| AIGatewayKey
create, delete |
| Field | Tracked |
| | created_at | false |
| hashed_secret | true |
| id | true |
| last_heartbeat_at | false |
| name | true |
| secret_prefix | true |
|
-| AIProvider
create, write, delete | | Field | Tracked |
| | base_url | true |
| created_at | false |
| deleted | true |
| display_name | true |
| enabled | true |
| icon | true |
| id | true |
| name | true |
| settings | true |
| settings_key_id | false |
| type | true |
| updated_at | false |
|
-| AIProviderKey
create, delete | | Field | Tracked |
| | api_key | true |
| api_key_key_id | false |
| created_at | false |
| id | true |
| provider_id | true |
| updated_at | false |
|
-| AISeatState
create | | Field | Tracked |
| | first_used_at | true |
| last_event_description | true |
| last_event_type | true |
| last_used_at | false |
| updated_at | false |
| user_id | true |
|
-| APIKey
login, logout, register, create, write, delete | | Field | Tracked |
| | allow_list | false |
| created_at | true |
| expires_at | true |
| hashed_secret | false |
| id | false |
| ip_address | false |
| last_used | true |
| lifetime_seconds | false |
| login_type | false |
| scopes | false |
| token_name | false |
| updated_at | false |
| user_id | true |
|
-| AuditOAuthConvertState
| | Field | Tracked |
| | created_at | true |
| expires_at | true |
| from_login_type | true |
| to_login_type | true |
| user_id | true |
|
-| Group
create, write, delete | | Field | Tracked |
| | avatar_url | true |
| chat_spend_limit_micros | true |
| display_name | true |
| id | true |
| members | true |
| name | true |
| organization_id | false |
| quota_allowance | true |
| source | false |
|
-| AuditableGroupAIBudget
write, delete | | Field | Tracked |
| | created_at | false |
| group_id | false |
| group_name | false |
| spend_limit | true |
| spend_limit_micros | false |
| updated_at | false |
|
-| AuditableOrganizationMember
| | Field | Tracked |
| | created_at | true |
| organization_id | false |
| roles | true |
| updated_at | true |
| user_id | true |
| username | true |
|
-| AuditableUserAIBudgetOverride
write, delete | | Field | Tracked |
| | created_at | false |
| group_id | true |
| group_name | true |
| spend_limit | true |
| spend_limit_micros | false |
| updated_at | false |
| user_id | false |
| username | false |
|
-| Chat
create, write | | Field | Tracked |
| | agent_id | false |
| archived | true |
| build_id | false |
| client_type | false |
| compaction_requested_at | false |
| context_aggregate_hash | false |
| context_dirty_resources | false |
| context_dirty_since | false |
| context_error | false |
| created_at | false |
| dynamic_tools | false |
| generation_attempt | false |
| group_acl | true |
| heartbeat_at | false |
| history_version | false |
| id | true |
| labels | true |
| last_error | false |
| last_model_config_id | false |
| last_read_message_id | false |
| last_reasoning_effort | false |
| last_turn_summary | false |
| mcp_server_ids | true |
| mode | true |
| organization_id | false |
| owner_id | true |
| owner_name | false |
| owner_username | false |
| parent_chat_id | false |
| pin_order | true |
| plan_mode | false |
| queue_version | false |
| requires_action_deadline_at | false |
| retry_state | false |
| retry_state_version | false |
| root_chat_id | false |
| runner_id | false |
| snapshot_version | false |
| started_at | false |
| status | false |
| title | true |
| updated_at | false |
| user_acl | true |
| worker_id | false |
| workspace_id | true |
|
-| CustomRole
| | Field | Tracked |
| | created_at | false |
| display_name | true |
| id | false |
| is_system | false |
| member_permissions | true |
| name | true |
| org_permissions | true |
| organization_id | false |
| site_permissions | true |
| updated_at | false |
| user_permissions | true |
|
-| GitSSHKey
create | | Field | Tracked |
| | created_at | false |
| private_key | true |
| private_key_key_id | false |
| public_key | true |
| updated_at | false |
| user_id | true |
|
-| GroupSyncSettings
| | Field | Tracked |
| | auto_create_missing_groups | true |
| field | true |
| legacy_group_name_mapping | false |
| mapping | true |
| regex_filter | true |
|
-| HealthSettings
| | Field | Tracked |
| | dismissed_healthchecks | true |
| id | false |
|
-| License
create, delete | | Field | Tracked |
| | exp | true |
| id | false |
| jwt | false |
| uploaded_at | true |
| uuid | true |
|
-| NotificationTemplate
| | Field | Tracked |
| | actions | true |
| body_template | true |
| enabled_by_default | true |
| group | true |
| id | false |
| kind | true |
| method | true |
| name | true |
| title_template | true |
|
-| NotificationsSettings
| | Field | Tracked |
| | id | false |
| notifier_paused | true |
|
-| OAuth2ProviderApp
| | Field | Tracked |
| | callback_url | true |
| client_id_issued_at | false |
| client_secret_expires_at | true |
| client_type | true |
| client_uri | true |
| contacts | true |
| created_at | false |
| dynamically_registered | true |
| grant_types | true |
| icon | true |
| id | false |
| jwks | true |
| jwks_uri | true |
| logo_uri | true |
| name | true |
| policy_uri | true |
| redirect_uris | true |
| registration_access_token | true |
| registration_client_uri | true |
| response_types | true |
| scope | true |
| software_id | true |
| software_version | true |
| token_endpoint_auth_method | true |
| tos_uri | true |
| updated_at | false |
|
-| OAuth2ProviderAppSecret
| | Field | Tracked |
| | app_id | false |
| created_at | false |
| display_secret | false |
| hashed_secret | false |
| id | false |
| last_used_at | false |
| secret_prefix | false |
|
-| Organization
| | Field | Tracked |
| | created_at | false |
| default_org_member_roles | true |
| deleted | true |
| description | true |
| display_name | true |
| icon | true |
| id | false |
| is_default | true |
| name | true |
| shareable_workspace_owners | true |
| updated_at | true |
|
-| OrganizationSyncSettings
| | Field | Tracked |
| | assign_default | true |
| field | true |
| mapping | true |
|
-| PrebuildsSettings
| | Field | Tracked |
| | id | false |
| reconciliation_paused | true |
|
-| RoleSyncSettings
| | Field | Tracked |
| | field | true |
| mapping | true |
|
-| TaskTable
| | Field | Tracked |
| | created_at | false |
| deleted_at | false |
| display_name | true |
| id | true |
| name | true |
| organization_id | false |
| owner_id | true |
| prompt | true |
| template_parameters | true |
| template_version_id | true |
| workspace_id | true |
|
-| Template
write, delete | | Field | Tracked |
| | active_version_id | true |
| activity_bump | true |
| allow_user_autostart | true |
| allow_user_autostop | true |
| allow_user_cancel_workspace_jobs | true |
| autostart_block_days_of_week | true |
| autostop_requirement_days_of_week | true |
| autostop_requirement_weeks | true |
| cors_behavior | true |
| created_at | false |
| created_by | true |
| created_by_avatar_url | false |
| created_by_name | false |
| created_by_username | false |
| default_ttl | true |
| deleted | false |
| deprecated | true |
| description | true |
| disable_module_cache | true |
| display_name | true |
| failure_ttl | true |
| group_acl | true |
| icon | true |
| id | true |
| max_port_sharing_level | true |
| name | true |
| organization_display_name | false |
| organization_icon | false |
| organization_id | false |
| organization_name | false |
| provisioner | true |
| require_active_version | true |
| time_til_autostop_notify | true |
| time_til_dormant | true |
| time_til_dormant_autodelete | true |
| updated_at | false |
| use_classic_parameter_flow | true |
| user_acl | true |
|
-| TemplateVersion
create, write | | Field | Tracked |
| | archived | true |
| created_at | false |
| created_by | true |
| created_by_avatar_url | false |
| created_by_name | false |
| created_by_username | false |
| external_auth_providers | false |
| has_ai_task | false |
| has_external_agent | false |
| id | true |
| job_id | false |
| message | false |
| name | true |
| organization_id | false |
| readme | true |
| source_example_id | false |
| template_id | true |
| updated_at | false |
|
-| User
create, write, delete | | Field | Tracked |
| | avatar_url | false |
| chat_spend_limit_micros | true |
| created_at | false |
| deleted | true |
| email | true |
| github_com_user_id | false |
| hashed_one_time_passcode | false |
| hashed_password | true |
| id | true |
| is_service_account | true |
| is_system | true |
| last_seen_at | false |
| login_type | true |
| name | true |
| one_time_passcode_expires_at | true |
| quiet_hours_schedule | true |
| rbac_roles | true |
| status | true |
| updated_at | false |
| username | true |
|
-| UserSecret
create, write, delete | | Field | Tracked |
| | created_at | false |
| description | true |
| env_name | true |
| file_path | true |
| id | true |
| name | true |
| updated_at | false |
| user_id | true |
| value | true |
| value_key_id | false |
|
-| UserSkill
create, write, delete | | Field | Tracked |
| | content | true |
| created_at | false |
| description | true |
| id | true |
| name | true |
| updated_at | false |
| user_id | true |
|
-| WorkspaceBuild
start, stop | | Field | Tracked |
| | build_number | false |
| created_at | false |
| daily_cost | false |
| deadline | false |
| has_ai_task | false |
| has_external_agent | false |
| id | false |
| initiator_by_avatar_url | false |
| initiator_by_name | false |
| initiator_by_username | false |
| initiator_id | false |
| job_id | false |
| max_deadline | false |
| notified_autostop_deadline | false |
| reason | false |
| template_version_id | true |
| template_version_preset_id | false |
| transition | false |
| updated_at | false |
| workspace_id | false |
|
-| WorkspaceProxy
| | Field | Tracked |
| | created_at | true |
| deleted | false |
| derp_enabled | true |
| derp_only | true |
| display_name | true |
| icon | true |
| id | true |
| name | true |
| region_id | true |
| token_hashed_secret | true |
| updated_at | false |
| url | true |
| version | true |
| wildcard_hostname | true |
|
-| WorkspaceTable
| | Field | Tracked |
| | automatic_updates | true |
| autostart_schedule | true |
| created_at | false |
| deleted | false |
| deleting_at | true |
| dormant_at | true |
| favorite | true |
| group_acl | true |
| id | true |
| last_used_at | false |
| name | true |
| next_start_at | true |
| organization_id | false |
| owner_id | true |
| template_id | true |
| ttl | true |
| updated_at | false |
| user_acl | true |
|
+| Resource | | |
+|-----------------------------------------------------------------|----------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| AIGatewayKey
create, delete | | Field | Tracked |
| | created_at | false |
| hashed_secret | true |
| id | true |
| last_heartbeat_at | false |
| name | true |
| secret_prefix | true |
|
+| AIProvider
create, write, delete | | Field | Tracked |
| | base_url | true |
| created_at | false |
| deleted | true |
| display_name | true |
| enabled | true |
| icon | true |
| id | true |
| name | true |
| settings | true |
| settings_key_id | false |
| type | true |
| updated_at | false |
|
+| AIProviderKey
create, delete | | Field | Tracked |
| | api_key | true |
| api_key_key_id | false |
| created_at | false |
| id | true |
| provider_id | true |
| updated_at | false |
|
+| AISeatState
create | | Field | Tracked |
| | first_used_at | true |
| last_event_description | true |
| last_event_type | true |
| last_used_at | false |
| updated_at | false |
| user_id | true |
|
+| APIKey
login, logout, register, create, write, delete | | Field | Tracked |
| | allow_list | false |
| created_at | true |
| expires_at | true |
| hashed_secret | false |
| id | false |
| ip_address | false |
| last_used | true |
| lifetime_seconds | false |
| login_type | false |
| scopes | false |
| token_name | false |
| updated_at | false |
| user_id | true |
|
+| AuditOAuthConvertState
| | Field | Tracked |
| | created_at | true |
| expires_at | true |
| from_login_type | true |
| to_login_type | true |
| user_id | true |
|
+| Group
create, write, delete | | Field | Tracked |
| | avatar_url | true |
| chat_spend_limit_micros | true |
| display_name | true |
| id | true |
| members | true |
| name | true |
| organization_id | false |
| quota_allowance | true |
| source | false |
|
+| AuditableGroupAIBudget
write, delete | | Field | Tracked |
| | created_at | false |
| group_id | false |
| group_name | false |
| spend_limit | true |
| spend_limit_micros | false |
| updated_at | false |
|
+| AuditableOrganizationMember
| | Field | Tracked |
| | created_at | true |
| organization_id | false |
| roles | true |
| updated_at | true |
| user_id | true |
| username | true |
|
+| AuditableUserAIBudgetOverride
write, delete | | Field | Tracked |
| | created_at | false |
| group_id | true |
| group_name | true |
| spend_limit | true |
| spend_limit_micros | false |
| updated_at | false |
| user_id | false |
| username | false |
|
+| Chat
create, write | | Field | Tracked |
| | agent_id | false |
| archived | true |
| build_id | false |
| client_type | false |
| compaction_requested_at | false |
| context_aggregate_hash | false |
| context_dirty_resources | false |
| context_dirty_since | false |
| context_error | false |
| created_at | false |
| dynamic_tools | false |
| generation_attempt | false |
| group_acl | true |
| heartbeat_at | false |
| history_version | false |
| id | true |
| labels | true |
| last_error | false |
| last_model_config_id | false |
| last_read_message_id | false |
| last_reasoning_effort | false |
| last_turn_summary | false |
| mcp_server_ids | true |
| mode | true |
| organization_id | false |
| owner_id | true |
| owner_name | false |
| owner_username | false |
| parent_chat_id | false |
| pin_order | true |
| plan_mode | false |
| queue_version | false |
| requires_action_deadline_at | false |
| retry_state | false |
| retry_state_version | false |
| root_chat_id | false |
| runner_id | false |
| snapshot_version | false |
| started_at | false |
| status | false |
| summary | false |
| summary_generated_at | false |
| title | true |
| updated_at | false |
| user_acl | true |
| worker_id | false |
| workspace_id | true |
|
+| CustomRole
| | Field | Tracked |
| | created_at | false |
| display_name | true |
| id | false |
| is_system | false |
| member_permissions | true |
| name | true |
| org_permissions | true |
| organization_id | false |
| site_permissions | true |
| updated_at | false |
| user_permissions | true |
|
+| GitSSHKey
create | | Field | Tracked |
| | created_at | false |
| private_key | true |
| private_key_key_id | false |
| public_key | true |
| updated_at | false |
| user_id | true |
|
+| GroupSyncSettings
| | Field | Tracked |
| | auto_create_missing_groups | true |
| field | true |
| legacy_group_name_mapping | false |
| mapping | true |
| regex_filter | true |
|
+| HealthSettings
| | Field | Tracked |
| | dismissed_healthchecks | true |
| id | false |
|
+| License
create, delete | | Field | Tracked |
| | exp | true |
| id | false |
| jwt | false |
| uploaded_at | true |
| uuid | true |
|
+| NotificationTemplate
| | Field | Tracked |
| | actions | true |
| body_template | true |
| enabled_by_default | true |
| group | true |
| id | false |
| kind | true |
| method | true |
| name | true |
| title_template | true |
|
+| NotificationsSettings
| | Field | Tracked |
| | id | false |
| notifier_paused | true |
|
+| OAuth2ProviderApp
| | Field | Tracked |
| | callback_url | true |
| client_id_issued_at | false |
| client_secret_expires_at | true |
| client_type | true |
| client_uri | true |
| contacts | true |
| created_at | false |
| dynamically_registered | true |
| grant_types | true |
| icon | true |
| id | false |
| jwks | true |
| jwks_uri | true |
| logo_uri | true |
| name | true |
| policy_uri | true |
| redirect_uris | true |
| registration_access_token | true |
| registration_client_uri | true |
| response_types | true |
| scope | true |
| software_id | true |
| software_version | true |
| token_endpoint_auth_method | true |
| tos_uri | true |
| updated_at | false |
|
+| OAuth2ProviderAppSecret
| | Field | Tracked |
| | app_id | false |
| created_at | false |
| display_secret | false |
| hashed_secret | false |
| id | false |
| last_used_at | false |
| secret_prefix | false |
|
+| Organization
| | Field | Tracked |
| | created_at | false |
| default_org_member_roles | true |
| deleted | true |
| description | true |
| display_name | true |
| icon | true |
| id | false |
| is_default | true |
| name | true |
| shareable_workspace_owners | true |
| updated_at | true |
|
+| OrganizationSyncSettings
| | Field | Tracked |
| | assign_default | true |
| field | true |
| mapping | true |
|
+| PrebuildsSettings
| | Field | Tracked |
| | id | false |
| reconciliation_paused | true |
|
+| RoleSyncSettings
| | Field | Tracked |
| | field | true |
| mapping | true |
|
+| TaskTable
| | Field | Tracked |
| | created_at | false |
| deleted_at | false |
| display_name | true |
| id | true |
| name | true |
| organization_id | false |
| owner_id | true |
| prompt | true |
| template_parameters | true |
| template_version_id | true |
| workspace_id | true |
|
+| Template
write, delete | | Field | Tracked |
| | active_version_id | true |
| activity_bump | true |
| allow_user_autostart | true |
| allow_user_autostop | true |
| allow_user_cancel_workspace_jobs | true |
| autostart_block_days_of_week | true |
| autostop_requirement_days_of_week | true |
| autostop_requirement_weeks | true |
| cors_behavior | true |
| created_at | false |
| created_by | true |
| created_by_avatar_url | false |
| created_by_name | false |
| created_by_username | false |
| default_ttl | true |
| deleted | false |
| deprecated | true |
| description | true |
| disable_module_cache | true |
| display_name | true |
| failure_ttl | true |
| group_acl | true |
| icon | true |
| id | true |
| max_port_sharing_level | true |
| name | true |
| organization_display_name | false |
| organization_icon | false |
| organization_id | false |
| organization_name | false |
| provisioner | true |
| require_active_version | true |
| time_til_autostop_notify | true |
| time_til_dormant | true |
| time_til_dormant_autodelete | true |
| updated_at | false |
| use_classic_parameter_flow | true |
| user_acl | true |
|
+| TemplateVersion
create, write | | Field | Tracked |
| | archived | true |
| created_at | false |
| created_by | true |
| created_by_avatar_url | false |
| created_by_name | false |
| created_by_username | false |
| external_auth_providers | false |
| has_ai_task | false |
| has_external_agent | false |
| id | true |
| job_id | false |
| message | false |
| name | true |
| organization_id | false |
| readme | true |
| source_example_id | false |
| template_id | true |
| updated_at | false |
|
+| User
create, write, delete | | Field | Tracked |
| | avatar_url | false |
| chat_spend_limit_micros | true |
| created_at | false |
| deleted | true |
| email | true |
| github_com_user_id | false |
| hashed_one_time_passcode | false |
| hashed_password | true |
| id | true |
| is_service_account | true |
| is_system | true |
| last_seen_at | false |
| login_type | true |
| name | true |
| one_time_passcode_expires_at | true |
| quiet_hours_schedule | true |
| rbac_roles | true |
| status | true |
| updated_at | false |
| username | true |
|
+| UserSecret
create, write, delete | | Field | Tracked |
| | created_at | false |
| description | true |
| env_name | true |
| file_path | true |
| id | true |
| name | true |
| updated_at | false |
| user_id | true |
| value | true |
| value_key_id | false |
|
+| UserSkill
create, write, delete | | Field | Tracked |
| | content | true |
| created_at | false |
| description | true |
| id | true |
| name | true |
| updated_at | false |
| user_id | true |
|
+| WorkspaceBuild
start, stop | | Field | Tracked |
| | build_number | false |
| created_at | false |
| daily_cost | false |
| deadline | false |
| has_ai_task | false |
| has_external_agent | false |
| id | false |
| initiator_by_avatar_url | false |
| initiator_by_name | false |
| initiator_by_username | false |
| initiator_id | false |
| job_id | false |
| max_deadline | false |
| notified_autostop_deadline | false |
| reason | false |
| template_version_id | true |
| template_version_preset_id | false |
| transition | false |
| updated_at | false |
| workspace_id | false |
|
+| WorkspaceProxy
| | Field | Tracked |
| | created_at | true |
| deleted | false |
| derp_enabled | true |
| derp_only | true |
| display_name | true |
| icon | true |
| id | true |
| name | true |
| region_id | true |
| token_hashed_secret | true |
| updated_at | false |
| url | true |
| version | true |
| wildcard_hostname | true |
|
+| WorkspaceTable
| | Field | Tracked |
| | automatic_updates | true |
| autostart_schedule | true |
| created_at | false |
| deleted | false |
| deleting_at | true |
| dormant_at | true |
| favorite | true |
| group_acl | true |
| id | true |
| last_used_at | false |
| name | true |
| next_start_at | true |
| organization_id | false |
| owner_id | true |
| template_id | true |
| ttl | true |
| updated_at | false |
| user_acl | true |
|
diff --git a/docs/reference/api/chats.md b/docs/reference/api/chats.md
index 1b89aeb459d..5e2e2c90f4e 100644
--- a/docs/reference/api/chats.md
+++ b/docs/reference/api/chats.md
@@ -122,6 +122,7 @@ Experimental: this endpoint is subject to change.
"root_chat_id": "2898031c-fdce-4e3e-8c53-4481dd42fcd7",
"shared": true,
"status": "waiting",
+ "summary": "string",
"title": "string",
"updated_at": "2019-08-24T14:15:22Z",
"warnings": [
@@ -218,6 +219,7 @@ Status Code **200**
| `» root_chat_id` | string(uuid) | false | | |
| `» shared` | boolean | false | | Shared is true when this chat's root chat has explicit user or group ACL entries. |
| `» status` | [codersdk.ChatStatus](schemas.md#codersdkchatstatus) | false | | |
+| `» summary` | string | false | | Summary is the persisted whole-chat summary, generated in the background. It is nil until the first summary has been produced. |
| `» title` | string | false | | |
| `» updated_at` | string(date-time) | false | | |
| `» warnings` | array | false | | |
@@ -397,6 +399,7 @@ Experimental: this endpoint is subject to change.
"root_chat_id": "2898031c-fdce-4e3e-8c53-4481dd42fcd7",
"shared": true,
"status": "waiting",
+ "summary": "string",
"title": "string",
"updated_at": "2019-08-24T14:15:22Z",
"warnings": [
@@ -490,6 +493,7 @@ Experimental: this endpoint is subject to change.
"root_chat_id": "2898031c-fdce-4e3e-8c53-4481dd42fcd7",
"shared": true,
"status": "waiting",
+ "summary": "string",
"title": "string",
"updated_at": "2019-08-24T14:15:22Z",
"warnings": [
@@ -740,6 +744,7 @@ Experimental: this endpoint is subject to change.
"root_chat_id": "2898031c-fdce-4e3e-8c53-4481dd42fcd7",
"shared": true,
"status": "waiting",
+ "summary": "string",
"title": "string",
"updated_at": "2019-08-24T14:15:22Z",
"warnings": [
@@ -887,6 +892,7 @@ Experimental: this endpoint is subject to change.
"root_chat_id": "2898031c-fdce-4e3e-8c53-4481dd42fcd7",
"shared": true,
"status": "waiting",
+ "summary": "string",
"title": "string",
"updated_at": "2019-08-24T14:15:22Z",
"warnings": [
@@ -980,6 +986,7 @@ Experimental: this endpoint is subject to change.
"root_chat_id": "2898031c-fdce-4e3e-8c53-4481dd42fcd7",
"shared": true,
"status": "waiting",
+ "summary": "string",
"title": "string",
"updated_at": "2019-08-24T14:15:22Z",
"warnings": [
@@ -1164,6 +1171,7 @@ Experimental: this endpoint is subject to change.
"root_chat_id": "2898031c-fdce-4e3e-8c53-4481dd42fcd7",
"shared": true,
"status": "waiting",
+ "summary": "string",
"title": "string",
"updated_at": "2019-08-24T14:15:22Z",
"warnings": [
@@ -1257,6 +1265,7 @@ Experimental: this endpoint is subject to change.
"root_chat_id": "2898031c-fdce-4e3e-8c53-4481dd42fcd7",
"shared": true,
"status": "waiting",
+ "summary": "string",
"title": "string",
"updated_at": "2019-08-24T14:15:22Z",
"warnings": [
@@ -1439,6 +1448,7 @@ Experimental: this endpoint is subject to change.
"root_chat_id": "2898031c-fdce-4e3e-8c53-4481dd42fcd7",
"shared": true,
"status": "waiting",
+ "summary": "string",
"title": "string",
"updated_at": "2019-08-24T14:15:22Z",
"warnings": [
@@ -1532,6 +1542,7 @@ Experimental: this endpoint is subject to change.
"root_chat_id": "2898031c-fdce-4e3e-8c53-4481dd42fcd7",
"shared": true,
"status": "waiting",
+ "summary": "string",
"title": "string",
"updated_at": "2019-08-24T14:15:22Z",
"warnings": [
@@ -2283,6 +2294,7 @@ Experimental: this endpoint is subject to change.
"root_chat_id": "2898031c-fdce-4e3e-8c53-4481dd42fcd7",
"shared": true,
"status": "waiting",
+ "summary": "string",
"title": "string",
"updated_at": "2019-08-24T14:15:22Z",
"warnings": [
@@ -2376,6 +2388,7 @@ Experimental: this endpoint is subject to change.
"root_chat_id": "2898031c-fdce-4e3e-8c53-4481dd42fcd7",
"shared": true,
"status": "waiting",
+ "summary": "string",
"title": "string",
"updated_at": "2019-08-24T14:15:22Z",
"warnings": [
@@ -2883,6 +2896,7 @@ Experimental: this endpoint is subject to change.
"root_chat_id": "2898031c-fdce-4e3e-8c53-4481dd42fcd7",
"shared": true,
"status": "waiting",
+ "summary": "string",
"title": "string",
"updated_at": "2019-08-24T14:15:22Z",
"warnings": [
@@ -2976,6 +2990,7 @@ Experimental: this endpoint is subject to change.
"root_chat_id": "2898031c-fdce-4e3e-8c53-4481dd42fcd7",
"shared": true,
"status": "waiting",
+ "summary": "string",
"title": "string",
"updated_at": "2019-08-24T14:15:22Z",
"warnings": [
diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md
index ca6b0fad0d4..e935ce7a708 100644
--- a/docs/reference/api/schemas.md
+++ b/docs/reference/api/schemas.md
@@ -2164,6 +2164,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"root_chat_id": "2898031c-fdce-4e3e-8c53-4481dd42fcd7",
"shared": true,
"status": "waiting",
+ "summary": "string",
"title": "string",
"updated_at": "2019-08-24T14:15:22Z",
"warnings": [
@@ -2257,6 +2258,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"root_chat_id": "2898031c-fdce-4e3e-8c53-4481dd42fcd7",
"shared": true,
"status": "waiting",
+ "summary": "string",
"title": "string",
"updated_at": "2019-08-24T14:15:22Z",
"warnings": [
@@ -2298,6 +2300,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
| `root_chat_id` | string | false | | |
| `shared` | boolean | false | | Shared is true when this chat's root chat has explicit user or group ACL entries. |
| `status` | [codersdk.ChatStatus](#codersdkchatstatus) | false | | |
+| `summary` | string | false | | Summary is the persisted whole-chat summary, generated in the background. It is nil until the first summary has been produced. |
| `title` | string | false | | |
| `updated_at` | string | false | | |
| `warnings` | array of string | false | | |
@@ -4042,6 +4045,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"root_chat_id": "2898031c-fdce-4e3e-8c53-4481dd42fcd7",
"shared": true,
"status": "waiting",
+ "summary": "string",
"title": "string",
"updated_at": "2019-08-24T14:15:22Z",
"warnings": [
@@ -4078,9 +4082,9 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
#### Enumerated Values
-| Value(s) |
-|-----------------------------------------------------------------------------------------------------------------------------------|
-| `action_required`, `context_dirty`, `created`, `deleted`, `diff_status_change`, `status_change`, `summary_change`, `title_change` |
+| Value(s) |
+|----------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `action_required`, `chat_summary_change`, `context_dirty`, `created`, `deleted`, `diff_status_change`, `status_change`, `summary_change`, `title_change` |
## codersdk.ClusterConfig
diff --git a/enterprise/audit/table.go b/enterprise/audit/table.go
index 50532c6b728..540f4cce34d 100644
--- a/enterprise/audit/table.go
+++ b/enterprise/audit/table.go
@@ -461,6 +461,8 @@ var auditableResourcesTypes = map[any]map[string]Action{
"archived": ActionTrack,
"last_error": ActionIgnore, // Internal.
"last_turn_summary": ActionIgnore, // Internal cached display text.
+ "summary": ActionIgnore, // Internal cached display text, generated asynchronously.
+ "summary_generated_at": ActionIgnore, // Internal freshness marker for the cached summary.
"mode": ActionTrack,
"mcp_server_ids": ActionTrack,
"labels": ActionTrack,
diff --git a/site/src/api/queries/chats.test.ts b/site/src/api/queries/chats.test.ts
index 9f2f5386377..64e6d25c5b3 100644
--- a/site/src/api/queries/chats.test.ts
+++ b/site/src/api/queries/chats.test.ts
@@ -125,6 +125,7 @@ const makeChat = (
has_unread: false,
client_type: "ui",
last_turn_summary: null,
+ summary: null,
children: [],
...overrides,
});
@@ -2294,6 +2295,71 @@ describe("mergeWatchedChatSummary", () => {
).toBe("Fixed the issue");
});
+ it("applies chat_summary_change even when event updated_at is older", () => {
+ const cachedChat = makeChat("chat-1", {
+ summary: null,
+ last_turn_summary: "Latest turn",
+ updated_at: "2025-01-01T00:05:00.000Z",
+ });
+ const watchedChat = makeChat("chat-1", {
+ summary: "Implemented the whole-chat summary feature.",
+ // chat_summary_change preserves updated_at, so the event carries an
+ // equal-or-older timestamp than the cached chat.
+ last_turn_summary: "Stale turn",
+ updated_at: "2025-01-01T00:00:00.000Z",
+ });
+
+ const merged = mergeWatchedChatSummary(cachedChat, watchedChat, {
+ eventKind: "chat_summary_change",
+ });
+ expect(merged.summary).toBe("Implemented the whole-chat summary feature.");
+ // A chat_summary_change event must not clobber last_turn_summary with the
+ // event's stale snapshot.
+ expect(merged.last_turn_summary).toBe("Latest turn");
+ });
+
+ it("does not clobber the whole-chat summary on a summary_change with equal updated_at", () => {
+ const cachedChat = makeChat("chat-1", {
+ summary: "Whole-chat summary.",
+ last_turn_summary: "Old turn",
+ updated_at: "2025-01-01T00:00:00.000Z",
+ });
+ // Neither summary write bumps chats.updated_at, so both events replay
+ // the triggering turn's timestamp and arrive with equal updated_at. The
+ // summary_change snapshot still carries a stale whole-chat summary from
+ // when the turn finished.
+ const watchedChat = makeChat("chat-1", {
+ summary: null,
+ last_turn_summary: "New turn",
+ updated_at: "2025-01-01T00:00:00.000Z",
+ });
+
+ const merged = mergeWatchedChatSummary(cachedChat, watchedChat, {
+ eventKind: "summary_change",
+ });
+ expect(merged.last_turn_summary).toBe("New turn");
+ expect(merged.summary).toBe("Whole-chat summary.");
+ });
+
+ it("does not clobber last_turn_summary on a chat_summary_change with equal updated_at", () => {
+ const cachedChat = makeChat("chat-1", {
+ summary: null,
+ last_turn_summary: "Latest turn",
+ updated_at: "2025-01-01T00:00:00.000Z",
+ });
+ const watchedChat = makeChat("chat-1", {
+ summary: "Implemented the whole-chat summary feature.",
+ last_turn_summary: "Stale turn",
+ updated_at: "2025-01-01T00:00:00.000Z",
+ });
+
+ const merged = mergeWatchedChatSummary(cachedChat, watchedChat, {
+ eventKind: "chat_summary_change",
+ });
+ expect(merged.summary).toBe("Implemented the whole-chat summary feature.");
+ expect(merged.last_turn_summary).toBe("Latest turn");
+ });
+
it("clears last_turn_summary on summary updates with matching updated_at", () => {
const cachedChat = makeChat("chat-1", {
last_turn_summary: "Previous summary",
diff --git a/site/src/api/queries/chats.ts b/site/src/api/queries/chats.ts
index 6a241ad8f5f..00b26af1316 100644
--- a/site/src/api/queries/chats.ts
+++ b/site/src/api/queries/chats.ts
@@ -420,6 +420,7 @@ export const mergeWatchedChatSummary = (
const isTitleEvent = eventKind === "title_change";
const isStatusEvent = eventKind === "status_change";
const isSummaryEvent = eventKind === "summary_change";
+ const isChatSummaryEvent = eventKind === "chat_summary_change";
const isDiffStatusEvent = eventKind === "diff_status_change";
const isContextDirtyEvent = eventKind === "context_dirty";
const updatedAtComparison = compareUpdatedAtInstants(
@@ -456,10 +457,17 @@ export const mergeWatchedChatSummary = (
const nextLastModelConfigId = isFreshEnough
? watchedChat.last_model_config_id
: cachedChat.last_model_config_id;
- const nextLastTurnSummary =
- isFreshEnough || isSummaryEvent
- ? watchedChat.last_turn_summary
- : cachedChat.last_turn_summary;
+ // The summary writes (UpdateChatLastTurnSummary, UpdateChatSummary) never
+ // bump chats.updated_at, and both events publish pre-write chat snapshots,
+ // so updated_at cannot order summary_change against chat_summary_change and
+ // isFreshEnough cannot guard these fields. Scope each field to its own
+ // event, else one event's stale snapshot clobbers the other field's value.
+ const nextLastTurnSummary = isSummaryEvent
+ ? watchedChat.last_turn_summary
+ : cachedChat.last_turn_summary;
+ const nextSummary = isChatSummaryEvent
+ ? watchedChat.summary
+ : cachedChat.summary;
const nextHasUnread =
isFreshEnough && isStatusEvent && watchedChat.id !== activeChatId
? true
@@ -478,6 +486,7 @@ export const mergeWatchedChatSummary = (
nextBuildId === cachedChat.build_id &&
nextLastModelConfigId === cachedChat.last_model_config_id &&
nextLastTurnSummary === cachedChat.last_turn_summary &&
+ nextSummary === cachedChat.summary &&
nextHasUnread === cachedChat.has_unread &&
nextUpdatedAt === cachedChat.updated_at &&
nextContext === cachedChat.context
@@ -494,6 +503,7 @@ export const mergeWatchedChatSummary = (
build_id: nextBuildId,
last_model_config_id: nextLastModelConfigId,
last_turn_summary: nextLastTurnSummary,
+ summary: nextSummary,
has_unread: nextHasUnread,
updated_at: nextUpdatedAt,
context: nextContext,
diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts
index 4158bd880bc..429d9bebaea 100644
--- a/site/src/api/typesGenerated.ts
+++ b/site/src/api/typesGenerated.ts
@@ -1679,6 +1679,11 @@ export interface Chat {
readonly plan_mode?: ChatPlanMode;
readonly last_error?: ChatError;
readonly last_turn_summary: string | null;
+ /**
+ * Summary is the persisted whole-chat summary, generated in the background.
+ * It is nil until the first summary has been produced.
+ */
+ readonly summary: string | null;
readonly diff_status?: ChatDiffStatus;
readonly created_at: string;
readonly updated_at: string;
@@ -3434,6 +3439,7 @@ export interface ChatWatchEvent {
// From codersdk/chats.go
export type ChatWatchEventKind =
| "action_required"
+ | "chat_summary_change"
| "context_dirty"
| "created"
| "deleted"
@@ -3444,6 +3450,7 @@ export type ChatWatchEventKind =
export const ChatWatchEventKinds: ChatWatchEventKind[] = [
"action_required",
+ "chat_summary_change",
"context_dirty",
"created",
"deleted",
diff --git a/site/src/pages/AgentsPage/AgentChatPage.stories.tsx b/site/src/pages/AgentsPage/AgentChatPage.stories.tsx
index e629f5030e7..8187e3ad5fc 100644
--- a/site/src/pages/AgentsPage/AgentChatPage.stories.tsx
+++ b/site/src/pages/AgentsPage/AgentChatPage.stories.tsx
@@ -152,6 +152,7 @@ const baseChatFields = {
has_unread: false,
client_type: "ui",
last_turn_summary: null,
+ summary: null,
children: [],
} as const;
diff --git a/site/src/pages/AgentsPage/components/ChatTopBar.stories.tsx b/site/src/pages/AgentsPage/components/ChatTopBar.stories.tsx
index 9c0c3c77d31..d9ddbf7f04c 100644
--- a/site/src/pages/AgentsPage/components/ChatTopBar.stories.tsx
+++ b/site/src/pages/AgentsPage/components/ChatTopBar.stories.tsx
@@ -74,6 +74,7 @@ export const WithParentChat: Story = {
title: "Set up CI/CD pipeline",
status: "waiting",
last_turn_summary: null,
+ summary: null,
created_at: "2026-02-18T00:00:00.000Z",
updated_at: "2026-02-18T00:00:00.000Z",
archived: false,
diff --git a/site/src/pages/AgentsPage/components/ChatsSidebar/dialogs/ChatSearchDialog.stories.tsx b/site/src/pages/AgentsPage/components/ChatsSidebar/dialogs/ChatSearchDialog.stories.tsx
index ce71561950c..0c1c57689e7 100644
--- a/site/src/pages/AgentsPage/components/ChatsSidebar/dialogs/ChatSearchDialog.stories.tsx
+++ b/site/src/pages/AgentsPage/components/ChatsSidebar/dialogs/ChatSearchDialog.stories.tsx
@@ -29,6 +29,7 @@ const mockChat: Chat = {
mcp_server_ids: [],
labels: {},
last_turn_summary: "Added migration script",
+ summary: "Investigated and fixed a race condition in the auth middleware.",
created_at: "2026-05-20T05:00:00.000Z",
updated_at: "2026-05-20T07:30:00.000Z",
archived: false,
diff --git a/site/src/testHelpers/chatEntities.ts b/site/src/testHelpers/chatEntities.ts
index efd5e52be63..c86519d1925 100644
--- a/site/src/testHelpers/chatEntities.ts
+++ b/site/src/testHelpers/chatEntities.ts
@@ -20,6 +20,7 @@ export const MockChat: Chat = {
title: "Agent",
status: "waiting",
last_turn_summary: null,
+ summary: null,
created_at: MOCK_TIMESTAMP,
updated_at: MOCK_TIMESTAMP,
archived: false,