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 | |
FieldTracked
created_atfalse
hashed_secrettrue
idtrue
last_heartbeat_atfalse
nametrue
secret_prefixtrue
| -| AIProvider
create, write, delete | |
FieldTracked
base_urltrue
created_atfalse
deletedtrue
display_nametrue
enabledtrue
icontrue
idtrue
nametrue
settingstrue
settings_key_idfalse
typetrue
updated_atfalse
| -| AIProviderKey
create, delete | |
FieldTracked
api_keytrue
api_key_key_idfalse
created_atfalse
idtrue
provider_idtrue
updated_atfalse
| -| AISeatState
create | |
FieldTracked
first_used_attrue
last_event_descriptiontrue
last_event_typetrue
last_used_atfalse
updated_atfalse
user_idtrue
| -| APIKey
login, logout, register, create, write, delete | |
FieldTracked
allow_listfalse
created_attrue
expires_attrue
hashed_secretfalse
idfalse
ip_addressfalse
last_usedtrue
lifetime_secondsfalse
login_typefalse
scopesfalse
token_namefalse
updated_atfalse
user_idtrue
| -| AuditOAuthConvertState
| |
FieldTracked
created_attrue
expires_attrue
from_login_typetrue
to_login_typetrue
user_idtrue
| -| Group
create, write, delete | |
FieldTracked
avatar_urltrue
chat_spend_limit_microstrue
display_nametrue
idtrue
memberstrue
nametrue
organization_idfalse
quota_allowancetrue
sourcefalse
| -| AuditableGroupAIBudget
write, delete | |
FieldTracked
created_atfalse
group_idfalse
group_namefalse
spend_limittrue
spend_limit_microsfalse
updated_atfalse
| -| AuditableOrganizationMember
| |
FieldTracked
created_attrue
organization_idfalse
rolestrue
updated_attrue
user_idtrue
usernametrue
| -| AuditableUserAIBudgetOverride
write, delete | |
FieldTracked
created_atfalse
group_idtrue
group_nametrue
spend_limittrue
spend_limit_microsfalse
updated_atfalse
user_idfalse
usernamefalse
| -| Chat
create, write | |
FieldTracked
agent_idfalse
archivedtrue
build_idfalse
client_typefalse
compaction_requested_atfalse
context_aggregate_hashfalse
context_dirty_resourcesfalse
context_dirty_sincefalse
context_errorfalse
created_atfalse
dynamic_toolsfalse
generation_attemptfalse
group_acltrue
heartbeat_atfalse
history_versionfalse
idtrue
labelstrue
last_errorfalse
last_model_config_idfalse
last_read_message_idfalse
last_reasoning_effortfalse
last_turn_summaryfalse
mcp_server_idstrue
modetrue
organization_idfalse
owner_idtrue
owner_namefalse
owner_usernamefalse
parent_chat_idfalse
pin_ordertrue
plan_modefalse
queue_versionfalse
requires_action_deadline_atfalse
retry_statefalse
retry_state_versionfalse
root_chat_idfalse
runner_idfalse
snapshot_versionfalse
started_atfalse
statusfalse
titletrue
updated_atfalse
user_acltrue
worker_idfalse
workspace_idtrue
| -| CustomRole
| |
FieldTracked
created_atfalse
display_nametrue
idfalse
is_systemfalse
member_permissionstrue
nametrue
org_permissionstrue
organization_idfalse
site_permissionstrue
updated_atfalse
user_permissionstrue
| -| GitSSHKey
create | |
FieldTracked
created_atfalse
private_keytrue
private_key_key_idfalse
public_keytrue
updated_atfalse
user_idtrue
| -| GroupSyncSettings
| |
FieldTracked
auto_create_missing_groupstrue
fieldtrue
legacy_group_name_mappingfalse
mappingtrue
regex_filtertrue
| -| HealthSettings
| |
FieldTracked
dismissed_healthcheckstrue
idfalse
| -| License
create, delete | |
FieldTracked
exptrue
idfalse
jwtfalse
uploaded_attrue
uuidtrue
| -| NotificationTemplate
| |
FieldTracked
actionstrue
body_templatetrue
enabled_by_defaulttrue
grouptrue
idfalse
kindtrue
methodtrue
nametrue
title_templatetrue
| -| NotificationsSettings
| |
FieldTracked
idfalse
notifier_pausedtrue
| -| OAuth2ProviderApp
| |
FieldTracked
callback_urltrue
client_id_issued_atfalse
client_secret_expires_attrue
client_typetrue
client_uritrue
contactstrue
created_atfalse
dynamically_registeredtrue
grant_typestrue
icontrue
idfalse
jwkstrue
jwks_uritrue
logo_uritrue
nametrue
policy_uritrue
redirect_uristrue
registration_access_tokentrue
registration_client_uritrue
response_typestrue
scopetrue
software_idtrue
software_versiontrue
token_endpoint_auth_methodtrue
tos_uritrue
updated_atfalse
| -| OAuth2ProviderAppSecret
| |
FieldTracked
app_idfalse
created_atfalse
display_secretfalse
hashed_secretfalse
idfalse
last_used_atfalse
secret_prefixfalse
| -| Organization
| |
FieldTracked
created_atfalse
default_org_member_rolestrue
deletedtrue
descriptiontrue
display_nametrue
icontrue
idfalse
is_defaulttrue
nametrue
shareable_workspace_ownerstrue
updated_attrue
| -| OrganizationSyncSettings
| |
FieldTracked
assign_defaulttrue
fieldtrue
mappingtrue
| -| PrebuildsSettings
| |
FieldTracked
idfalse
reconciliation_pausedtrue
| -| RoleSyncSettings
| |
FieldTracked
fieldtrue
mappingtrue
| -| TaskTable
| |
FieldTracked
created_atfalse
deleted_atfalse
display_nametrue
idtrue
nametrue
organization_idfalse
owner_idtrue
prompttrue
template_parameterstrue
template_version_idtrue
workspace_idtrue
| -| Template
write, delete | |
FieldTracked
active_version_idtrue
activity_bumptrue
allow_user_autostarttrue
allow_user_autostoptrue
allow_user_cancel_workspace_jobstrue
autostart_block_days_of_weektrue
autostop_requirement_days_of_weektrue
autostop_requirement_weekstrue
cors_behaviortrue
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_namefalse
created_by_usernamefalse
default_ttltrue
deletedfalse
deprecatedtrue
descriptiontrue
disable_module_cachetrue
display_nametrue
failure_ttltrue
group_acltrue
icontrue
idtrue
max_port_sharing_leveltrue
nametrue
organization_display_namefalse
organization_iconfalse
organization_idfalse
organization_namefalse
provisionertrue
require_active_versiontrue
time_til_autostop_notifytrue
time_til_dormanttrue
time_til_dormant_autodeletetrue
updated_atfalse
use_classic_parameter_flowtrue
user_acltrue
| -| TemplateVersion
create, write | |
FieldTracked
archivedtrue
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_namefalse
created_by_usernamefalse
external_auth_providersfalse
has_ai_taskfalse
has_external_agentfalse
idtrue
job_idfalse
messagefalse
nametrue
organization_idfalse
readmetrue
source_example_idfalse
template_idtrue
updated_atfalse
| -| User
create, write, delete | |
FieldTracked
avatar_urlfalse
chat_spend_limit_microstrue
created_atfalse
deletedtrue
emailtrue
github_com_user_idfalse
hashed_one_time_passcodefalse
hashed_passwordtrue
idtrue
is_service_accounttrue
is_systemtrue
last_seen_atfalse
login_typetrue
nametrue
one_time_passcode_expires_attrue
quiet_hours_scheduletrue
rbac_rolestrue
statustrue
updated_atfalse
usernametrue
| -| UserSecret
create, write, delete | |
FieldTracked
created_atfalse
descriptiontrue
env_nametrue
file_pathtrue
idtrue
nametrue
updated_atfalse
user_idtrue
valuetrue
value_key_idfalse
| -| UserSkill
create, write, delete | |
FieldTracked
contenttrue
created_atfalse
descriptiontrue
idtrue
nametrue
updated_atfalse
user_idtrue
| -| WorkspaceBuild
start, stop | |
FieldTracked
build_numberfalse
created_atfalse
daily_costfalse
deadlinefalse
has_ai_taskfalse
has_external_agentfalse
idfalse
initiator_by_avatar_urlfalse
initiator_by_namefalse
initiator_by_usernamefalse
initiator_idfalse
job_idfalse
max_deadlinefalse
notified_autostop_deadlinefalse
reasonfalse
template_version_idtrue
template_version_preset_idfalse
transitionfalse
updated_atfalse
workspace_idfalse
| -| WorkspaceProxy
| |
FieldTracked
created_attrue
deletedfalse
derp_enabledtrue
derp_onlytrue
display_nametrue
icontrue
idtrue
nametrue
region_idtrue
token_hashed_secrettrue
updated_atfalse
urltrue
versiontrue
wildcard_hostnametrue
| -| WorkspaceTable
| |
FieldTracked
automatic_updatestrue
autostart_scheduletrue
created_atfalse
deletedfalse
deleting_attrue
dormant_attrue
favoritetrue
group_acltrue
idtrue
last_used_atfalse
nametrue
next_start_attrue
organization_idfalse
owner_idtrue
template_idtrue
ttltrue
updated_atfalse
user_acltrue
| +| Resource | | | +|-----------------------------------------------------------------|----------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| AIGatewayKey
create, delete | |
FieldTracked
created_atfalse
hashed_secrettrue
idtrue
last_heartbeat_atfalse
nametrue
secret_prefixtrue
| +| AIProvider
create, write, delete | |
FieldTracked
base_urltrue
created_atfalse
deletedtrue
display_nametrue
enabledtrue
icontrue
idtrue
nametrue
settingstrue
settings_key_idfalse
typetrue
updated_atfalse
| +| AIProviderKey
create, delete | |
FieldTracked
api_keytrue
api_key_key_idfalse
created_atfalse
idtrue
provider_idtrue
updated_atfalse
| +| AISeatState
create | |
FieldTracked
first_used_attrue
last_event_descriptiontrue
last_event_typetrue
last_used_atfalse
updated_atfalse
user_idtrue
| +| APIKey
login, logout, register, create, write, delete | |
FieldTracked
allow_listfalse
created_attrue
expires_attrue
hashed_secretfalse
idfalse
ip_addressfalse
last_usedtrue
lifetime_secondsfalse
login_typefalse
scopesfalse
token_namefalse
updated_atfalse
user_idtrue
| +| AuditOAuthConvertState
| |
FieldTracked
created_attrue
expires_attrue
from_login_typetrue
to_login_typetrue
user_idtrue
| +| Group
create, write, delete | |
FieldTracked
avatar_urltrue
chat_spend_limit_microstrue
display_nametrue
idtrue
memberstrue
nametrue
organization_idfalse
quota_allowancetrue
sourcefalse
| +| AuditableGroupAIBudget
write, delete | |
FieldTracked
created_atfalse
group_idfalse
group_namefalse
spend_limittrue
spend_limit_microsfalse
updated_atfalse
| +| AuditableOrganizationMember
| |
FieldTracked
created_attrue
organization_idfalse
rolestrue
updated_attrue
user_idtrue
usernametrue
| +| AuditableUserAIBudgetOverride
write, delete | |
FieldTracked
created_atfalse
group_idtrue
group_nametrue
spend_limittrue
spend_limit_microsfalse
updated_atfalse
user_idfalse
usernamefalse
| +| Chat
create, write | |
FieldTracked
agent_idfalse
archivedtrue
build_idfalse
client_typefalse
compaction_requested_atfalse
context_aggregate_hashfalse
context_dirty_resourcesfalse
context_dirty_sincefalse
context_errorfalse
created_atfalse
dynamic_toolsfalse
generation_attemptfalse
group_acltrue
heartbeat_atfalse
history_versionfalse
idtrue
labelstrue
last_errorfalse
last_model_config_idfalse
last_read_message_idfalse
last_reasoning_effortfalse
last_turn_summaryfalse
mcp_server_idstrue
modetrue
organization_idfalse
owner_idtrue
owner_namefalse
owner_usernamefalse
parent_chat_idfalse
pin_ordertrue
plan_modefalse
queue_versionfalse
requires_action_deadline_atfalse
retry_statefalse
retry_state_versionfalse
root_chat_idfalse
runner_idfalse
snapshot_versionfalse
started_atfalse
statusfalse
summaryfalse
summary_generated_atfalse
titletrue
updated_atfalse
user_acltrue
worker_idfalse
workspace_idtrue
| +| CustomRole
| |
FieldTracked
created_atfalse
display_nametrue
idfalse
is_systemfalse
member_permissionstrue
nametrue
org_permissionstrue
organization_idfalse
site_permissionstrue
updated_atfalse
user_permissionstrue
| +| GitSSHKey
create | |
FieldTracked
created_atfalse
private_keytrue
private_key_key_idfalse
public_keytrue
updated_atfalse
user_idtrue
| +| GroupSyncSettings
| |
FieldTracked
auto_create_missing_groupstrue
fieldtrue
legacy_group_name_mappingfalse
mappingtrue
regex_filtertrue
| +| HealthSettings
| |
FieldTracked
dismissed_healthcheckstrue
idfalse
| +| License
create, delete | |
FieldTracked
exptrue
idfalse
jwtfalse
uploaded_attrue
uuidtrue
| +| NotificationTemplate
| |
FieldTracked
actionstrue
body_templatetrue
enabled_by_defaulttrue
grouptrue
idfalse
kindtrue
methodtrue
nametrue
title_templatetrue
| +| NotificationsSettings
| |
FieldTracked
idfalse
notifier_pausedtrue
| +| OAuth2ProviderApp
| |
FieldTracked
callback_urltrue
client_id_issued_atfalse
client_secret_expires_attrue
client_typetrue
client_uritrue
contactstrue
created_atfalse
dynamically_registeredtrue
grant_typestrue
icontrue
idfalse
jwkstrue
jwks_uritrue
logo_uritrue
nametrue
policy_uritrue
redirect_uristrue
registration_access_tokentrue
registration_client_uritrue
response_typestrue
scopetrue
software_idtrue
software_versiontrue
token_endpoint_auth_methodtrue
tos_uritrue
updated_atfalse
| +| OAuth2ProviderAppSecret
| |
FieldTracked
app_idfalse
created_atfalse
display_secretfalse
hashed_secretfalse
idfalse
last_used_atfalse
secret_prefixfalse
| +| Organization
| |
FieldTracked
created_atfalse
default_org_member_rolestrue
deletedtrue
descriptiontrue
display_nametrue
icontrue
idfalse
is_defaulttrue
nametrue
shareable_workspace_ownerstrue
updated_attrue
| +| OrganizationSyncSettings
| |
FieldTracked
assign_defaulttrue
fieldtrue
mappingtrue
| +| PrebuildsSettings
| |
FieldTracked
idfalse
reconciliation_pausedtrue
| +| RoleSyncSettings
| |
FieldTracked
fieldtrue
mappingtrue
| +| TaskTable
| |
FieldTracked
created_atfalse
deleted_atfalse
display_nametrue
idtrue
nametrue
organization_idfalse
owner_idtrue
prompttrue
template_parameterstrue
template_version_idtrue
workspace_idtrue
| +| Template
write, delete | |
FieldTracked
active_version_idtrue
activity_bumptrue
allow_user_autostarttrue
allow_user_autostoptrue
allow_user_cancel_workspace_jobstrue
autostart_block_days_of_weektrue
autostop_requirement_days_of_weektrue
autostop_requirement_weekstrue
cors_behaviortrue
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_namefalse
created_by_usernamefalse
default_ttltrue
deletedfalse
deprecatedtrue
descriptiontrue
disable_module_cachetrue
display_nametrue
failure_ttltrue
group_acltrue
icontrue
idtrue
max_port_sharing_leveltrue
nametrue
organization_display_namefalse
organization_iconfalse
organization_idfalse
organization_namefalse
provisionertrue
require_active_versiontrue
time_til_autostop_notifytrue
time_til_dormanttrue
time_til_dormant_autodeletetrue
updated_atfalse
use_classic_parameter_flowtrue
user_acltrue
| +| TemplateVersion
create, write | |
FieldTracked
archivedtrue
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_namefalse
created_by_usernamefalse
external_auth_providersfalse
has_ai_taskfalse
has_external_agentfalse
idtrue
job_idfalse
messagefalse
nametrue
organization_idfalse
readmetrue
source_example_idfalse
template_idtrue
updated_atfalse
| +| User
create, write, delete | |
FieldTracked
avatar_urlfalse
chat_spend_limit_microstrue
created_atfalse
deletedtrue
emailtrue
github_com_user_idfalse
hashed_one_time_passcodefalse
hashed_passwordtrue
idtrue
is_service_accounttrue
is_systemtrue
last_seen_atfalse
login_typetrue
nametrue
one_time_passcode_expires_attrue
quiet_hours_scheduletrue
rbac_rolestrue
statustrue
updated_atfalse
usernametrue
| +| UserSecret
create, write, delete | |
FieldTracked
created_atfalse
descriptiontrue
env_nametrue
file_pathtrue
idtrue
nametrue
updated_atfalse
user_idtrue
valuetrue
value_key_idfalse
| +| UserSkill
create, write, delete | |
FieldTracked
contenttrue
created_atfalse
descriptiontrue
idtrue
nametrue
updated_atfalse
user_idtrue
| +| WorkspaceBuild
start, stop | |
FieldTracked
build_numberfalse
created_atfalse
daily_costfalse
deadlinefalse
has_ai_taskfalse
has_external_agentfalse
idfalse
initiator_by_avatar_urlfalse
initiator_by_namefalse
initiator_by_usernamefalse
initiator_idfalse
job_idfalse
max_deadlinefalse
notified_autostop_deadlinefalse
reasonfalse
template_version_idtrue
template_version_preset_idfalse
transitionfalse
updated_atfalse
workspace_idfalse
| +| WorkspaceProxy
| |
FieldTracked
created_attrue
deletedfalse
derp_enabledtrue
derp_onlytrue
display_nametrue
icontrue
idtrue
nametrue
region_idtrue
token_hashed_secrettrue
updated_atfalse
urltrue
versiontrue
wildcard_hostnametrue
| +| WorkspaceTable
| |
FieldTracked
automatic_updatestrue
autostart_scheduletrue
created_atfalse
deletedfalse
deleting_attrue
dormant_attrue
favoritetrue
group_acltrue
idtrue
last_used_atfalse
nametrue
next_start_attrue
organization_idfalse
owner_idtrue
template_idtrue
ttltrue
updated_atfalse
user_acltrue
| 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,