Hugo Dutka
2026-06-12 13:33:12 +02:00
committed by GitHub
parent 4a07f61c50
commit 4debd23cbb
155 changed files with 37612 additions and 24513 deletions
+246 -3
View File
@@ -1692,9 +1692,6 @@ func (q *querier) ArchiveUnusedTemplateVersions(ctx context.Context, arg databas
}
func (q *querier) AutoArchiveInactiveChats(ctx context.Context, arg database.AutoArchiveInactiveChatsParams) ([]database.AutoArchiveInactiveChatsRow, error) {
// Background write by dbpurge. The LATERAL read of chat_messages rows
// happens below the RBAC boundary; only the chat row itself requires
// authorization.
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceChat); err != nil {
return nil, err
}
@@ -1718,6 +1715,13 @@ func (q *querier) BackoffChatDiffStatus(ctx context.Context, arg database.Backof
return q.db.BackoffChatDiffStatus(ctx, arg)
}
func (q *querier) BatchDeleteChatHeartbeats(ctx context.Context, arg database.BatchDeleteChatHeartbeatsParams) (int64, error) {
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceChat); err != nil {
return 0, err
}
return q.db.BatchDeleteChatHeartbeats(ctx, arg)
}
func (q *querier) BatchUpdateWorkspaceAgentMetadata(ctx context.Context, arg database.BatchUpdateWorkspaceAgentMetadataParams) error {
// Could be any workspace agent and checking auth to each workspace agent is overkill for
// the purpose of this function.
@@ -1743,6 +1747,13 @@ func (q *querier) BatchUpdateWorkspaceNextStartAt(ctx context.Context, arg datab
return q.db.BatchUpdateWorkspaceNextStartAt(ctx, arg)
}
func (q *querier) BatchUpsertChatHeartbeats(ctx context.Context, arg database.BatchUpsertChatHeartbeatsParams) error {
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceChat); err != nil {
return err
}
return q.db.BatchUpsertChatHeartbeats(ctx, arg)
}
func (q *querier) BatchUpsertConnectionLogs(ctx context.Context, arg database.BatchUpsertConnectionLogsParams) error {
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceConnectionLog); err != nil {
return err
@@ -1857,6 +1868,14 @@ func (q *querier) CountAuditLogs(ctx context.Context, arg database.CountAuditLog
return q.db.CountAuthorizedAuditLogs(ctx, arg, prep)
}
func (q *querier) CountChatQueuedMessages(ctx context.Context, chatID uuid.UUID) (int64, error) {
_, err := q.GetChatByID(ctx, chatID)
if err != nil {
return 0, err
}
return q.db.CountChatQueuedMessages(ctx, chatID)
}
func (q *querier) CountConnectionLogs(ctx context.Context, arg database.CountConnectionLogsParams) (int64, error) {
// Just like the actual query, shortcut if the user is an owner.
err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceConnectionLog)
@@ -1954,6 +1973,18 @@ func (q *querier) DeleteAPIKeysByUserID(ctx context.Context, userID uuid.UUID) e
return q.db.DeleteAPIKeysByUserID(ctx, userID)
}
func (q *querier) DeleteAllChatHeartbeats(ctx context.Context, chatID uuid.UUID) error {
chat, err := q.db.GetChatByID(ctx, chatID)
if err != nil {
return err
}
if err := q.authorizeContext(ctx, policy.ActionUpdate, chat); err != nil {
return err
}
_ = chat
return q.db.DeleteAllChatHeartbeats(ctx, chatID)
}
func (q *querier) DeleteAllChatQueuedMessages(ctx context.Context, chatID uuid.UUID) error {
chat, err := q.db.GetChatByID(ctx, chatID)
if err != nil {
@@ -1965,6 +1996,18 @@ func (q *querier) DeleteAllChatQueuedMessages(ctx context.Context, chatID uuid.U
return q.db.DeleteAllChatQueuedMessages(ctx, chatID)
}
func (q *querier) DeleteAllChatQueuedMessagesReturningCount(ctx context.Context, chatID uuid.UUID) (int64, error) {
chat, err := q.db.GetChatByID(ctx, chatID)
if err != nil {
return 0, err
}
if err := q.authorizeContext(ctx, policy.ActionUpdate, chat); err != nil {
return 0, err
}
_ = chat
return q.db.DeleteAllChatQueuedMessagesReturningCount(ctx, chatID)
}
func (q *querier) DeleteAllTailnetTunnels(ctx context.Context, arg database.DeleteAllTailnetTunnelsParams) ([]database.DeleteAllTailnetTunnelsRow, error) {
if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceTailnetCoordinator); err != nil {
return nil, err
@@ -2043,6 +2086,18 @@ func (q *querier) DeleteChatQueuedMessage(ctx context.Context, arg database.Dele
return q.db.DeleteChatQueuedMessage(ctx, arg)
}
func (q *querier) DeleteChatQueuedMessageReturningCount(ctx context.Context, arg database.DeleteChatQueuedMessageReturningCountParams) (int64, error) {
chat, err := q.db.GetChatByID(ctx, arg.ChatID)
if err != nil {
return 0, err
}
if err := q.authorizeContext(ctx, policy.ActionUpdate, chat); err != nil {
return 0, err
}
_ = chat
return q.db.DeleteChatQueuedMessageReturningCount(ctx, arg)
}
func (q *querier) DeleteChatUsageLimitGroupOverride(ctx context.Context, groupID uuid.UUID) error {
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceDeploymentConfig); err != nil {
return err
@@ -2315,6 +2370,13 @@ func (q *querier) DeleteRuntimeConfig(ctx context.Context, key string) error {
return q.db.DeleteRuntimeConfig(ctx, key)
}
func (q *querier) DeleteStaleChatHeartbeats(ctx context.Context, staleSeconds int32) (int64, error) {
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceChat); err != nil {
return 0, err
}
return q.db.DeleteStaleChatHeartbeats(ctx, staleSeconds)
}
func (q *querier) DeleteTailnetPeer(ctx context.Context, arg database.DeleteTailnetPeerParams) (database.DeleteTailnetPeerRow, error) {
if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceTailnetCoordinator); err != nil {
return database.DeleteTailnetPeerRow{}, err
@@ -2826,6 +2888,13 @@ func (q *querier) GetAuthorizationUserRoles(ctx context.Context, userID uuid.UUI
return q.db.GetAuthorizationUserRoles(ctx, userID)
}
func (q *querier) GetAutoArchiveInactiveChatCandidates(ctx context.Context, arg database.GetAutoArchiveInactiveChatCandidatesParams) ([]database.GetAutoArchiveInactiveChatCandidatesRow, error) {
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceChat); err != nil {
return nil, err
}
return q.db.GetAutoArchiveInactiveChatCandidates(ctx, arg)
}
func (q *querier) GetBoundaryLogByID(ctx context.Context, id uuid.UUID) (database.BoundaryLog, error) {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceBoundaryLog); err != nil {
return database.BoundaryLog{}, err
@@ -2877,6 +2946,10 @@ func (q *querier) GetChatByID(ctx context.Context, id uuid.UUID) (database.Chat,
return fetch(q.log, q.auth, q.db.GetChatByID)(ctx, id)
}
func (q *querier) GetChatByIDForShare(ctx context.Context, id uuid.UUID) (database.Chat, error) {
return fetch(q.log, q.auth, q.db.GetChatByIDForShare)(ctx, id)
}
func (q *querier) GetChatByIDForUpdate(ctx context.Context, id uuid.UUID) (database.Chat, error) {
return fetch(q.log, q.auth, q.db.GetChatByIDForUpdate)(ctx, id)
}
@@ -3048,6 +3121,18 @@ func (q *querier) GetChatExploreModelOverride(ctx context.Context) (string, erro
return q.db.GetChatExploreModelOverride(ctx)
}
func (q *querier) GetChatFamilyIDsByRootID(ctx context.Context, id uuid.UUID) ([]uuid.UUID, error) {
// This is a read-only query: it returns the chat IDs that belong
// to a family. Authorize as Read against the root chat. The
// individual SetArchived (or other) transitions that consume
// these IDs run their own per-row authorization, so we do not
// gate the listing itself on Update permission.
if _, err := q.GetChatByID(ctx, id); err != nil {
return nil, err
}
return q.db.GetChatFamilyIDsByRootID(ctx, id)
}
func (q *querier) GetChatFileByID(ctx context.Context, id uuid.UUID) (database.ChatFile, error) {
file, err := q.db.GetChatFileByID(ctx, id)
if err != nil {
@@ -3114,6 +3199,14 @@ func (q *querier) GetChatGeneralModelOverride(ctx context.Context) (string, erro
return q.db.GetChatGeneralModelOverride(ctx)
}
func (q *querier) GetChatHeartbeat(ctx context.Context, arg database.GetChatHeartbeatParams) (database.ChatHeartbeat, error) {
_, err := q.GetChatByID(ctx, arg.ChatID)
if err != nil {
return database.ChatHeartbeat{}, err
}
return q.db.GetChatHeartbeat(ctx, arg)
}
func (q *querier) GetChatIncludeDefaultSystemPrompt(ctx context.Context) (bool, error) {
// The include-default-system-prompt flag is a deployment-wide setting read
// during chat creation by every authenticated user, so no RBAC policy
@@ -3174,6 +3267,14 @@ func (q *querier) GetChatMessagesByChatIDDescPaginated(ctx context.Context, arg
return q.db.GetChatMessagesByChatIDDescPaginated(ctx, arg)
}
func (q *querier) GetChatMessagesByRevisionForStream(ctx context.Context, arg database.GetChatMessagesByRevisionForStreamParams) ([]database.ChatMessage, error) {
_, err := q.GetChatByID(ctx, arg.ChatID)
if err != nil {
return nil, err
}
return q.db.GetChatMessagesByRevisionForStream(ctx, arg)
}
func (q *querier) GetChatMessagesForPromptByChatID(ctx context.Context, chatID uuid.UUID) ([]database.ChatMessage, error) {
// Authorize read on the parent chat.
_, err := q.GetChatByID(ctx, chatID)
@@ -3222,6 +3323,22 @@ func (q *querier) GetChatPlanModeInstructions(ctx context.Context) (string, erro
return q.db.GetChatPlanModeInstructions(ctx)
}
func (q *querier) GetChatQueuedMessageByID(ctx context.Context, arg database.GetChatQueuedMessageByIDParams) (database.ChatQueuedMessage, error) {
_, err := q.GetChatByID(ctx, arg.ChatID)
if err != nil {
return database.ChatQueuedMessage{}, err
}
return q.db.GetChatQueuedMessageByID(ctx, arg)
}
func (q *querier) GetChatQueuedMessageHead(ctx context.Context, chatID uuid.UUID) (database.ChatQueuedMessage, error) {
_, err := q.GetChatByID(ctx, chatID)
if err != nil {
return database.ChatQueuedMessage{}, err
}
return q.db.GetChatQueuedMessageHead(ctx, chatID)
}
func (q *querier) GetChatQueuedMessages(ctx context.Context, chatID uuid.UUID) ([]database.ChatQueuedMessage, error) {
_, err := q.GetChatByID(ctx, chatID)
if err != nil {
@@ -3230,6 +3347,14 @@ func (q *querier) GetChatQueuedMessages(ctx context.Context, chatID uuid.UUID) (
return q.db.GetChatQueuedMessages(ctx, chatID)
}
func (q *querier) GetChatQueuedMessagesByPosition(ctx context.Context, chatID uuid.UUID) ([]database.ChatQueuedMessage, error) {
_, err := q.GetChatByID(ctx, chatID)
if err != nil {
return nil, err
}
return q.db.GetChatQueuedMessagesByPosition(ctx, chatID)
}
func (q *querier) GetChatRetentionDays(ctx context.Context) (int32, error) {
// Chat retention is a deployment-wide config read by dbpurge.
// Only requires a valid actor in context.
@@ -3239,6 +3364,13 @@ func (q *querier) GetChatRetentionDays(ctx context.Context) (int32, error) {
return q.db.GetChatRetentionDays(ctx)
}
func (q *querier) GetChatStreamSyncRows(ctx context.Context, ids []uuid.UUID) ([]database.GetChatStreamSyncRowsRow, error) {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceChat); err != nil {
return nil, err
}
return q.db.GetChatStreamSyncRows(ctx, ids)
}
func (q *querier) GetChatSystemPrompt(ctx context.Context) (string, error) {
// The system prompt is a deployment-wide setting read during chat
// creation by every authenticated user, so no RBAC policy check
@@ -3311,6 +3443,13 @@ func (q *querier) GetChatUserPromptsByChatID(ctx context.Context, arg database.G
return q.db.GetChatUserPromptsByChatID(ctx, arg)
}
func (q *querier) GetChatWorkerAcquisitionCandidates(ctx context.Context, arg database.GetChatWorkerAcquisitionCandidatesParams) ([]database.GetChatWorkerAcquisitionCandidatesRow, error) {
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceChat); err != nil {
return nil, err
}
return q.db.GetChatWorkerAcquisitionCandidates(ctx, arg)
}
func (q *querier) GetChatWorkspaceTTL(ctx context.Context) (string, error) {
// The workspace-TTL setting is a deployment-wide value read by any
// authenticated chat user. We only require that an explicit actor is
@@ -3333,6 +3472,13 @@ func (q *querier) GetChatsByChatFileID(ctx context.Context, fileID uuid.UUID) ([
return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetChatsByChatFileID)(ctx, fileID)
}
func (q *querier) GetChatsByIDsForRunnerSync(ctx context.Context, ids []uuid.UUID) ([]database.Chat, error) {
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceChat); err != nil {
return nil, err
}
return q.db.GetChatsByIDsForRunnerSync(ctx, ids)
}
func (q *querier) GetChatsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]database.Chat, error) {
return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetChatsByWorkspaceIDs)(ctx, ids)
}
@@ -3403,6 +3549,10 @@ func (q *querier) GetDERPMeshKey(ctx context.Context) (string, error) {
return q.db.GetDERPMeshKey(ctx)
}
func (q *querier) GetDatabaseNow(ctx context.Context) (time.Time, error) {
return q.db.GetDatabaseNow(ctx)
}
func (q *querier) GetDefaultChatModelConfig(ctx context.Context) (database.ChatModelConfig, error) {
// Reading the default model config is needed for chat creation.
// TODO(CODAGT-161): scope this check when org context is available.
@@ -5468,6 +5618,18 @@ func (q *querier) GetWorkspacesForWorkspaceMetrics(ctx context.Context) ([]datab
return q.db.GetWorkspacesForWorkspaceMetrics(ctx)
}
func (q *querier) IncrementChatGenerationAttempt(ctx context.Context, id uuid.UUID) (int64, error) {
chat, err := q.db.GetChatByID(ctx, id)
if err != nil {
return 0, err
}
if err := q.authorizeContext(ctx, policy.ActionUpdate, chat); err != nil {
return 0, err
}
_ = chat
return q.db.IncrementChatGenerationAttempt(ctx, id)
}
func (q *querier) InsertAIBridgeInterception(ctx context.Context, arg database.InsertAIBridgeInterceptionParams) (database.AIBridgeInterception, error) {
return insert(q.log, q.auth, rbac.ResourceAibridgeInterception.WithOwner(arg.InitiatorID.String()), q.db.InsertAIBridgeInterception)(ctx, arg)
}
@@ -5638,6 +5800,18 @@ func (q *querier) InsertChatQueuedMessage(ctx context.Context, arg database.Inse
return q.db.InsertChatQueuedMessage(ctx, arg)
}
func (q *querier) InsertChatQueuedMessageWithCreator(ctx context.Context, arg database.InsertChatQueuedMessageWithCreatorParams) (database.ChatQueuedMessage, error) {
chat, err := q.db.GetChatByID(ctx, arg.ChatID)
if err != nil {
return database.ChatQueuedMessage{}, err
}
if err := q.authorizeContext(ctx, policy.ActionUpdate, chat); err != nil {
return database.ChatQueuedMessage{}, err
}
_ = chat
return q.db.InsertChatQueuedMessageWithCreator(ctx, arg)
}
func (q *querier) InsertCryptoKey(ctx context.Context, arg database.InsertCryptoKeyParams) (database.CryptoKey, error) {
if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceCryptoKey); err != nil {
return database.CryptoKey{}, err
@@ -6210,6 +6384,14 @@ func (q *querier) InsertWorkspaceResourceMetadata(ctx context.Context, arg datab
return q.db.InsertWorkspaceResourceMetadata(ctx, arg)
}
func (q *querier) IsChatHeartbeatStale(ctx context.Context, arg database.IsChatHeartbeatStaleParams) (bool, error) {
_, err := q.GetChatByID(ctx, arg.ChatID)
if err != nil {
return false, err
}
return q.db.IsChatHeartbeatStale(ctx, arg)
}
func (q *querier) LinkChatFiles(ctx context.Context, arg database.LinkChatFilesParams) (int32, error) {
chat, err := q.db.GetChatByID(ctx, arg.ChatID)
if err != nil {
@@ -6394,6 +6576,18 @@ func (q *querier) ListWorkspaceAgentPortShares(ctx context.Context, workspaceID
return q.db.ListWorkspaceAgentPortShares(ctx, workspaceID)
}
func (q *querier) LockChatAndBumpSnapshotVersion(ctx context.Context, id uuid.UUID) (database.Chat, error) {
chat, err := q.db.GetChatByID(ctx, id)
if err != nil {
return database.Chat{}, err
}
if err := q.authorizeContext(ctx, policy.ActionUpdate, chat); err != nil {
return database.Chat{}, err
}
_ = chat
return q.db.LockChatAndBumpSnapshotVersion(ctx, id)
}
func (q *querier) MarkAllInboxNotificationsAsRead(ctx context.Context, arg database.MarkAllInboxNotificationsAsReadParams) error {
resource := rbac.ResourceInboxNotification.WithOwner(arg.UserID.String())
@@ -6500,6 +6694,18 @@ func (q *querier) ReorderChatQueuedMessageToFront(ctx context.Context, arg datab
return q.db.ReorderChatQueuedMessageToFront(ctx, arg)
}
func (q *querier) ReorderChatQueuedMessageToHead(ctx context.Context, arg database.ReorderChatQueuedMessageToHeadParams) (int64, error) {
chat, err := q.db.GetChatByID(ctx, arg.ChatID)
if err != nil {
return 0, err
}
if err := q.authorizeContext(ctx, policy.ActionUpdate, chat); err != nil {
return 0, err
}
_ = chat
return q.db.ReorderChatQueuedMessageToHead(ctx, arg)
}
func (q *querier) ResolveUserChatSpendLimit(ctx context.Context, arg database.ResolveUserChatSpendLimitParams) (database.ResolveUserChatSpendLimitRow, error) {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceChat.WithOwner(arg.UserID.String())); err != nil {
return database.ResolveUserChatSpendLimitRow{}, err
@@ -6742,6 +6948,18 @@ func (q *querier) UpdateChatDebugStep(ctx context.Context, arg database.UpdateCh
return q.db.UpdateChatDebugStep(ctx, arg)
}
func (q *querier) UpdateChatExecutionState(ctx context.Context, arg database.UpdateChatExecutionStateParams) (database.Chat, error) {
chat, err := q.db.GetChatByID(ctx, arg.ID)
if err != nil {
return database.Chat{}, err
}
if err := q.authorizeContext(ctx, policy.ActionUpdate, chat); err != nil {
return database.Chat{}, err
}
_ = chat
return q.db.UpdateChatExecutionState(ctx, arg)
}
func (q *querier) UpdateChatHeartbeats(ctx context.Context, arg database.UpdateChatHeartbeatsParams) ([]uuid.UUID, error) {
// The batch heartbeat is a system-level operation filtered by
// worker_id. Authorization is enforced by the AsChatd context
@@ -6864,6 +7082,19 @@ func (q *querier) UpdateChatPlanModeByID(ctx context.Context, arg database.Updat
return q.db.UpdateChatPlanModeByID(ctx, arg)
}
func (q *querier) UpdateChatRetryState(ctx context.Context, arg database.UpdateChatRetryStateParams) (database.Chat, error) {
// UpdateChatRetryState is used by the chat processor to publish
// transient retry state. It should be called with system context.
chat, err := q.db.GetChatByID(ctx, arg.ID)
if err != nil {
return database.Chat{}, err
}
if err := q.authorizeContext(ctx, policy.ActionUpdate, chat); err != nil {
return database.Chat{}, err
}
return q.db.UpdateChatRetryState(ctx, arg)
}
func (q *querier) UpdateChatStatus(ctx context.Context, arg database.UpdateChatStatusParams) (database.Chat, error) {
// UpdateChatStatus is used by the chat processor to change chat status.
// It should be called with system context.
@@ -8243,6 +8474,18 @@ func (q *querier) UpsertChatGeneralModelOverride(ctx context.Context, value stri
return q.db.UpsertChatGeneralModelOverride(ctx, value)
}
func (q *querier) UpsertChatHeartbeat(ctx context.Context, arg database.UpsertChatHeartbeatParams) error {
chat, err := q.db.GetChatByID(ctx, arg.ChatID)
if err != nil {
return err
}
if err := q.authorizeContext(ctx, policy.ActionUpdate, chat); err != nil {
return err
}
_ = chat
return q.db.UpsertChatHeartbeat(ctx, arg)
}
func (q *querier) UpsertChatIncludeDefaultSystemPrompt(ctx context.Context, includeDefaultSystemPrompt bool) error {
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceDeploymentConfig); err != nil {
return err
+186 -6
View File
@@ -537,6 +537,21 @@ func (s *MethodTestSuite) TestChats() {
dbm.EXPECT().AcquireChats(gomock.Any(), arg).Return([]database.Chat{chat}, nil).AnyTimes()
check.Args(arg).Asserts(rbac.ResourceChat, policy.ActionUpdate).Returns([]database.Chat{chat})
}))
s.Run("GetChatWorkerAcquisitionCandidates", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
arg := database.GetChatWorkerAcquisitionCandidatesParams{
StaleSeconds: 30,
LimitCount: 100,
}
row := testutil.Fake(s.T(), faker, database.GetChatWorkerAcquisitionCandidatesRow{})
dbm.EXPECT().GetChatWorkerAcquisitionCandidates(gomock.Any(), arg).Return([]database.GetChatWorkerAcquisitionCandidatesRow{row}, nil).AnyTimes()
check.Args(arg).Asserts(rbac.ResourceChat, policy.ActionUpdate).Returns([]database.GetChatWorkerAcquisitionCandidatesRow{row})
}))
s.Run("GetChatsByIDsForRunnerSync", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
ids := []uuid.UUID{uuid.New(), uuid.New()}
chat := testutil.Fake(s.T(), faker, database.Chat{ID: ids[0]})
dbm.EXPECT().GetChatsByIDsForRunnerSync(gomock.Any(), ids).Return([]database.Chat{chat}, nil).AnyTimes()
check.Args(ids).Asserts(rbac.ResourceChat, policy.ActionUpdate).Returns([]database.Chat{chat})
}))
s.Run("DeleteAllChatQueuedMessages", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
chat := testutil.Fake(s.T(), faker, database.Chat{})
dbm.EXPECT().GetChatByID(gomock.Any(), chat.ID).Return(chat, nil).AnyTimes()
@@ -763,6 +778,24 @@ func (s *MethodTestSuite) TestChats() {
dbm.EXPECT().GetChatByIDForUpdate(gomock.Any(), chat.ID).Return(chat, nil).AnyTimes()
check.Args(chat.ID).Asserts(chat, policy.ActionRead).Returns(chat)
}))
s.Run("GetChatByIDForShare", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
chat := testutil.Fake(s.T(), faker, database.Chat{})
dbm.EXPECT().GetChatByIDForShare(gomock.Any(), chat.ID).Return(chat, nil).AnyTimes()
check.Args(chat.ID).Asserts(chat, policy.ActionRead).Returns(chat)
}))
s.Run("GetChatStreamSyncRows", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
ids := []uuid.UUID{uuid.New(), uuid.New()}
rows := []database.GetChatStreamSyncRowsRow{{ID: ids[0]}}
dbm.EXPECT().GetChatStreamSyncRows(gomock.Any(), ids).Return(rows, nil).AnyTimes()
check.Args(ids).Asserts(rbac.ResourceChat, policy.ActionRead).Returns(rows)
}))
s.Run("GetChatFamilyIDsByRootID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
chat := testutil.Fake(s.T(), faker, database.Chat{})
ids := []uuid.UUID{chat.ID}
dbm.EXPECT().GetChatByID(gomock.Any(), chat.ID).Return(chat, nil).AnyTimes()
dbm.EXPECT().GetChatFamilyIDsByRootID(gomock.Any(), chat.ID).Return(ids, nil).AnyTimes()
check.Args(chat.ID).Asserts(chat, policy.ActionRead).Returns(ids)
}))
s.Run("GetChatsByWorkspaceIDs", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
chatA := testutil.Fake(s.T(), faker, database.Chat{})
chatB := testutil.Fake(s.T(), faker, database.Chat{})
@@ -950,9 +983,10 @@ func (s *MethodTestSuite) TestChats() {
dbm.EXPECT().UpsertChatAutoArchiveDays(gomock.Any(), int32(90)).Return(nil).AnyTimes()
check.Args(int32(90)).Asserts(rbac.ResourceDeploymentConfig, policy.ActionUpdate)
}))
s.Run("AutoArchiveInactiveChats", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
dbm.EXPECT().AutoArchiveInactiveChats(gomock.Any(), database.AutoArchiveInactiveChatsParams{}).Return([]database.AutoArchiveInactiveChatsRow{}, nil).AnyTimes()
check.Args(database.AutoArchiveInactiveChatsParams{}).Asserts(rbac.ResourceChat, policy.ActionUpdate)
s.Run("GetAutoArchiveInactiveChatCandidates", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
arg := database.GetAutoArchiveInactiveChatCandidatesParams{LimitCount: 100}
dbm.EXPECT().GetAutoArchiveInactiveChatCandidates(gomock.Any(), arg).Return([]database.GetAutoArchiveInactiveChatCandidatesRow{}, nil).AnyTimes()
check.Args(arg).Asserts(rbac.ResourceChat, policy.ActionUpdate).Returns([]database.GetAutoArchiveInactiveChatCandidatesRow{})
}))
s.Run("GetChatMessageByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
chat := testutil.Fake(s.T(), faker, database.Chat{})
@@ -985,6 +1019,14 @@ func (s *MethodTestSuite) TestChats() {
dbm.EXPECT().GetChatMessagesByChatIDDescPaginated(gomock.Any(), arg).Return(msgs, nil).AnyTimes()
check.Args(arg).Asserts(chat, policy.ActionRead).Returns(msgs)
}))
s.Run("GetChatMessagesByRevisionForStream", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
chat := testutil.Fake(s.T(), faker, database.Chat{})
msgs := []database.ChatMessage{testutil.Fake(s.T(), faker, database.ChatMessage{ChatID: chat.ID})}
arg := database.GetChatMessagesByRevisionForStreamParams{ChatID: chat.ID, AfterRevision: 1}
dbm.EXPECT().GetChatByID(gomock.Any(), chat.ID).Return(chat, nil).AnyTimes()
dbm.EXPECT().GetChatMessagesByRevisionForStream(gomock.Any(), arg).Return(msgs, nil).AnyTimes()
check.Args(arg).Asserts(chat, policy.ActionRead).Returns(msgs)
}))
s.Run("GetChatUserPromptsByChatID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
chat := testutil.Fake(s.T(), faker, database.Chat{})
rows := []database.GetChatUserPromptsByChatIDRow{{ID: 1, Text: "hello"}}
@@ -1214,6 +1256,136 @@ func (s *MethodTestSuite) TestChats() {
dbm.EXPECT().UpdateChatACLByID(gomock.Any(), arg).Return(nil).AnyTimes()
check.Args(arg).Asserts(chat, policy.ActionShare).Returns()
}))
s.Run("LockChatAndBumpSnapshotVersion", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
chat := testutil.Fake(s.T(), faker, database.Chat{})
dbm.EXPECT().GetChatByID(gomock.Any(), chat.ID).Return(chat, nil).AnyTimes()
dbm.EXPECT().LockChatAndBumpSnapshotVersion(gomock.Any(), chat.ID).Return(chat, nil).AnyTimes()
check.Args(chat.ID).Asserts(chat, policy.ActionUpdate).Returns(chat)
}))
s.Run("UpdateChatExecutionState", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
chat := testutil.Fake(s.T(), faker, database.Chat{})
arg := database.UpdateChatExecutionStateParams{ID: chat.ID, Status: database.ChatStatusRunning}
dbm.EXPECT().GetChatByID(gomock.Any(), chat.ID).Return(chat, nil).AnyTimes()
dbm.EXPECT().UpdateChatExecutionState(gomock.Any(), arg).Return(chat, nil).AnyTimes()
check.Args(arg).Asserts(chat, policy.ActionUpdate).Returns(chat)
}))
s.Run("IncrementChatGenerationAttempt", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
chat := testutil.Fake(s.T(), faker, database.Chat{})
dbm.EXPECT().GetChatByID(gomock.Any(), chat.ID).Return(chat, nil).AnyTimes()
dbm.EXPECT().IncrementChatGenerationAttempt(gomock.Any(), chat.ID).Return(int64(7), nil).AnyTimes()
check.Args(chat.ID).Asserts(chat, policy.ActionUpdate).Returns(int64(7))
}))
s.Run("UpdateChatRetryState", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
chat := testutil.Fake(s.T(), faker, database.Chat{})
arg := database.UpdateChatRetryStateParams{ID: chat.ID, RetryState: []byte(`{"attempt":1}`)}
dbm.EXPECT().GetChatByID(gomock.Any(), chat.ID).Return(chat, nil).AnyTimes()
dbm.EXPECT().UpdateChatRetryState(gomock.Any(), arg).Return(chat, nil).AnyTimes()
check.Args(arg).Asserts(chat, policy.ActionUpdate).Returns(chat)
}))
s.Run("GetDatabaseNow", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
now := time.Now()
dbm.EXPECT().GetDatabaseNow(gomock.Any()).Return(now, nil).AnyTimes()
check.Args().Asserts().Returns(now)
}))
s.Run("InsertChatQueuedMessageWithCreator", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
chat := testutil.Fake(s.T(), faker, database.Chat{})
arg := testutil.Fake(s.T(), faker, database.InsertChatQueuedMessageWithCreatorParams{ChatID: chat.ID})
qm := testutil.Fake(s.T(), faker, database.ChatQueuedMessage{})
dbm.EXPECT().GetChatByID(gomock.Any(), chat.ID).Return(chat, nil).AnyTimes()
dbm.EXPECT().InsertChatQueuedMessageWithCreator(gomock.Any(), arg).Return(qm, nil).AnyTimes()
check.Args(arg).Asserts(chat, policy.ActionUpdate).Returns(qm)
}))
s.Run("GetChatQueuedMessagesByPosition", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
chat := testutil.Fake(s.T(), faker, database.Chat{})
qms := []database.ChatQueuedMessage{}
dbm.EXPECT().GetChatByID(gomock.Any(), chat.ID).Return(chat, nil).AnyTimes()
dbm.EXPECT().GetChatQueuedMessagesByPosition(gomock.Any(), chat.ID).Return(qms, nil).AnyTimes()
check.Args(chat.ID).Asserts(chat, policy.ActionRead).Returns(qms)
}))
s.Run("CountChatQueuedMessages", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
chat := testutil.Fake(s.T(), faker, database.Chat{})
dbm.EXPECT().GetChatByID(gomock.Any(), chat.ID).Return(chat, nil).AnyTimes()
dbm.EXPECT().CountChatQueuedMessages(gomock.Any(), chat.ID).Return(int64(3), nil).AnyTimes()
check.Args(chat.ID).Asserts(chat, policy.ActionRead).Returns(int64(3))
}))
s.Run("GetChatQueuedMessageHead", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
chat := testutil.Fake(s.T(), faker, database.Chat{})
qm := testutil.Fake(s.T(), faker, database.ChatQueuedMessage{ChatID: chat.ID})
dbm.EXPECT().GetChatByID(gomock.Any(), chat.ID).Return(chat, nil).AnyTimes()
dbm.EXPECT().GetChatQueuedMessageHead(gomock.Any(), chat.ID).Return(qm, nil).AnyTimes()
check.Args(chat.ID).Asserts(chat, policy.ActionRead).Returns(qm)
}))
s.Run("GetChatQueuedMessageByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
chat := testutil.Fake(s.T(), faker, database.Chat{})
qm := testutil.Fake(s.T(), faker, database.ChatQueuedMessage{ChatID: chat.ID})
arg := database.GetChatQueuedMessageByIDParams{ID: qm.ID, ChatID: chat.ID}
dbm.EXPECT().GetChatByID(gomock.Any(), chat.ID).Return(chat, nil).AnyTimes()
dbm.EXPECT().GetChatQueuedMessageByID(gomock.Any(), arg).Return(qm, nil).AnyTimes()
check.Args(arg).Asserts(chat, policy.ActionRead).Returns(qm)
}))
s.Run("DeleteChatQueuedMessageReturningCount", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
chat := testutil.Fake(s.T(), faker, database.Chat{})
arg := database.DeleteChatQueuedMessageReturningCountParams{ID: 1, ChatID: chat.ID}
dbm.EXPECT().GetChatByID(gomock.Any(), chat.ID).Return(chat, nil).AnyTimes()
dbm.EXPECT().DeleteChatQueuedMessageReturningCount(gomock.Any(), arg).Return(int64(1), nil).AnyTimes()
check.Args(arg).Asserts(chat, policy.ActionUpdate).Returns(int64(1))
}))
s.Run("DeleteAllChatQueuedMessagesReturningCount", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
chat := testutil.Fake(s.T(), faker, database.Chat{})
dbm.EXPECT().GetChatByID(gomock.Any(), chat.ID).Return(chat, nil).AnyTimes()
dbm.EXPECT().DeleteAllChatQueuedMessagesReturningCount(gomock.Any(), chat.ID).Return(int64(1), nil).AnyTimes()
check.Args(chat.ID).Asserts(chat, policy.ActionUpdate).Returns(int64(1))
}))
s.Run("ReorderChatQueuedMessageToHead", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
chat := testutil.Fake(s.T(), faker, database.Chat{})
arg := database.ReorderChatQueuedMessageToHeadParams{ChatID: chat.ID, ID: 1}
dbm.EXPECT().GetChatByID(gomock.Any(), chat.ID).Return(chat, nil).AnyTimes()
dbm.EXPECT().ReorderChatQueuedMessageToHead(gomock.Any(), arg).Return(int64(1), nil).AnyTimes()
check.Args(arg).Asserts(chat, policy.ActionUpdate).Returns(int64(1))
}))
s.Run("UpsertChatHeartbeat", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
chat := testutil.Fake(s.T(), faker, database.Chat{})
arg := database.UpsertChatHeartbeatParams{ChatID: chat.ID, RunnerID: uuid.New()}
dbm.EXPECT().GetChatByID(gomock.Any(), chat.ID).Return(chat, nil).AnyTimes()
dbm.EXPECT().UpsertChatHeartbeat(gomock.Any(), arg).Return(nil).AnyTimes()
check.Args(arg).Asserts(chat, policy.ActionUpdate).Returns()
}))
s.Run("BatchUpsertChatHeartbeats", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
arg := database.BatchUpsertChatHeartbeatsParams{ChatIds: []uuid.UUID{uuid.New()}, RunnerIds: []uuid.UUID{uuid.New()}}
dbm.EXPECT().BatchUpsertChatHeartbeats(gomock.Any(), arg).Return(nil).AnyTimes()
check.Args(arg).Asserts(rbac.ResourceChat, policy.ActionUpdate).Returns()
}))
s.Run("GetChatHeartbeat", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
chat := testutil.Fake(s.T(), faker, database.Chat{})
arg := database.GetChatHeartbeatParams{ChatID: chat.ID, RunnerID: uuid.New()}
hb := database.ChatHeartbeat{ChatID: chat.ID, RunnerID: arg.RunnerID}
dbm.EXPECT().GetChatByID(gomock.Any(), chat.ID).Return(chat, nil).AnyTimes()
dbm.EXPECT().GetChatHeartbeat(gomock.Any(), arg).Return(hb, nil).AnyTimes()
check.Args(arg).Asserts(chat, policy.ActionRead).Returns(hb)
}))
s.Run("IsChatHeartbeatStale", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
chat := testutil.Fake(s.T(), faker, database.Chat{})
arg := database.IsChatHeartbeatStaleParams{ChatID: chat.ID, RunnerID: uuid.New(), StaleSeconds: 30}
dbm.EXPECT().GetChatByID(gomock.Any(), chat.ID).Return(chat, nil).AnyTimes()
dbm.EXPECT().IsChatHeartbeatStale(gomock.Any(), arg).Return(false, nil).AnyTimes()
check.Args(arg).Asserts(chat, policy.ActionRead).Returns(false)
}))
s.Run("DeleteAllChatHeartbeats", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
chat := testutil.Fake(s.T(), faker, database.Chat{})
dbm.EXPECT().GetChatByID(gomock.Any(), chat.ID).Return(chat, nil).AnyTimes()
dbm.EXPECT().DeleteAllChatHeartbeats(gomock.Any(), chat.ID).Return(nil).AnyTimes()
check.Args(chat.ID).Asserts(chat, policy.ActionUpdate).Returns()
}))
s.Run("BatchDeleteChatHeartbeats", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
arg := database.BatchDeleteChatHeartbeatsParams{ChatIds: []uuid.UUID{uuid.New()}, RunnerIds: []uuid.UUID{uuid.New()}}
dbm.EXPECT().BatchDeleteChatHeartbeats(gomock.Any(), arg).Return(int64(1), nil).AnyTimes()
check.Args(arg).Asserts(rbac.ResourceChat, policy.ActionUpdate).Returns(int64(1))
}))
s.Run("DeleteStaleChatHeartbeats", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
const staleSeconds int32 = 30
dbm.EXPECT().DeleteStaleChatHeartbeats(gomock.Any(), staleSeconds).Return(int64(1), nil).AnyTimes()
check.Args(staleSeconds).Asserts(rbac.ResourceChat, policy.ActionUpdate).Returns(int64(1))
}))
s.Run("UpdateChatByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
chat := testutil.Fake(s.T(), faker, database.Chat{})
arg := database.UpdateChatByIDParams{
@@ -1408,6 +1580,14 @@ func (s *MethodTestSuite) TestChats() {
dbm.EXPECT().BackoffChatDiffStatus(gomock.Any(), arg).Return(nil).AnyTimes()
check.Args(arg).Asserts(rbac.ResourceChat, policy.ActionUpdate).Returns()
}))
s.Run("AutoArchiveInactiveChats", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
arg := database.AutoArchiveInactiveChatsParams{
ArchiveCutoff: dbtime.Now(),
LimitCount: 100,
}
dbm.EXPECT().AutoArchiveInactiveChats(gomock.Any(), arg).Return([]database.AutoArchiveInactiveChatsRow{}, nil).AnyTimes()
check.Args(arg).Asserts(rbac.ResourceChat, policy.ActionUpdate).Returns([]database.AutoArchiveInactiveChatsRow{})
}))
s.Run("UpsertChatIncludeDefaultSystemPrompt", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
dbm.EXPECT().UpsertChatIncludeDefaultSystemPrompt(gomock.Any(), false).Return(nil).AnyTimes()
check.Args(false).Asserts(rbac.ResourceDeploymentConfig, policy.ActionUpdate)
@@ -1697,9 +1877,9 @@ func (s *MethodTestSuite) TestChats() {
s.Run("UpdateChatLastTurnSummary", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
chat := testutil.Fake(s.T(), faker, database.Chat{})
arg := database.UpdateChatLastTurnSummaryParams{
ID: chat.ID,
ExpectedUpdatedAt: chat.UpdatedAt,
LastTurnSummary: sql.NullString{String: "resolved the issue", Valid: true},
ID: chat.ID,
ExpectedHistoryVersion: chat.HistoryVersion,
LastTurnSummary: sql.NullString{String: "resolved the issue", Valid: true},
}
dbm.EXPECT().GetChatByID(gomock.Any(), chat.ID).Return(chat, nil).AnyTimes()
dbm.EXPECT().UpdateChatLastTurnSummary(gomock.Any(), arg).Return(int64(1), nil).AnyTimes()
+12 -2
View File
@@ -115,13 +115,23 @@ func ChatMessage(t testing.TB, db database.Store, seed database.ChatMessage) dat
if seed.Content.Valid {
content = string(seed.Content.RawMessage)
}
role := takeFirst(seed.Role, database.ChatMessageRoleUser)
apiKeyID := seed.APIKeyID.String
// Mint a real API key for user turns so the api_key_id foreign key is
// satisfied. Without a creator we leave it empty, which the insert query
// stores as NULL.
if role == database.ChatMessageRoleUser && apiKeyID == "" &&
seed.CreatedBy.Valid && seed.CreatedBy.UUID != uuid.Nil {
key, _ := APIKey(t, db, database.APIKey{UserID: seed.CreatedBy.UUID})
apiKeyID = key.ID
}
msgs, err := db.InsertChatMessages(genCtx, database.InsertChatMessagesParams{
ChatID: seed.ChatID,
CreatedBy: []uuid.UUID{seed.CreatedBy.UUID},
APIKeyID: []string{seed.APIKeyID.String},
APIKeyID: []string{apiKeyID},
ModelConfigID: []uuid.UUID{seed.ModelConfigID.UUID},
Role: []database.ChatMessageRole{takeFirst(seed.Role, database.ChatMessageRoleUser)},
Role: []database.ChatMessageRole{role},
Content: []string{content},
ContentVersion: []int16{takeFirst(seed.ContentVersion, chatprompt.CurrentContentVersion)},
Visibility: []database.ChatMessageVisibility{takeFirst(seed.Visibility, database.ChatMessageVisibilityBoth)},
+216
View File
@@ -202,6 +202,14 @@ func (m queryMetricsStore) BackoffChatDiffStatus(ctx context.Context, arg databa
return r0
}
func (m queryMetricsStore) BatchDeleteChatHeartbeats(ctx context.Context, arg database.BatchDeleteChatHeartbeatsParams) (int64, error) {
start := time.Now()
r0, r1 := m.s.BatchDeleteChatHeartbeats(ctx, arg)
m.queryLatencies.WithLabelValues("BatchDeleteChatHeartbeats").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "BatchDeleteChatHeartbeats").Inc()
return r0, r1
}
func (m queryMetricsStore) BatchUpdateWorkspaceAgentMetadata(ctx context.Context, arg database.BatchUpdateWorkspaceAgentMetadataParams) error {
start := time.Now()
r0 := m.s.BatchUpdateWorkspaceAgentMetadata(ctx, arg)
@@ -226,6 +234,14 @@ func (m queryMetricsStore) BatchUpdateWorkspaceNextStartAt(ctx context.Context,
return r0
}
func (m queryMetricsStore) BatchUpsertChatHeartbeats(ctx context.Context, arg database.BatchUpsertChatHeartbeatsParams) error {
start := time.Now()
r0 := m.s.BatchUpsertChatHeartbeats(ctx, arg)
m.queryLatencies.WithLabelValues("BatchUpsertChatHeartbeats").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "BatchUpsertChatHeartbeats").Inc()
return r0
}
func (m queryMetricsStore) BatchUpsertConnectionLogs(ctx context.Context, arg database.BatchUpsertConnectionLogsParams) error {
start := time.Now()
r0 := m.s.BatchUpsertConnectionLogs(ctx, arg)
@@ -322,6 +338,14 @@ func (m queryMetricsStore) CountAuditLogs(ctx context.Context, arg database.Coun
return r0, r1
}
func (m queryMetricsStore) CountChatQueuedMessages(ctx context.Context, chatID uuid.UUID) (int64, error) {
start := time.Now()
r0, r1 := m.s.CountChatQueuedMessages(ctx, chatID)
m.queryLatencies.WithLabelValues("CountChatQueuedMessages").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "CountChatQueuedMessages").Inc()
return r0, r1
}
func (m queryMetricsStore) CountConnectionLogs(ctx context.Context, arg database.CountConnectionLogsParams) (int64, error) {
start := time.Now()
r0, r1 := m.s.CountConnectionLogs(ctx, arg)
@@ -418,6 +442,14 @@ func (m queryMetricsStore) DeleteAPIKeysByUserID(ctx context.Context, userID uui
return r0
}
func (m queryMetricsStore) DeleteAllChatHeartbeats(ctx context.Context, chatID uuid.UUID) error {
start := time.Now()
r0 := m.s.DeleteAllChatHeartbeats(ctx, chatID)
m.queryLatencies.WithLabelValues("DeleteAllChatHeartbeats").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "DeleteAllChatHeartbeats").Inc()
return r0
}
func (m queryMetricsStore) DeleteAllChatQueuedMessages(ctx context.Context, chatID uuid.UUID) error {
start := time.Now()
r0 := m.s.DeleteAllChatQueuedMessages(ctx, chatID)
@@ -426,6 +458,14 @@ func (m queryMetricsStore) DeleteAllChatQueuedMessages(ctx context.Context, chat
return r0
}
func (m queryMetricsStore) DeleteAllChatQueuedMessagesReturningCount(ctx context.Context, chatID uuid.UUID) (int64, error) {
start := time.Now()
r0, r1 := m.s.DeleteAllChatQueuedMessagesReturningCount(ctx, chatID)
m.queryLatencies.WithLabelValues("DeleteAllChatQueuedMessagesReturningCount").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "DeleteAllChatQueuedMessagesReturningCount").Inc()
return r0, r1
}
func (m queryMetricsStore) DeleteAllTailnetTunnels(ctx context.Context, arg database.DeleteAllTailnetTunnelsParams) ([]database.DeleteAllTailnetTunnelsRow, error) {
start := time.Now()
r0, r1 := m.s.DeleteAllTailnetTunnels(ctx, arg)
@@ -498,6 +538,14 @@ func (m queryMetricsStore) DeleteChatQueuedMessage(ctx context.Context, arg data
return r0
}
func (m queryMetricsStore) DeleteChatQueuedMessageReturningCount(ctx context.Context, arg database.DeleteChatQueuedMessageReturningCountParams) (int64, error) {
start := time.Now()
r0, r1 := m.s.DeleteChatQueuedMessageReturningCount(ctx, arg)
m.queryLatencies.WithLabelValues("DeleteChatQueuedMessageReturningCount").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "DeleteChatQueuedMessageReturningCount").Inc()
return r0, r1
}
func (m queryMetricsStore) DeleteChatUsageLimitGroupOverride(ctx context.Context, groupID uuid.UUID) error {
start := time.Now()
r0 := m.s.DeleteChatUsageLimitGroupOverride(ctx, groupID)
@@ -778,6 +826,14 @@ func (m queryMetricsStore) DeleteRuntimeConfig(ctx context.Context, key string)
return r0
}
func (m queryMetricsStore) DeleteStaleChatHeartbeats(ctx context.Context, staleSeconds int32) (int64, error) {
start := time.Now()
r0, r1 := m.s.DeleteStaleChatHeartbeats(ctx, staleSeconds)
m.queryLatencies.WithLabelValues("DeleteStaleChatHeartbeats").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "DeleteStaleChatHeartbeats").Inc()
return r0, r1
}
func (m queryMetricsStore) DeleteTailnetPeer(ctx context.Context, arg database.DeleteTailnetPeerParams) (database.DeleteTailnetPeerRow, error) {
start := time.Now()
r0, r1 := m.s.DeleteTailnetPeer(ctx, arg)
@@ -1274,6 +1330,14 @@ func (m queryMetricsStore) GetAuthorizationUserRoles(ctx context.Context, userID
return r0, r1
}
func (m queryMetricsStore) GetAutoArchiveInactiveChatCandidates(ctx context.Context, arg database.GetAutoArchiveInactiveChatCandidatesParams) ([]database.GetAutoArchiveInactiveChatCandidatesRow, error) {
start := time.Now()
r0, r1 := m.s.GetAutoArchiveInactiveChatCandidates(ctx, arg)
m.queryLatencies.WithLabelValues("GetAutoArchiveInactiveChatCandidates").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetAutoArchiveInactiveChatCandidates").Inc()
return r0, r1
}
func (m queryMetricsStore) GetBoundaryLogByID(ctx context.Context, id uuid.UUID) (database.BoundaryLog, error) {
start := time.Now()
r0, r1 := m.s.GetBoundaryLogByID(ctx, id)
@@ -1322,6 +1386,14 @@ func (m queryMetricsStore) GetChatByID(ctx context.Context, id uuid.UUID) (datab
return r0, r1
}
func (m queryMetricsStore) GetChatByIDForShare(ctx context.Context, id uuid.UUID) (database.Chat, error) {
start := time.Now()
r0, r1 := m.s.GetChatByIDForShare(ctx, id)
m.queryLatencies.WithLabelValues("GetChatByIDForShare").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetChatByIDForShare").Inc()
return r0, r1
}
func (m queryMetricsStore) GetChatByIDForUpdate(ctx context.Context, id uuid.UUID) (database.Chat, error) {
start := time.Now()
r0, r1 := m.s.GetChatByIDForUpdate(ctx, id)
@@ -1450,6 +1522,14 @@ func (m queryMetricsStore) GetChatExploreModelOverride(ctx context.Context) (str
return r0, r1
}
func (m queryMetricsStore) GetChatFamilyIDsByRootID(ctx context.Context, id uuid.UUID) ([]uuid.UUID, error) {
start := time.Now()
r0, r1 := m.s.GetChatFamilyIDsByRootID(ctx, id)
m.queryLatencies.WithLabelValues("GetChatFamilyIDsByRootID").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetChatFamilyIDsByRootID").Inc()
return r0, r1
}
func (m queryMetricsStore) GetChatFileByID(ctx context.Context, id uuid.UUID) (database.ChatFile, error) {
start := time.Now()
r0, r1 := m.s.GetChatFileByID(ctx, id)
@@ -1482,6 +1562,14 @@ func (m queryMetricsStore) GetChatGeneralModelOverride(ctx context.Context) (str
return r0, r1
}
func (m queryMetricsStore) GetChatHeartbeat(ctx context.Context, arg database.GetChatHeartbeatParams) (database.ChatHeartbeat, error) {
start := time.Now()
r0, r1 := m.s.GetChatHeartbeat(ctx, arg)
m.queryLatencies.WithLabelValues("GetChatHeartbeat").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetChatHeartbeat").Inc()
return r0, r1
}
func (m queryMetricsStore) GetChatIncludeDefaultSystemPrompt(ctx context.Context) (bool, error) {
start := time.Now()
r0, r1 := m.s.GetChatIncludeDefaultSystemPrompt(ctx)
@@ -1530,6 +1618,14 @@ func (m queryMetricsStore) GetChatMessagesByChatIDDescPaginated(ctx context.Cont
return r0, r1
}
func (m queryMetricsStore) GetChatMessagesByRevisionForStream(ctx context.Context, arg database.GetChatMessagesByRevisionForStreamParams) ([]database.ChatMessage, error) {
start := time.Now()
r0, r1 := m.s.GetChatMessagesByRevisionForStream(ctx, arg)
m.queryLatencies.WithLabelValues("GetChatMessagesByRevisionForStream").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetChatMessagesByRevisionForStream").Inc()
return r0, r1
}
func (m queryMetricsStore) GetChatMessagesForPromptByChatID(ctx context.Context, chatID uuid.UUID) ([]database.ChatMessage, error) {
start := time.Now()
r0, r1 := m.s.GetChatMessagesForPromptByChatID(ctx, chatID)
@@ -1578,6 +1674,22 @@ func (m queryMetricsStore) GetChatPlanModeInstructions(ctx context.Context) (str
return r0, r1
}
func (m queryMetricsStore) GetChatQueuedMessageByID(ctx context.Context, arg database.GetChatQueuedMessageByIDParams) (database.ChatQueuedMessage, error) {
start := time.Now()
r0, r1 := m.s.GetChatQueuedMessageByID(ctx, arg)
m.queryLatencies.WithLabelValues("GetChatQueuedMessageByID").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetChatQueuedMessageByID").Inc()
return r0, r1
}
func (m queryMetricsStore) GetChatQueuedMessageHead(ctx context.Context, chatID uuid.UUID) (database.ChatQueuedMessage, error) {
start := time.Now()
r0, r1 := m.s.GetChatQueuedMessageHead(ctx, chatID)
m.queryLatencies.WithLabelValues("GetChatQueuedMessageHead").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetChatQueuedMessageHead").Inc()
return r0, r1
}
func (m queryMetricsStore) GetChatQueuedMessages(ctx context.Context, chatID uuid.UUID) ([]database.ChatQueuedMessage, error) {
start := time.Now()
r0, r1 := m.s.GetChatQueuedMessages(ctx, chatID)
@@ -1586,6 +1698,14 @@ func (m queryMetricsStore) GetChatQueuedMessages(ctx context.Context, chatID uui
return r0, r1
}
func (m queryMetricsStore) GetChatQueuedMessagesByPosition(ctx context.Context, chatID uuid.UUID) ([]database.ChatQueuedMessage, error) {
start := time.Now()
r0, r1 := m.s.GetChatQueuedMessagesByPosition(ctx, chatID)
m.queryLatencies.WithLabelValues("GetChatQueuedMessagesByPosition").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetChatQueuedMessagesByPosition").Inc()
return r0, r1
}
func (m queryMetricsStore) GetChatRetentionDays(ctx context.Context) (int32, error) {
start := time.Now()
r0, r1 := m.s.GetChatRetentionDays(ctx)
@@ -1594,6 +1714,14 @@ func (m queryMetricsStore) GetChatRetentionDays(ctx context.Context) (int32, err
return r0, r1
}
func (m queryMetricsStore) GetChatStreamSyncRows(ctx context.Context, ids []uuid.UUID) ([]database.GetChatStreamSyncRowsRow, error) {
start := time.Now()
r0, r1 := m.s.GetChatStreamSyncRows(ctx, ids)
m.queryLatencies.WithLabelValues("GetChatStreamSyncRows").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetChatStreamSyncRows").Inc()
return r0, r1
}
func (m queryMetricsStore) GetChatSystemPrompt(ctx context.Context) (string, error) {
start := time.Now()
r0, r1 := m.s.GetChatSystemPrompt(ctx)
@@ -1658,6 +1786,14 @@ func (m queryMetricsStore) GetChatUserPromptsByChatID(ctx context.Context, arg d
return r0, r1
}
func (m queryMetricsStore) GetChatWorkerAcquisitionCandidates(ctx context.Context, arg database.GetChatWorkerAcquisitionCandidatesParams) ([]database.GetChatWorkerAcquisitionCandidatesRow, error) {
start := time.Now()
r0, r1 := m.s.GetChatWorkerAcquisitionCandidates(ctx, arg)
m.queryLatencies.WithLabelValues("GetChatWorkerAcquisitionCandidates").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetChatWorkerAcquisitionCandidates").Inc()
return r0, r1
}
func (m queryMetricsStore) GetChatWorkspaceTTL(ctx context.Context) (string, error) {
start := time.Now()
r0, r1 := m.s.GetChatWorkspaceTTL(ctx)
@@ -1682,6 +1818,14 @@ func (m queryMetricsStore) GetChatsByChatFileID(ctx context.Context, fileID uuid
return r0, r1
}
func (m queryMetricsStore) GetChatsByIDsForRunnerSync(ctx context.Context, ids []uuid.UUID) ([]database.Chat, error) {
start := time.Now()
r0, r1 := m.s.GetChatsByIDsForRunnerSync(ctx, ids)
m.queryLatencies.WithLabelValues("GetChatsByIDsForRunnerSync").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetChatsByIDsForRunnerSync").Inc()
return r0, r1
}
func (m queryMetricsStore) GetChatsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]database.Chat, error) {
start := time.Now()
r0, r1 := m.s.GetChatsByWorkspaceIDs(ctx, ids)
@@ -1754,6 +1898,14 @@ func (m queryMetricsStore) GetDERPMeshKey(ctx context.Context) (string, error) {
return r0, r1
}
func (m queryMetricsStore) GetDatabaseNow(ctx context.Context) (time.Time, error) {
start := time.Now()
r0, r1 := m.s.GetDatabaseNow(ctx)
m.queryLatencies.WithLabelValues("GetDatabaseNow").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetDatabaseNow").Inc()
return r0, r1
}
func (m queryMetricsStore) GetDefaultChatModelConfig(ctx context.Context) (database.ChatModelConfig, error) {
start := time.Now()
r0, r1 := m.s.GetDefaultChatModelConfig(ctx)
@@ -3706,6 +3858,14 @@ func (m queryMetricsStore) GetWorkspacesForWorkspaceMetrics(ctx context.Context)
return r0, r1
}
func (m queryMetricsStore) IncrementChatGenerationAttempt(ctx context.Context, id uuid.UUID) (int64, error) {
start := time.Now()
r0, r1 := m.s.IncrementChatGenerationAttempt(ctx, id)
m.queryLatencies.WithLabelValues("IncrementChatGenerationAttempt").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "IncrementChatGenerationAttempt").Inc()
return r0, r1
}
func (m queryMetricsStore) InsertAIBridgeInterception(ctx context.Context, arg database.InsertAIBridgeInterceptionParams) (database.AIBridgeInterception, error) {
start := time.Now()
r0, r1 := m.s.InsertAIBridgeInterception(ctx, arg)
@@ -3866,6 +4026,14 @@ func (m queryMetricsStore) InsertChatQueuedMessage(ctx context.Context, arg data
return r0, r1
}
func (m queryMetricsStore) InsertChatQueuedMessageWithCreator(ctx context.Context, arg database.InsertChatQueuedMessageWithCreatorParams) (database.ChatQueuedMessage, error) {
start := time.Now()
r0, r1 := m.s.InsertChatQueuedMessageWithCreator(ctx, arg)
m.queryLatencies.WithLabelValues("InsertChatQueuedMessageWithCreator").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "InsertChatQueuedMessageWithCreator").Inc()
return r0, r1
}
func (m queryMetricsStore) InsertCryptoKey(ctx context.Context, arg database.InsertCryptoKeyParams) (database.CryptoKey, error) {
start := time.Now()
r0, r1 := m.s.InsertCryptoKey(ctx, arg)
@@ -4362,6 +4530,14 @@ func (m queryMetricsStore) InsertWorkspaceResourceMetadata(ctx context.Context,
return r0, r1
}
func (m queryMetricsStore) IsChatHeartbeatStale(ctx context.Context, arg database.IsChatHeartbeatStaleParams) (bool, error) {
start := time.Now()
r0, r1 := m.s.IsChatHeartbeatStale(ctx, arg)
m.queryLatencies.WithLabelValues("IsChatHeartbeatStale").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "IsChatHeartbeatStale").Inc()
return r0, r1
}
func (m queryMetricsStore) LinkChatFiles(ctx context.Context, arg database.LinkChatFilesParams) (int32, error) {
start := time.Now()
r0, r1 := m.s.LinkChatFiles(ctx, arg)
@@ -4546,6 +4722,14 @@ func (m queryMetricsStore) ListWorkspaceAgentPortShares(ctx context.Context, wor
return r0, r1
}
func (m queryMetricsStore) LockChatAndBumpSnapshotVersion(ctx context.Context, id uuid.UUID) (database.Chat, error) {
start := time.Now()
r0, r1 := m.s.LockChatAndBumpSnapshotVersion(ctx, id)
m.queryLatencies.WithLabelValues("LockChatAndBumpSnapshotVersion").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "LockChatAndBumpSnapshotVersion").Inc()
return r0, r1
}
func (m queryMetricsStore) MarkAllInboxNotificationsAsRead(ctx context.Context, arg database.MarkAllInboxNotificationsAsReadParams) error {
start := time.Now()
r0 := m.s.MarkAllInboxNotificationsAsRead(ctx, arg)
@@ -4634,6 +4818,14 @@ func (m queryMetricsStore) ReorderChatQueuedMessageToFront(ctx context.Context,
return r0, r1
}
func (m queryMetricsStore) ReorderChatQueuedMessageToHead(ctx context.Context, arg database.ReorderChatQueuedMessageToHeadParams) (int64, error) {
start := time.Now()
r0, r1 := m.s.ReorderChatQueuedMessageToHead(ctx, arg)
m.queryLatencies.WithLabelValues("ReorderChatQueuedMessageToHead").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "ReorderChatQueuedMessageToHead").Inc()
return r0, r1
}
func (m queryMetricsStore) ResolveUserChatSpendLimit(ctx context.Context, userID database.ResolveUserChatSpendLimitParams) (database.ResolveUserChatSpendLimitRow, error) {
start := time.Now()
r0, r1 := m.s.ResolveUserChatSpendLimit(ctx, userID)
@@ -4826,6 +5018,14 @@ func (m queryMetricsStore) UpdateChatDebugStep(ctx context.Context, arg database
return r0, r1
}
func (m queryMetricsStore) UpdateChatExecutionState(ctx context.Context, arg database.UpdateChatExecutionStateParams) (database.Chat, error) {
start := time.Now()
r0, r1 := m.s.UpdateChatExecutionState(ctx, arg)
m.queryLatencies.WithLabelValues("UpdateChatExecutionState").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpdateChatExecutionState").Inc()
return r0, r1
}
func (m queryMetricsStore) UpdateChatHeartbeats(ctx context.Context, arg database.UpdateChatHeartbeatsParams) ([]uuid.UUID, error) {
start := time.Now()
r0, r1 := m.s.UpdateChatHeartbeats(ctx, arg)
@@ -4914,6 +5114,14 @@ func (m queryMetricsStore) UpdateChatPlanModeByID(ctx context.Context, arg datab
return r0, r1
}
func (m queryMetricsStore) UpdateChatRetryState(ctx context.Context, arg database.UpdateChatRetryStateParams) (database.Chat, error) {
start := time.Now()
r0, r1 := m.s.UpdateChatRetryState(ctx, arg)
m.queryLatencies.WithLabelValues("UpdateChatRetryState").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpdateChatRetryState").Inc()
return r0, r1
}
func (m queryMetricsStore) UpdateChatStatus(ctx context.Context, arg database.UpdateChatStatusParams) (database.Chat, error) {
start := time.Now()
r0, r1 := m.s.UpdateChatStatus(ctx, arg)
@@ -5866,6 +6074,14 @@ func (m queryMetricsStore) UpsertChatGeneralModelOverride(ctx context.Context, v
return r0
}
func (m queryMetricsStore) UpsertChatHeartbeat(ctx context.Context, arg database.UpsertChatHeartbeatParams) error {
start := time.Now()
r0 := m.s.UpsertChatHeartbeat(ctx, arg)
m.queryLatencies.WithLabelValues("UpsertChatHeartbeat").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpsertChatHeartbeat").Inc()
return r0
}
func (m queryMetricsStore) UpsertChatIncludeDefaultSystemPrompt(ctx context.Context, includeDefaultSystemPrompt bool) error {
start := time.Now()
r0 := m.s.UpsertChatIncludeDefaultSystemPrompt(ctx, includeDefaultSystemPrompt)
+402
View File
@@ -223,6 +223,21 @@ func (mr *MockStoreMockRecorder) BackoffChatDiffStatus(ctx, arg any) *gomock.Cal
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BackoffChatDiffStatus", reflect.TypeOf((*MockStore)(nil).BackoffChatDiffStatus), ctx, arg)
}
// BatchDeleteChatHeartbeats mocks base method.
func (m *MockStore) BatchDeleteChatHeartbeats(ctx context.Context, arg database.BatchDeleteChatHeartbeatsParams) (int64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "BatchDeleteChatHeartbeats", ctx, arg)
ret0, _ := ret[0].(int64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// BatchDeleteChatHeartbeats indicates an expected call of BatchDeleteChatHeartbeats.
func (mr *MockStoreMockRecorder) BatchDeleteChatHeartbeats(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BatchDeleteChatHeartbeats", reflect.TypeOf((*MockStore)(nil).BatchDeleteChatHeartbeats), ctx, arg)
}
// BatchUpdateWorkspaceAgentMetadata mocks base method.
func (m *MockStore) BatchUpdateWorkspaceAgentMetadata(ctx context.Context, arg database.BatchUpdateWorkspaceAgentMetadataParams) error {
m.ctrl.T.Helper()
@@ -265,6 +280,20 @@ func (mr *MockStoreMockRecorder) BatchUpdateWorkspaceNextStartAt(ctx, arg any) *
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BatchUpdateWorkspaceNextStartAt", reflect.TypeOf((*MockStore)(nil).BatchUpdateWorkspaceNextStartAt), ctx, arg)
}
// BatchUpsertChatHeartbeats mocks base method.
func (m *MockStore) BatchUpsertChatHeartbeats(ctx context.Context, arg database.BatchUpsertChatHeartbeatsParams) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "BatchUpsertChatHeartbeats", ctx, arg)
ret0, _ := ret[0].(error)
return ret0
}
// BatchUpsertChatHeartbeats indicates an expected call of BatchUpsertChatHeartbeats.
func (mr *MockStoreMockRecorder) BatchUpsertChatHeartbeats(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BatchUpsertChatHeartbeats", reflect.TypeOf((*MockStore)(nil).BatchUpsertChatHeartbeats), ctx, arg)
}
// BatchUpsertConnectionLogs mocks base method.
func (m *MockStore) BatchUpsertConnectionLogs(ctx context.Context, arg database.BatchUpsertConnectionLogsParams) error {
m.ctrl.T.Helper()
@@ -484,6 +513,21 @@ func (mr *MockStoreMockRecorder) CountAuthorizedConnectionLogs(ctx, arg, prepare
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CountAuthorizedConnectionLogs", reflect.TypeOf((*MockStore)(nil).CountAuthorizedConnectionLogs), ctx, arg, prepared)
}
// CountChatQueuedMessages mocks base method.
func (m *MockStore) CountChatQueuedMessages(ctx context.Context, chatID uuid.UUID) (int64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CountChatQueuedMessages", ctx, chatID)
ret0, _ := ret[0].(int64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CountChatQueuedMessages indicates an expected call of CountChatQueuedMessages.
func (mr *MockStoreMockRecorder) CountChatQueuedMessages(ctx, chatID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CountChatQueuedMessages", reflect.TypeOf((*MockStore)(nil).CountChatQueuedMessages), ctx, chatID)
}
// CountConnectionLogs mocks base method.
func (m *MockStore) CountConnectionLogs(ctx context.Context, arg database.CountConnectionLogsParams) (int64, error) {
m.ctrl.T.Helper()
@@ -660,6 +704,20 @@ func (mr *MockStoreMockRecorder) DeleteAPIKeysByUserID(ctx, userID any) *gomock.
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAPIKeysByUserID", reflect.TypeOf((*MockStore)(nil).DeleteAPIKeysByUserID), ctx, userID)
}
// DeleteAllChatHeartbeats mocks base method.
func (m *MockStore) DeleteAllChatHeartbeats(ctx context.Context, chatID uuid.UUID) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteAllChatHeartbeats", ctx, chatID)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteAllChatHeartbeats indicates an expected call of DeleteAllChatHeartbeats.
func (mr *MockStoreMockRecorder) DeleteAllChatHeartbeats(ctx, chatID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAllChatHeartbeats", reflect.TypeOf((*MockStore)(nil).DeleteAllChatHeartbeats), ctx, chatID)
}
// DeleteAllChatQueuedMessages mocks base method.
func (m *MockStore) DeleteAllChatQueuedMessages(ctx context.Context, chatID uuid.UUID) error {
m.ctrl.T.Helper()
@@ -674,6 +732,21 @@ func (mr *MockStoreMockRecorder) DeleteAllChatQueuedMessages(ctx, chatID any) *g
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAllChatQueuedMessages", reflect.TypeOf((*MockStore)(nil).DeleteAllChatQueuedMessages), ctx, chatID)
}
// DeleteAllChatQueuedMessagesReturningCount mocks base method.
func (m *MockStore) DeleteAllChatQueuedMessagesReturningCount(ctx context.Context, chatID uuid.UUID) (int64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteAllChatQueuedMessagesReturningCount", ctx, chatID)
ret0, _ := ret[0].(int64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DeleteAllChatQueuedMessagesReturningCount indicates an expected call of DeleteAllChatQueuedMessagesReturningCount.
func (mr *MockStoreMockRecorder) DeleteAllChatQueuedMessagesReturningCount(ctx, chatID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAllChatQueuedMessagesReturningCount", reflect.TypeOf((*MockStore)(nil).DeleteAllChatQueuedMessagesReturningCount), ctx, chatID)
}
// DeleteAllTailnetTunnels mocks base method.
func (m *MockStore) DeleteAllTailnetTunnels(ctx context.Context, arg database.DeleteAllTailnetTunnelsParams) ([]database.DeleteAllTailnetTunnelsRow, error) {
m.ctrl.T.Helper()
@@ -803,6 +876,21 @@ func (mr *MockStoreMockRecorder) DeleteChatQueuedMessage(ctx, arg any) *gomock.C
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteChatQueuedMessage", reflect.TypeOf((*MockStore)(nil).DeleteChatQueuedMessage), ctx, arg)
}
// DeleteChatQueuedMessageReturningCount mocks base method.
func (m *MockStore) DeleteChatQueuedMessageReturningCount(ctx context.Context, arg database.DeleteChatQueuedMessageReturningCountParams) (int64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteChatQueuedMessageReturningCount", ctx, arg)
ret0, _ := ret[0].(int64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DeleteChatQueuedMessageReturningCount indicates an expected call of DeleteChatQueuedMessageReturningCount.
func (mr *MockStoreMockRecorder) DeleteChatQueuedMessageReturningCount(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteChatQueuedMessageReturningCount", reflect.TypeOf((*MockStore)(nil).DeleteChatQueuedMessageReturningCount), ctx, arg)
}
// DeleteChatUsageLimitGroupOverride mocks base method.
func (m *MockStore) DeleteChatUsageLimitGroupOverride(ctx context.Context, groupID uuid.UUID) error {
m.ctrl.T.Helper()
@@ -1305,6 +1393,21 @@ func (mr *MockStoreMockRecorder) DeleteRuntimeConfig(ctx, key any) *gomock.Call
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteRuntimeConfig", reflect.TypeOf((*MockStore)(nil).DeleteRuntimeConfig), ctx, key)
}
// DeleteStaleChatHeartbeats mocks base method.
func (m *MockStore) DeleteStaleChatHeartbeats(ctx context.Context, staleSeconds int32) (int64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteStaleChatHeartbeats", ctx, staleSeconds)
ret0, _ := ret[0].(int64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DeleteStaleChatHeartbeats indicates an expected call of DeleteStaleChatHeartbeats.
func (mr *MockStoreMockRecorder) DeleteStaleChatHeartbeats(ctx, staleSeconds any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteStaleChatHeartbeats", reflect.TypeOf((*MockStore)(nil).DeleteStaleChatHeartbeats), ctx, staleSeconds)
}
// DeleteTailnetPeer mocks base method.
func (m *MockStore) DeleteTailnetPeer(ctx context.Context, arg database.DeleteTailnetPeerParams) (database.DeleteTailnetPeerRow, error) {
m.ctrl.T.Helper()
@@ -2341,6 +2444,21 @@ func (mr *MockStoreMockRecorder) GetAuthorizedWorkspacesAndAgentsByOwnerID(ctx,
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuthorizedWorkspacesAndAgentsByOwnerID", reflect.TypeOf((*MockStore)(nil).GetAuthorizedWorkspacesAndAgentsByOwnerID), ctx, ownerID, prepared)
}
// GetAutoArchiveInactiveChatCandidates mocks base method.
func (m *MockStore) GetAutoArchiveInactiveChatCandidates(ctx context.Context, arg database.GetAutoArchiveInactiveChatCandidatesParams) ([]database.GetAutoArchiveInactiveChatCandidatesRow, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAutoArchiveInactiveChatCandidates", ctx, arg)
ret0, _ := ret[0].([]database.GetAutoArchiveInactiveChatCandidatesRow)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetAutoArchiveInactiveChatCandidates indicates an expected call of GetAutoArchiveInactiveChatCandidates.
func (mr *MockStoreMockRecorder) GetAutoArchiveInactiveChatCandidates(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAutoArchiveInactiveChatCandidates", reflect.TypeOf((*MockStore)(nil).GetAutoArchiveInactiveChatCandidates), ctx, arg)
}
// GetBoundaryLogByID mocks base method.
func (m *MockStore) GetBoundaryLogByID(ctx context.Context, id uuid.UUID) (database.BoundaryLog, error) {
m.ctrl.T.Helper()
@@ -2431,6 +2549,21 @@ func (mr *MockStoreMockRecorder) GetChatByID(ctx, id any) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChatByID", reflect.TypeOf((*MockStore)(nil).GetChatByID), ctx, id)
}
// GetChatByIDForShare mocks base method.
func (m *MockStore) GetChatByIDForShare(ctx context.Context, id uuid.UUID) (database.Chat, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetChatByIDForShare", ctx, id)
ret0, _ := ret[0].(database.Chat)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetChatByIDForShare indicates an expected call of GetChatByIDForShare.
func (mr *MockStoreMockRecorder) GetChatByIDForShare(ctx, id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChatByIDForShare", reflect.TypeOf((*MockStore)(nil).GetChatByIDForShare), ctx, id)
}
// GetChatByIDForUpdate mocks base method.
func (m *MockStore) GetChatByIDForUpdate(ctx context.Context, id uuid.UUID) (database.Chat, error) {
m.ctrl.T.Helper()
@@ -2671,6 +2804,21 @@ func (mr *MockStoreMockRecorder) GetChatExploreModelOverride(ctx any) *gomock.Ca
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChatExploreModelOverride", reflect.TypeOf((*MockStore)(nil).GetChatExploreModelOverride), ctx)
}
// GetChatFamilyIDsByRootID mocks base method.
func (m *MockStore) GetChatFamilyIDsByRootID(ctx context.Context, id uuid.UUID) ([]uuid.UUID, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetChatFamilyIDsByRootID", ctx, id)
ret0, _ := ret[0].([]uuid.UUID)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetChatFamilyIDsByRootID indicates an expected call of GetChatFamilyIDsByRootID.
func (mr *MockStoreMockRecorder) GetChatFamilyIDsByRootID(ctx, id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChatFamilyIDsByRootID", reflect.TypeOf((*MockStore)(nil).GetChatFamilyIDsByRootID), ctx, id)
}
// GetChatFileByID mocks base method.
func (m *MockStore) GetChatFileByID(ctx context.Context, id uuid.UUID) (database.ChatFile, error) {
m.ctrl.T.Helper()
@@ -2731,6 +2879,21 @@ func (mr *MockStoreMockRecorder) GetChatGeneralModelOverride(ctx any) *gomock.Ca
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChatGeneralModelOverride", reflect.TypeOf((*MockStore)(nil).GetChatGeneralModelOverride), ctx)
}
// GetChatHeartbeat mocks base method.
func (m *MockStore) GetChatHeartbeat(ctx context.Context, arg database.GetChatHeartbeatParams) (database.ChatHeartbeat, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetChatHeartbeat", ctx, arg)
ret0, _ := ret[0].(database.ChatHeartbeat)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetChatHeartbeat indicates an expected call of GetChatHeartbeat.
func (mr *MockStoreMockRecorder) GetChatHeartbeat(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChatHeartbeat", reflect.TypeOf((*MockStore)(nil).GetChatHeartbeat), ctx, arg)
}
// GetChatIncludeDefaultSystemPrompt mocks base method.
func (m *MockStore) GetChatIncludeDefaultSystemPrompt(ctx context.Context) (bool, error) {
m.ctrl.T.Helper()
@@ -2821,6 +2984,21 @@ func (mr *MockStoreMockRecorder) GetChatMessagesByChatIDDescPaginated(ctx, arg a
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChatMessagesByChatIDDescPaginated", reflect.TypeOf((*MockStore)(nil).GetChatMessagesByChatIDDescPaginated), ctx, arg)
}
// GetChatMessagesByRevisionForStream mocks base method.
func (m *MockStore) GetChatMessagesByRevisionForStream(ctx context.Context, arg database.GetChatMessagesByRevisionForStreamParams) ([]database.ChatMessage, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetChatMessagesByRevisionForStream", ctx, arg)
ret0, _ := ret[0].([]database.ChatMessage)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetChatMessagesByRevisionForStream indicates an expected call of GetChatMessagesByRevisionForStream.
func (mr *MockStoreMockRecorder) GetChatMessagesByRevisionForStream(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChatMessagesByRevisionForStream", reflect.TypeOf((*MockStore)(nil).GetChatMessagesByRevisionForStream), ctx, arg)
}
// GetChatMessagesForPromptByChatID mocks base method.
func (m *MockStore) GetChatMessagesForPromptByChatID(ctx context.Context, chatID uuid.UUID) ([]database.ChatMessage, error) {
m.ctrl.T.Helper()
@@ -2911,6 +3089,36 @@ func (mr *MockStoreMockRecorder) GetChatPlanModeInstructions(ctx any) *gomock.Ca
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChatPlanModeInstructions", reflect.TypeOf((*MockStore)(nil).GetChatPlanModeInstructions), ctx)
}
// GetChatQueuedMessageByID mocks base method.
func (m *MockStore) GetChatQueuedMessageByID(ctx context.Context, arg database.GetChatQueuedMessageByIDParams) (database.ChatQueuedMessage, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetChatQueuedMessageByID", ctx, arg)
ret0, _ := ret[0].(database.ChatQueuedMessage)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetChatQueuedMessageByID indicates an expected call of GetChatQueuedMessageByID.
func (mr *MockStoreMockRecorder) GetChatQueuedMessageByID(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChatQueuedMessageByID", reflect.TypeOf((*MockStore)(nil).GetChatQueuedMessageByID), ctx, arg)
}
// GetChatQueuedMessageHead mocks base method.
func (m *MockStore) GetChatQueuedMessageHead(ctx context.Context, chatID uuid.UUID) (database.ChatQueuedMessage, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetChatQueuedMessageHead", ctx, chatID)
ret0, _ := ret[0].(database.ChatQueuedMessage)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetChatQueuedMessageHead indicates an expected call of GetChatQueuedMessageHead.
func (mr *MockStoreMockRecorder) GetChatQueuedMessageHead(ctx, chatID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChatQueuedMessageHead", reflect.TypeOf((*MockStore)(nil).GetChatQueuedMessageHead), ctx, chatID)
}
// GetChatQueuedMessages mocks base method.
func (m *MockStore) GetChatQueuedMessages(ctx context.Context, chatID uuid.UUID) ([]database.ChatQueuedMessage, error) {
m.ctrl.T.Helper()
@@ -2926,6 +3134,21 @@ func (mr *MockStoreMockRecorder) GetChatQueuedMessages(ctx, chatID any) *gomock.
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChatQueuedMessages", reflect.TypeOf((*MockStore)(nil).GetChatQueuedMessages), ctx, chatID)
}
// GetChatQueuedMessagesByPosition mocks base method.
func (m *MockStore) GetChatQueuedMessagesByPosition(ctx context.Context, chatID uuid.UUID) ([]database.ChatQueuedMessage, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetChatQueuedMessagesByPosition", ctx, chatID)
ret0, _ := ret[0].([]database.ChatQueuedMessage)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetChatQueuedMessagesByPosition indicates an expected call of GetChatQueuedMessagesByPosition.
func (mr *MockStoreMockRecorder) GetChatQueuedMessagesByPosition(ctx, chatID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChatQueuedMessagesByPosition", reflect.TypeOf((*MockStore)(nil).GetChatQueuedMessagesByPosition), ctx, chatID)
}
// GetChatRetentionDays mocks base method.
func (m *MockStore) GetChatRetentionDays(ctx context.Context) (int32, error) {
m.ctrl.T.Helper()
@@ -2941,6 +3164,21 @@ func (mr *MockStoreMockRecorder) GetChatRetentionDays(ctx any) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChatRetentionDays", reflect.TypeOf((*MockStore)(nil).GetChatRetentionDays), ctx)
}
// GetChatStreamSyncRows mocks base method.
func (m *MockStore) GetChatStreamSyncRows(ctx context.Context, ids []uuid.UUID) ([]database.GetChatStreamSyncRowsRow, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetChatStreamSyncRows", ctx, ids)
ret0, _ := ret[0].([]database.GetChatStreamSyncRowsRow)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetChatStreamSyncRows indicates an expected call of GetChatStreamSyncRows.
func (mr *MockStoreMockRecorder) GetChatStreamSyncRows(ctx, ids any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChatStreamSyncRows", reflect.TypeOf((*MockStore)(nil).GetChatStreamSyncRows), ctx, ids)
}
// GetChatSystemPrompt mocks base method.
func (m *MockStore) GetChatSystemPrompt(ctx context.Context) (string, error) {
m.ctrl.T.Helper()
@@ -3061,6 +3299,21 @@ func (mr *MockStoreMockRecorder) GetChatUserPromptsByChatID(ctx, arg any) *gomoc
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChatUserPromptsByChatID", reflect.TypeOf((*MockStore)(nil).GetChatUserPromptsByChatID), ctx, arg)
}
// GetChatWorkerAcquisitionCandidates mocks base method.
func (m *MockStore) GetChatWorkerAcquisitionCandidates(ctx context.Context, arg database.GetChatWorkerAcquisitionCandidatesParams) ([]database.GetChatWorkerAcquisitionCandidatesRow, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetChatWorkerAcquisitionCandidates", ctx, arg)
ret0, _ := ret[0].([]database.GetChatWorkerAcquisitionCandidatesRow)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetChatWorkerAcquisitionCandidates indicates an expected call of GetChatWorkerAcquisitionCandidates.
func (mr *MockStoreMockRecorder) GetChatWorkerAcquisitionCandidates(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChatWorkerAcquisitionCandidates", reflect.TypeOf((*MockStore)(nil).GetChatWorkerAcquisitionCandidates), ctx, arg)
}
// GetChatWorkspaceTTL mocks base method.
func (m *MockStore) GetChatWorkspaceTTL(ctx context.Context) (string, error) {
m.ctrl.T.Helper()
@@ -3106,6 +3359,21 @@ func (mr *MockStoreMockRecorder) GetChatsByChatFileID(ctx, fileID any) *gomock.C
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChatsByChatFileID", reflect.TypeOf((*MockStore)(nil).GetChatsByChatFileID), ctx, fileID)
}
// GetChatsByIDsForRunnerSync mocks base method.
func (m *MockStore) GetChatsByIDsForRunnerSync(ctx context.Context, ids []uuid.UUID) ([]database.Chat, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetChatsByIDsForRunnerSync", ctx, ids)
ret0, _ := ret[0].([]database.Chat)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetChatsByIDsForRunnerSync indicates an expected call of GetChatsByIDsForRunnerSync.
func (mr *MockStoreMockRecorder) GetChatsByIDsForRunnerSync(ctx, ids any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChatsByIDsForRunnerSync", reflect.TypeOf((*MockStore)(nil).GetChatsByIDsForRunnerSync), ctx, ids)
}
// GetChatsByWorkspaceIDs mocks base method.
func (m *MockStore) GetChatsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]database.Chat, error) {
m.ctrl.T.Helper()
@@ -3241,6 +3509,21 @@ func (mr *MockStoreMockRecorder) GetDERPMeshKey(ctx any) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDERPMeshKey", reflect.TypeOf((*MockStore)(nil).GetDERPMeshKey), ctx)
}
// GetDatabaseNow mocks base method.
func (m *MockStore) GetDatabaseNow(ctx context.Context) (time.Time, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetDatabaseNow", ctx)
ret0, _ := ret[0].(time.Time)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetDatabaseNow indicates an expected call of GetDatabaseNow.
func (mr *MockStoreMockRecorder) GetDatabaseNow(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDatabaseNow", reflect.TypeOf((*MockStore)(nil).GetDatabaseNow), ctx)
}
// GetDefaultChatModelConfig mocks base method.
func (m *MockStore) GetDefaultChatModelConfig(ctx context.Context) (database.ChatModelConfig, error) {
m.ctrl.T.Helper()
@@ -6945,6 +7228,21 @@ func (mr *MockStoreMockRecorder) InTx(arg0, arg1 any) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InTx", reflect.TypeOf((*MockStore)(nil).InTx), arg0, arg1)
}
// IncrementChatGenerationAttempt mocks base method.
func (m *MockStore) IncrementChatGenerationAttempt(ctx context.Context, id uuid.UUID) (int64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IncrementChatGenerationAttempt", ctx, id)
ret0, _ := ret[0].(int64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// IncrementChatGenerationAttempt indicates an expected call of IncrementChatGenerationAttempt.
func (mr *MockStoreMockRecorder) IncrementChatGenerationAttempt(ctx, id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IncrementChatGenerationAttempt", reflect.TypeOf((*MockStore)(nil).IncrementChatGenerationAttempt), ctx, id)
}
// InsertAIBridgeInterception mocks base method.
func (m *MockStore) InsertAIBridgeInterception(ctx context.Context, arg database.InsertAIBridgeInterceptionParams) (database.AIBridgeInterception, error) {
m.ctrl.T.Helper()
@@ -7245,6 +7543,21 @@ func (mr *MockStoreMockRecorder) InsertChatQueuedMessage(ctx, arg any) *gomock.C
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertChatQueuedMessage", reflect.TypeOf((*MockStore)(nil).InsertChatQueuedMessage), ctx, arg)
}
// InsertChatQueuedMessageWithCreator mocks base method.
func (m *MockStore) InsertChatQueuedMessageWithCreator(ctx context.Context, arg database.InsertChatQueuedMessageWithCreatorParams) (database.ChatQueuedMessage, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "InsertChatQueuedMessageWithCreator", ctx, arg)
ret0, _ := ret[0].(database.ChatQueuedMessage)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// InsertChatQueuedMessageWithCreator indicates an expected call of InsertChatQueuedMessageWithCreator.
func (mr *MockStoreMockRecorder) InsertChatQueuedMessageWithCreator(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertChatQueuedMessageWithCreator", reflect.TypeOf((*MockStore)(nil).InsertChatQueuedMessageWithCreator), ctx, arg)
}
// InsertCryptoKey mocks base method.
func (m *MockStore) InsertCryptoKey(ctx context.Context, arg database.InsertCryptoKeyParams) (database.CryptoKey, error) {
m.ctrl.T.Helper()
@@ -8160,6 +8473,21 @@ func (mr *MockStoreMockRecorder) InsertWorkspaceResourceMetadata(ctx, arg any) *
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceResourceMetadata", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceResourceMetadata), ctx, arg)
}
// IsChatHeartbeatStale mocks base method.
func (m *MockStore) IsChatHeartbeatStale(ctx context.Context, arg database.IsChatHeartbeatStaleParams) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IsChatHeartbeatStale", ctx, arg)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// IsChatHeartbeatStale indicates an expected call of IsChatHeartbeatStale.
func (mr *MockStoreMockRecorder) IsChatHeartbeatStale(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsChatHeartbeatStale", reflect.TypeOf((*MockStore)(nil).IsChatHeartbeatStale), ctx, arg)
}
// LinkChatFiles mocks base method.
func (m *MockStore) LinkChatFiles(ctx context.Context, arg database.LinkChatFilesParams) (int32, error) {
m.ctrl.T.Helper()
@@ -8565,6 +8893,21 @@ func (mr *MockStoreMockRecorder) ListWorkspaceAgentPortShares(ctx, workspaceID a
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListWorkspaceAgentPortShares", reflect.TypeOf((*MockStore)(nil).ListWorkspaceAgentPortShares), ctx, workspaceID)
}
// LockChatAndBumpSnapshotVersion mocks base method.
func (m *MockStore) LockChatAndBumpSnapshotVersion(ctx context.Context, id uuid.UUID) (database.Chat, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LockChatAndBumpSnapshotVersion", ctx, id)
ret0, _ := ret[0].(database.Chat)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// LockChatAndBumpSnapshotVersion indicates an expected call of LockChatAndBumpSnapshotVersion.
func (mr *MockStoreMockRecorder) LockChatAndBumpSnapshotVersion(ctx, id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LockChatAndBumpSnapshotVersion", reflect.TypeOf((*MockStore)(nil).LockChatAndBumpSnapshotVersion), ctx, id)
}
// MarkAllInboxNotificationsAsRead mocks base method.
func (m *MockStore) MarkAllInboxNotificationsAsRead(ctx context.Context, arg database.MarkAllInboxNotificationsAsReadParams) error {
m.ctrl.T.Helper()
@@ -8757,6 +9100,21 @@ func (mr *MockStoreMockRecorder) ReorderChatQueuedMessageToFront(ctx, arg any) *
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReorderChatQueuedMessageToFront", reflect.TypeOf((*MockStore)(nil).ReorderChatQueuedMessageToFront), ctx, arg)
}
// ReorderChatQueuedMessageToHead mocks base method.
func (m *MockStore) ReorderChatQueuedMessageToHead(ctx context.Context, arg database.ReorderChatQueuedMessageToHeadParams) (int64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReorderChatQueuedMessageToHead", ctx, arg)
ret0, _ := ret[0].(int64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReorderChatQueuedMessageToHead indicates an expected call of ReorderChatQueuedMessageToHead.
func (mr *MockStoreMockRecorder) ReorderChatQueuedMessageToHead(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReorderChatQueuedMessageToHead", reflect.TypeOf((*MockStore)(nil).ReorderChatQueuedMessageToHead), ctx, arg)
}
// ResolveUserChatSpendLimit mocks base method.
func (m *MockStore) ResolveUserChatSpendLimit(ctx context.Context, arg database.ResolveUserChatSpendLimitParams) (database.ResolveUserChatSpendLimitRow, error) {
m.ctrl.T.Helper()
@@ -9103,6 +9461,21 @@ func (mr *MockStoreMockRecorder) UpdateChatDebugStep(ctx, arg any) *gomock.Call
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateChatDebugStep", reflect.TypeOf((*MockStore)(nil).UpdateChatDebugStep), ctx, arg)
}
// UpdateChatExecutionState mocks base method.
func (m *MockStore) UpdateChatExecutionState(ctx context.Context, arg database.UpdateChatExecutionStateParams) (database.Chat, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateChatExecutionState", ctx, arg)
ret0, _ := ret[0].(database.Chat)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateChatExecutionState indicates an expected call of UpdateChatExecutionState.
func (mr *MockStoreMockRecorder) UpdateChatExecutionState(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateChatExecutionState", reflect.TypeOf((*MockStore)(nil).UpdateChatExecutionState), ctx, arg)
}
// UpdateChatHeartbeats mocks base method.
func (m *MockStore) UpdateChatHeartbeats(ctx context.Context, arg database.UpdateChatHeartbeatsParams) ([]uuid.UUID, error) {
m.ctrl.T.Helper()
@@ -9266,6 +9639,21 @@ func (mr *MockStoreMockRecorder) UpdateChatPlanModeByID(ctx, arg any) *gomock.Ca
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateChatPlanModeByID", reflect.TypeOf((*MockStore)(nil).UpdateChatPlanModeByID), ctx, arg)
}
// UpdateChatRetryState mocks base method.
func (m *MockStore) UpdateChatRetryState(ctx context.Context, arg database.UpdateChatRetryStateParams) (database.Chat, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateChatRetryState", ctx, arg)
ret0, _ := ret[0].(database.Chat)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateChatRetryState indicates an expected call of UpdateChatRetryState.
func (mr *MockStoreMockRecorder) UpdateChatRetryState(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateChatRetryState", reflect.TypeOf((*MockStore)(nil).UpdateChatRetryState), ctx, arg)
}
// UpdateChatStatus mocks base method.
func (m *MockStore) UpdateChatStatus(ctx context.Context, arg database.UpdateChatStatusParams) (database.Chat, error) {
m.ctrl.T.Helper()
@@ -10990,6 +11378,20 @@ func (mr *MockStoreMockRecorder) UpsertChatGeneralModelOverride(ctx, value any)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertChatGeneralModelOverride", reflect.TypeOf((*MockStore)(nil).UpsertChatGeneralModelOverride), ctx, value)
}
// UpsertChatHeartbeat mocks base method.
func (m *MockStore) UpsertChatHeartbeat(ctx context.Context, arg database.UpsertChatHeartbeatParams) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpsertChatHeartbeat", ctx, arg)
ret0, _ := ret[0].(error)
return ret0
}
// UpsertChatHeartbeat indicates an expected call of UpsertChatHeartbeat.
func (mr *MockStoreMockRecorder) UpsertChatHeartbeat(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertChatHeartbeat", reflect.TypeOf((*MockStore)(nil).UpsertChatHeartbeat), ctx, arg)
}
// UpsertChatIncludeDefaultSystemPrompt mocks base method.
func (m *MockStore) UpsertChatIncludeDefaultSystemPrompt(ctx context.Context, includeDefaultSystemPrompt bool) error {
m.ctrl.T.Helper()
+30 -315
View File
@@ -1,18 +1,12 @@
package dbpurge
import (
"cmp"
"context"
"errors"
"io"
"net/http"
"slices"
"strconv"
"sync/atomic"
"time"
"github.com/dustin/go-humanize"
"github.com/google/uuid"
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/xerrors"
@@ -21,9 +15,7 @@ import (
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/notifications"
"github.com/coder/coder/v2/coderd/pproflabel"
"github.com/coder/coder/v2/coderd/util/slice"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/quartz"
)
@@ -52,18 +44,8 @@ const (
// Chat debug run deletions can cascade into steps with large JSONB
// payloads, so they use the same conservative batch size.
chatDebugRunsBatchSize = 1000
// chatAutoArchiveDigestMaxChats bounds how many chat titles a
// single digest body lists. Past the cap, surplus titles are
// summarized as "...and N more". 25 is a readable email-friendly
// length; the cap is unrelated to chatAutoArchiveBatchSize, which
// bounds work per tick.
chatAutoArchiveDigestMaxChats = 25
)
// defaultChatAutoArchiveBatchSize bounds how many root chats one
// tick will archive by default.
const defaultChatAutoArchiveBatchSize int32 = 1000
type Option func(*instance)
// WithClock overrides the clock used by the purger. Defaults to
@@ -72,34 +54,12 @@ func WithClock(clk quartz.Clock) Option {
return func(i *instance) { i.clk = clk }
}
// WithChatAutoArchiveBatchSize overrides how many root chats a
// single tick will auto-archive. Defaults to
// defaultChatAutoArchiveBatchSize (1000).
func WithChatAutoArchiveBatchSize(n int32) Option {
return func(i *instance) { i.chatAutoArchiveBatchSize = n }
}
// WithNotificationsEnqueuer sets the enqueuer used for digest
// notifications. Defaults to notifications.NewNoopEnqueuer(). Panics
// if e is nil: a nil enqueuer would NPE on the first dispatch tick,
// and failing fast at option-apply time surfaces the misuse at
// startup rather than minutes later.
func WithNotificationsEnqueuer(e notifications.Enqueuer) Option {
if e == nil {
panic("developer error: WithNotificationsEnqueuer called with nil enqueuer")
}
return func(i *instance) { i.enqueuer = e }
}
// New creates a new periodically purging database instance.
// Callers must Close the returned instance.
//
// The auditor pointer is loaded on each dispatch tick so runtime
// entitlement changes (e.g. toggling the audit-log feature) take
// effect without restarting the process. Notifications enqueuer
// defaults to no-op. Use WithNotificationsEnqueuer to pass a real
// one.
func New(ctx context.Context, logger slog.Logger, db database.Store, vals *codersdk.DeploymentValues, reg prometheus.Registerer, auditor *atomic.Pointer[audit.Auditor], opts ...Option) io.Closer {
// The auditor pointer is accepted for compatibility with other background
// services. Dbpurge does not emit audit logs directly.
func New(ctx context.Context, logger slog.Logger, db database.Store, vals *codersdk.DeploymentValues, reg prometheus.Registerer, _ *atomic.Pointer[audit.Auditor], opts ...Option) io.Closer {
closed := make(chan struct{})
ctx, cancelFunc := context.WithCancel(ctx)
@@ -123,26 +83,14 @@ func New(ctx context.Context, logger slog.Logger, db database.Store, vals *coder
}, []string{"record_type"})
reg.MustRegister(recordsPurged)
chatAutoArchiveRecords := prometheus.NewCounter(prometheus.CounterOpts{
Namespace: "coderd",
Subsystem: "chat_auto_archive",
Name: "records_archived_total",
Help: "Total number of chats archived by the auto-archive job (counting both roots and cascaded children).",
})
reg.MustRegister(chatAutoArchiveRecords)
inst := &instance{
cancel: cancelFunc,
closed: closed,
logger: logger,
vals: vals,
clk: quartz.NewReal(),
auditor: auditor,
enqueuer: notifications.NewNoopEnqueuer(),
iterationDuration: iterationDuration,
recordsPurged: recordsPurged,
chatAutoArchiveRecords: chatAutoArchiveRecords,
chatAutoArchiveBatchSize: defaultChatAutoArchiveBatchSize,
cancel: cancelFunc,
closed: closed,
logger: logger,
vals: vals,
clk: quartz.NewReal(),
iterationDuration: iterationDuration,
recordsPurged: recordsPurged,
}
for _, opt := range opts {
opt(inst)
@@ -185,19 +133,14 @@ func New(ctx context.Context, logger slog.Logger, db database.Store, vals *coder
func (i *instance) purgeTick(ctx context.Context, db database.Store, start time.Time) error {
// Read chat configs outside the tx so a corrupt value can't
// poison subsequent queries. On config read errors, log and stash
// the error, then run unrelated purges best-effort. Retention and
// auto-archive errors skip only the conversation purge and
// auto-archive work. Debug retention errors skip only the debug
// purge. purgeTick returns chatConfigErr after the tx so the failed
// iteration is operator-visible via metric and logs.
// the error, then run unrelated purges best-effort. Retention
// errors skip only the conversation purge. Debug retention errors
// skip only the debug purge. purgeTick returns chatConfigErr after
// the tx so the failed iteration is operator-visible via metric and
// logs.
chatRetentionDays, chatRetentionErr := db.GetChatRetentionDays(ctx)
if chatRetentionErr != nil {
i.logger.Error(ctx, "failed to read chat retention config: skipping chat purge and auto-archive this tick", slog.Error(chatRetentionErr))
}
chatAutoArchiveDays, chatAutoArchiveErr := db.GetChatAutoArchiveDays(ctx, codersdk.DefaultChatAutoArchiveDays)
if chatAutoArchiveErr != nil {
i.logger.Error(ctx, "failed to read chat auto-archive config: skipping chat purge and auto-archive this tick", slog.Error(chatAutoArchiveErr))
i.logger.Error(ctx, "failed to read chat retention config: skipping chat purge this tick", slog.Error(chatRetentionErr))
}
chatDebugRetentionDays, chatDebugRetentionErr := db.GetChatDebugRetentionDays(ctx, codersdk.DefaultChatDebugRetentionDays)
@@ -205,11 +148,7 @@ func (i *instance) purgeTick(ctx context.Context, db database.Store, start time.
i.logger.Error(ctx, "failed to read chat debug retention config: skipping chat debug purge this tick", slog.Error(chatDebugRetentionErr))
}
chatRetentionConfigErr := errors.Join(chatRetentionErr, chatAutoArchiveErr)
chatConfigErr := errors.Join(chatRetentionConfigErr, chatDebugRetentionErr)
// Populated inside the tx; dispatched post-commit.
var archivedChats []database.AutoArchiveInactiveChatsRow
chatConfigErr := errors.Join(chatRetentionErr, chatDebugRetentionErr)
// Start a transaction to grab advisory lock, we don't want to run
// multiple purges at the same time (multiple replicas).
@@ -316,8 +255,8 @@ func (i *instance) purgeTick(ctx context.Context, db database.Store, start time.
}
var purgedChats, purgedChatFiles, purgedChatDebugRuns int64
if chatRetentionConfigErr == nil {
purgedChats, purgedChatFiles, archivedChats, err = i.purgeChatsInTx(ctx, tx, start, chatRetentionDays, chatAutoArchiveDays)
if chatRetentionErr == nil {
purgedChats, purgedChatFiles, err = i.purgeChatsInTx(ctx, tx, start, chatRetentionDays)
if err != nil {
return xerrors.Errorf("failed to purge chats: %w", err)
}
@@ -345,7 +284,6 @@ func (i *instance) purgeTick(ctx context.Context, db database.Store, start time.
slog.F("chats", purgedChats),
slog.F("chat_files", purgedChatFiles),
slog.F("chat_debug_runs", purgedChatDebugRuns),
slog.F("auto_archived_chats", len(archivedChats)),
slog.F("duration", i.clk.Since(start)),
)
@@ -379,35 +317,17 @@ func (i *instance) purgeTick(ctx context.Context, db database.Store, start time.
return xerrors.Errorf("chat config read failed this tick: %w", chatConfigErr)
}
// Dispatch audits and digests post-commit. Detached context for audit
// so that ticker cancellation cannot truncate the audit trail.
// Notification enqueue uses the cancellable parent context to avoid
// stalling shutdown.
// Owners with more eligible chats than batch size will get a
// notification per tick until their backlog drains.
// If this is deemed too noisy, users can disable the
// "Chats Auto-Archived" template from their notification preferences.
if len(archivedChats) > 0 {
i.chatAutoArchiveRecords.Add(float64(len(archivedChats)))
auditCtx := context.WithoutCancel(ctx)
i.dispatchChatAutoArchive(auditCtx, ctx, start, chatAutoArchiveDays, chatRetentionDays, archivedChats)
}
return nil
}
type instance struct {
cancel context.CancelFunc
closed chan struct{}
logger slog.Logger
vals *codersdk.DeploymentValues
clk quartz.Clock
auditor *atomic.Pointer[audit.Auditor]
enqueuer notifications.Enqueuer
iterationDuration *prometheus.HistogramVec
recordsPurged *prometheus.CounterVec
chatAutoArchiveRecords prometheus.Counter
chatAutoArchiveBatchSize int32
cancel context.CancelFunc
closed chan struct{}
logger slog.Logger
vals *codersdk.DeploymentValues
clk quartz.Clock
iterationDuration *prometheus.HistogramVec
recordsPurged *prometheus.CounterVec
}
func (i *instance) Close() error {
@@ -416,73 +336,8 @@ func (i *instance) Close() error {
return nil
}
// chatFromAutoArchiveRow reshapes the query row into a database.Chat for
// audit.Auditable[database.Chat].
func chatFromAutoArchiveRow(logger slog.Logger, r database.AutoArchiveInactiveChatsRow) database.Chat {
var labels database.StringMap
// sqlc's StringMap override doesn't reach CTE-aliased columns, so Labels
// arrives as raw JSON bytes. StringMap.Scan handles []byte and nil.
if err := labels.Scan([]byte(r.Labels)); err != nil {
logger.Warn(context.Background(), "failed to parse chat labels from auto-archive row",
slog.F("chat_id", r.ID),
slog.F("raw_labels", string(r.Labels)),
slog.Error(err),
)
}
var userACL database.ChatACL
if err := userACL.Scan([]byte(r.UserACL)); err != nil {
logger.Warn(context.Background(), "failed to parse chat user ACL from auto-archive row",
slog.F("chat_id", r.ID),
slog.F("raw_user_acl", string(r.UserACL)),
slog.Error(err),
)
}
var groupACL database.ChatACL
if err := groupACL.Scan([]byte(r.GroupACL)); err != nil {
logger.Warn(context.Background(), "failed to parse chat group ACL from auto-archive row",
slog.F("chat_id", r.ID),
slog.F("raw_group_acl", string(r.GroupACL)),
slog.Error(err),
)
}
return database.Chat{
ID: r.ID,
OwnerID: r.OwnerID,
OrganizationID: r.OrganizationID,
WorkspaceID: r.WorkspaceID,
BuildID: r.BuildID,
AgentID: r.AgentID,
Title: r.Title,
Status: r.Status,
WorkerID: r.WorkerID,
StartedAt: r.StartedAt,
HeartbeatAt: r.HeartbeatAt,
CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt,
ParentChatID: r.ParentChatID,
RootChatID: r.RootChatID,
LastModelConfigID: r.LastModelConfigID,
Archived: r.Archived,
LastError: r.LastError,
Mode: r.Mode,
MCPServerIDs: r.MCPServerIDs,
Labels: labels,
UserACL: userACL,
GroupACL: groupACL,
PinOrder: r.PinOrder,
LastReadMessageID: r.LastReadMessageID,
LastInjectedContext: r.LastInjectedContext,
DynamicTools: r.DynamicTools,
PlanMode: r.PlanMode,
ClientType: r.ClientType,
}
}
// purgeChatsInTx MUST BE CALLED WITH A TRANSACTION
func (i *instance) purgeChatsInTx(ctx context.Context, tx database.Store, start time.Time, chatRetentionDays, chatAutoArchiveDays int32) (purgedChats, purgedChatFiles int64, archivedChats []database.AutoArchiveInactiveChatsRow, err error) {
func (*instance) purgeChatsInTx(ctx context.Context, tx database.Store, start time.Time, chatRetentionDays int32) (purgedChats, purgedChatFiles int64, err error) {
// Delete old archived chats first, then orphaned files
// (cascade clears chat_file_links but not chat_files).
if chatRetentionDays > 0 {
@@ -492,7 +347,7 @@ func (i *instance) purgeChatsInTx(ctx context.Context, tx database.Store, start
LimitCount: chatsBatchSize,
})
if err != nil {
return 0, 0, nil, xerrors.Errorf("failed to delete old chats: %w", err)
return 0, 0, xerrors.Errorf("failed to delete old chats: %w", err)
}
purgedChatFiles, err = tx.DeleteOldChatFiles(ctx, database.DeleteOldChatFilesParams{
@@ -500,149 +355,9 @@ func (i *instance) purgeChatsInTx(ctx context.Context, tx database.Store, start
LimitCount: chatFilesBatchSize,
})
if err != nil {
return 0, 0, nil, xerrors.Errorf("failed to delete old chat files: %w", err)
return 0, 0, xerrors.Errorf("failed to delete old chat files: %w", err)
}
}
// Auto-archive runs after the delete pass so newly
// archived chats aren't eligible for deletion this tick.
// Eligibility uses UTC day boundaries: a chat is archived on the
// start of the UTC day after its inactivity period has elapsed.
if chatAutoArchiveDays > 0 {
today := dbtime.StartOfDay(start)
archiveCutoff := today.Add(-time.Duration(chatAutoArchiveDays) * 24 * time.Hour)
archivedChats, err = tx.AutoArchiveInactiveChats(ctx, database.AutoArchiveInactiveChatsParams{
ArchiveCutoff: archiveCutoff,
LimitCount: i.chatAutoArchiveBatchSize,
})
if err != nil {
return 0, 0, nil, xerrors.Errorf("failed to auto-archive inactive chats: %w", err)
}
}
return purgedChats, purgedChatFiles, archivedChats, nil
}
// dispatchChatAutoArchive audits every archived root chat and enqueues one
// notification per owner covering the roots archived in this tick. Children
// inherit their root's archival decision and are skipped for audit, matching
// the manual archive path (patchChat audits the root only). Enqueue is
// per-tick: owners whose backlog spans multiple ticks receive multiple
// notifications; notification_messages dedupe does not collapse them because
// each tick's payload differs.
//
// auditCtx is detached from the ticker so audits always complete. enqueueCtx
// is the cancellable parent: on shutdown we abandon any remaining digests
// rather than blocking Close.
func (i *instance) dispatchChatAutoArchive(auditCtx, enqueueCtx context.Context, tickStart time.Time, autoArchiveDays, retentionDays int32, archived []database.AutoArchiveInactiveChatsRow) {
// Children inherit their root's archival decision and are skipped
// for both audit and digest. Partition once so the two loops
// cannot drift apart if the cascade shape ever changes.
roots := slice.Filter(archived, func(r database.AutoArchiveInactiveChatsRow) bool {
return !r.ParentChatID.Valid
})
auditor := *i.auditor.Load()
for _, row := range roots {
after := chatFromAutoArchiveRow(i.logger, row)
before := after
before.Archived = false
audit.BackgroundAudit(auditCtx, &audit.BackgroundAuditParams[database.Chat]{
Audit: auditor,
Log: i.logger,
UserID: row.OwnerID,
OrganizationID: row.OrganizationID,
Action: database.AuditActionWrite,
Old: before,
New: after,
Status: http.StatusOK,
AdditionalFields: audit.BackgroundTaskFieldsBytes(auditCtx, i.logger, audit.BackgroundSubsystemChatAutoArchive),
})
}
// Group archived roots by owner. Inline because this is the
// only call site and the loop body is self-explanatory.
rootsByOwner := make(map[uuid.UUID][]database.AutoArchiveInactiveChatsRow, len(roots))
for _, row := range roots {
rootsByOwner[row.OwnerID] = append(rootsByOwner[row.OwnerID], row)
}
// Sort owner IDs so shutdown abandons a deterministic tail of the dispatch list.
ownerIDs := make([]uuid.UUID, 0, len(rootsByOwner))
for id := range rootsByOwner {
ownerIDs = append(ownerIDs, id)
}
slices.SortFunc(ownerIDs, func(a, b uuid.UUID) int {
return cmp.Compare(a.String(), b.String())
})
dispatched := 0
for _, ownerID := range ownerIDs {
// Check between iterations so shutdown unblocks promptly. A
// hung in-flight enqueue is unblocked by enqueueCtx propagating
// cancellation into the DB call. Skipped owners are not
// re-notified on the next tick because AutoArchiveInactiveChats
// only returns rows with archived = false; we accept that
// tradeoff over hanging shutdown.
if err := enqueueCtx.Err(); err != nil {
i.logger.Warn(enqueueCtx, "chat auto-archive digest dispatch canceled",
slog.F("remaining_owners", len(ownerIDs)-dispatched),
slog.Error(err))
return
}
dispatched++
ownerRoots := rootsByOwner[ownerID]
data := buildDigestData(ownerRoots, autoArchiveDays, retentionDays, tickStart)
// nolint:gocritic // Background digest runs as the notifier subject.
if _, err := i.enqueuer.EnqueueWithData(
dbauthz.AsNotifier(enqueueCtx),
ownerID,
notifications.TemplateChatAutoArchiveDigest,
map[string]string{},
data,
string(audit.BackgroundSubsystemChatAutoArchive),
); err != nil {
i.logger.Warn(enqueueCtx, "failed to enqueue chat auto-archive digest",
slog.F("owner_id", ownerID),
slog.Error(err))
}
}
}
// buildDigestData builds the notification payload; shape mirrors the
// golden fixtures in coderd/notifications/testdata. Truncation keeps
// the oldest archived roots (created_at ASC from the query) to
// preserve index-driven ordering; revisit if the digest becomes the
// primary surface for reviewing archived chats.
func buildDigestData(rows []database.AutoArchiveInactiveChatsRow, autoArchiveDays, retentionDays int32, tickStart time.Time) map[string]any {
// Cap titles; overflow surfaces as "...and N more" via the template.
overflow := 0
if len(rows) > chatAutoArchiveDigestMaxChats {
overflow = len(rows) - chatAutoArchiveDigestMaxChats
rows = rows[:chatAutoArchiveDigestMaxChats]
}
chats := make([]map[string]any, 0, len(rows))
for _, r := range rows {
chats = append(chats, map[string]any{
"title": r.Title,
"last_activity_humanized": humanize.RelTime(r.LastActivityAt, tickStart, "ago", "from now"),
})
}
// Stringify the int32 config values: the template's
// {{if eq .Data.retention_days "0"}} branch requires both
// operands to share a type, and Go templates do not coerce
// numeric ↔ string. Storing a raw int here would silently
// take the deletion-warning branch on every notification.
data := map[string]any{
"auto_archive_days": strconv.Itoa(int(autoArchiveDays)),
"retention_days": strconv.Itoa(int(retentionDays)),
"archived_chats": chats,
}
if overflow > 0 {
data["additional_archived_count"] = strconv.Itoa(overflow)
}
return data
return purgedChats, purgedChatFiles, nil
}
File diff suppressed because it is too large Load Diff
+190 -3
View File
@@ -341,7 +341,8 @@ CREATE TYPE chat_status AS ENUM (
'paused',
'completed',
'error',
'requires_action'
'requires_action',
'interrupting'
);
CREATE TYPE connection_status AS ENUM (
@@ -716,6 +717,29 @@ BEGIN
END;
$$;
CREATE FUNCTION bump_chat_queue_version_on_queued_message_change() RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
changed_chat_id uuid;
BEGIN
IF TG_OP = 'DELETE' THEN
changed_chat_id = OLD.chat_id;
ELSE
changed_chat_id = NEW.chat_id;
END IF;
UPDATE chats
SET queue_version = snapshot_version
WHERE id = changed_chat_id;
IF TG_OP = 'DELETE' THEN
RETURN OLD;
END IF;
RETURN NEW;
END;
$$;
CREATE FUNCTION check_workspace_agent_name_unique() RETURNS trigger
LANGUAGE plpgsql
AS $$
@@ -1293,6 +1317,103 @@ BEGIN
END;
$$;
CREATE FUNCTION set_chat_message_revision_before() RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
chat_snapshot_version bigint;
BEGIN
IF TG_OP = 'INSERT' AND NEW.revision IS NOT NULL THEN
RAISE EXCEPTION 'chat_messages.revision must be assigned by trigger';
END IF;
IF TG_OP = 'UPDATE' THEN
IF OLD.chat_id IS DISTINCT FROM NEW.chat_id THEN
RAISE EXCEPTION 'chat_messages.chat_id is immutable';
END IF;
IF OLD.revision IS DISTINCT FROM NEW.revision THEN
RAISE EXCEPTION 'chat_messages.revision must be assigned by trigger';
END IF;
IF OLD IS NOT DISTINCT FROM NEW THEN
RETURN NEW;
END IF;
END IF;
SELECT snapshot_version INTO chat_snapshot_version
FROM chats WHERE id = NEW.chat_id;
IF chat_snapshot_version IS NULL THEN
RAISE EXCEPTION 'chat % does not exist', NEW.chat_id;
END IF;
NEW.revision = chat_snapshot_version;
RETURN NEW;
END;
$$;
CREATE FUNCTION sync_chat_retry_state() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF OLD.retry_state_version IS DISTINCT FROM NEW.retry_state_version THEN
RAISE EXCEPTION 'chats.retry_state_version must be assigned by trigger';
END IF;
IF NEW.generation_attempt IS DISTINCT FROM OLD.generation_attempt THEN
NEW.retry_state = NULL;
END IF;
IF NEW.retry_state IS DISTINCT FROM OLD.retry_state THEN
NEW.retry_state_version = NEW.snapshot_version;
END IF;
RETURN NEW;
END;
$$;
CREATE FUNCTION update_chat_history_after_message_insert() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
UPDATE chats c
SET history_version = c.snapshot_version,
generation_attempt = 0
FROM (
SELECT DISTINCT chat_id FROM chat_message_history_new_rows
) AS affected
WHERE c.id = affected.chat_id
AND (
c.history_version IS DISTINCT FROM c.snapshot_version
OR c.generation_attempt <> 0
);
RETURN NULL;
END;
$$;
CREATE FUNCTION update_chat_history_after_message_update() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
UPDATE chats c
SET history_version = c.snapshot_version,
generation_attempt = 0
FROM (
SELECT DISTINCT n.chat_id
FROM chat_message_history_new_rows n
JOIN chat_message_history_old_rows o ON o.id = n.id
WHERE o IS DISTINCT FROM n
) AS affected
WHERE c.id = affected.chat_id
AND (
c.history_version IS DISTINCT FROM c.snapshot_version
OR c.generation_attempt <> 0
);
RETURN NULL;
END;
$$;
CREATE TABLE ai_gateway_keys (
id uuid NOT NULL,
created_at timestamp with time zone NOT NULL,
@@ -1670,6 +1791,14 @@ CREATE TABLE chat_files (
data bytea NOT NULL
);
CREATE UNLOGGED TABLE chat_heartbeats (
chat_id uuid NOT NULL,
runner_id uuid NOT NULL,
heartbeat_at timestamp with time zone NOT NULL
);
COMMENT ON TABLE chat_heartbeats IS 'Ephemeral runner ownership leases for runnable chats. The table is unlogged because losing heartbeat rows after a crash is safe: missing heartbeats are treated as stale ownership and cause workers to reacquire runnable chats.';
CREATE TABLE chat_messages (
id bigint NOT NULL,
chat_id uuid NOT NULL,
@@ -1692,7 +1821,8 @@ CREATE TABLE chat_messages (
runtime_ms bigint,
deleted boolean DEFAULT false NOT NULL,
provider_response_id text,
api_key_id text
api_key_id text,
revision bigint NOT NULL
);
CREATE SEQUENCE chat_messages_id_seq
@@ -1726,13 +1856,22 @@ CREATE TABLE chat_model_configs (
CONSTRAINT chat_model_configs_context_limit_check CHECK ((context_limit > 0))
);
CREATE SEQUENCE chat_queued_messages_position_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
CREATE TABLE chat_queued_messages (
id bigint NOT NULL,
chat_id uuid NOT NULL,
content jsonb NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL,
model_config_id uuid,
api_key_id text
api_key_id text,
"position" bigint DEFAULT nextval('chat_queued_messages_position_seq'::regclass) NOT NULL,
created_by uuid NOT NULL
);
CREATE SEQUENCE chat_queued_messages_id_seq
@@ -1797,6 +1936,14 @@ CREATE TABLE chats (
last_turn_summary text,
user_acl jsonb DEFAULT '{}'::jsonb NOT NULL,
group_acl jsonb DEFAULT '{}'::jsonb NOT NULL,
snapshot_version bigint DEFAULT 1 NOT NULL,
history_version bigint DEFAULT 0 NOT NULL,
queue_version bigint DEFAULT 0 NOT NULL,
generation_attempt bigint DEFAULT 0 NOT NULL,
retry_state jsonb,
retry_state_version bigint DEFAULT 0 NOT NULL,
runner_id uuid,
requires_action_deadline_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))),
@@ -1804,6 +1951,12 @@ CREATE TABLE chats (
CONSTRAINT chats_pin_order_parent_check CHECK (((pin_order = 0) OR (parent_chat_id IS NULL)))
);
COMMENT ON COLUMN chats.snapshot_version IS 'Monotonic version for the full chat snapshot. Starts at 1 so stream loops and workers can use 0 to mean they have not loaded the chat yet.';
COMMENT ON COLUMN chats.history_version IS 'Snapshot version of the latest durable history change. Starts at 0 until chat_messages triggers set it to the current snapshot_version.';
COMMENT ON COLUMN chats.queue_version IS 'Snapshot version of the latest queued-message change. Starts at 0 until chat_queued_messages triggers set it to the current snapshot_version.';
CREATE TABLE users (
id uuid NOT NULL,
email text NOT NULL,
@@ -1884,6 +2037,14 @@ CREATE VIEW chats_expanded AS
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,
@@ -3848,6 +4009,9 @@ ALTER TABLE ONLY chat_file_links
ALTER TABLE ONLY chat_files
ADD CONSTRAINT chat_files_pkey PRIMARY KEY (id);
ALTER TABLE ONLY chat_heartbeats
ADD CONSTRAINT chat_heartbeats_pkey PRIMARY KEY (chat_id, runner_id);
ALTER TABLE ONLY chat_messages
ADD CONSTRAINT chat_messages_pkey PRIMARY KEY (id);
@@ -4190,6 +4354,8 @@ CREATE INDEX api_keys_last_used_idx ON api_keys USING btree (last_used DESC);
COMMENT ON INDEX api_keys_last_used_idx IS 'Index for optimizing api_keys queries filtering by last_used';
CREATE INDEX chat_heartbeats_heartbeat_at_idx ON chat_heartbeats USING btree (heartbeat_at);
CREATE INDEX idx_agent_stats_created_at ON workspace_agent_stats USING btree (created_at);
CREATE INDEX idx_agent_stats_user_id ON workspace_agent_stats USING btree (user_id);
@@ -4320,6 +4486,8 @@ CREATE INDEX idx_chats_pending ON chats USING btree (status) WHERE (status = 'pe
CREATE INDEX idx_chats_root_chat_id ON chats USING btree (root_chat_id);
CREATE INDEX idx_chats_worker_acquisition_candidates ON chats USING btree (status, updated_at, id) WHERE (archived = false);
CREATE INDEX idx_chats_workspace ON chats USING btree (workspace_id);
CREATE INDEX idx_connection_logs_connect_time_desc ON connection_logs USING btree (connect_time DESC);
@@ -4550,6 +4718,12 @@ COMMENT ON TRIGGER remove_organization_member_custom_role ON custom_roles IS 'Wh
CREATE TRIGGER trigger_aggregate_usage_event AFTER INSERT ON usage_events FOR EACH ROW EXECUTE FUNCTION aggregate_usage_event();
CREATE TRIGGER trigger_bump_chat_queue_version_on_queued_message_delete AFTER DELETE ON chat_queued_messages FOR EACH ROW EXECUTE FUNCTION bump_chat_queue_version_on_queued_message_change();
CREATE TRIGGER trigger_bump_chat_queue_version_on_queued_message_insert AFTER INSERT ON chat_queued_messages FOR EACH ROW EXECUTE FUNCTION bump_chat_queue_version_on_queued_message_change();
CREATE TRIGGER trigger_bump_chat_queue_version_on_queued_message_update AFTER UPDATE OF content, model_config_id, "position", created_by ON chat_queued_messages FOR EACH ROW EXECUTE FUNCTION bump_chat_queue_version_on_queued_message_change();
CREATE TRIGGER trigger_delete_group_members_on_org_member_delete BEFORE DELETE ON organization_members FOR EACH ROW EXECUTE FUNCTION delete_group_members_on_org_member_delete();
CREATE TRIGGER trigger_delete_oauth2_provider_app_token AFTER DELETE ON oauth2_provider_app_tokens FOR EACH ROW EXECUTE FUNCTION delete_deleted_oauth2_provider_app_token_api_key();
@@ -4566,6 +4740,16 @@ CREATE TRIGGER trigger_insert_organization_system_roles AFTER INSERT ON organiza
CREATE TRIGGER trigger_nullify_next_start_at_on_workspace_autostart_modificati AFTER UPDATE ON workspaces FOR EACH ROW EXECUTE FUNCTION nullify_next_start_at_on_workspace_autostart_modification();
CREATE TRIGGER trigger_set_chat_message_revision_on_insert BEFORE INSERT ON chat_messages FOR EACH ROW EXECUTE FUNCTION set_chat_message_revision_before();
CREATE TRIGGER trigger_set_chat_message_revision_on_update BEFORE UPDATE ON chat_messages FOR EACH ROW EXECUTE FUNCTION set_chat_message_revision_before();
CREATE TRIGGER trigger_sync_chat_retry_state BEFORE UPDATE OF retry_state, retry_state_version, generation_attempt ON chats FOR EACH ROW EXECUTE FUNCTION sync_chat_retry_state();
CREATE TRIGGER trigger_update_chat_history_after_message_insert AFTER INSERT ON chat_messages REFERENCING NEW TABLE AS chat_message_history_new_rows FOR EACH STATEMENT EXECUTE FUNCTION update_chat_history_after_message_insert();
CREATE TRIGGER trigger_update_chat_history_after_message_update AFTER UPDATE ON chat_messages REFERENCING OLD TABLE AS chat_message_history_old_rows NEW TABLE AS chat_message_history_new_rows FOR EACH STATEMENT EXECUTE FUNCTION update_chat_history_after_message_update();
CREATE TRIGGER trigger_update_users AFTER INSERT OR UPDATE ON users FOR EACH ROW WHEN ((new.deleted = true)) EXECUTE FUNCTION delete_deleted_user_resources();
CREATE TRIGGER trigger_upsert_user_links BEFORE INSERT OR UPDATE ON user_links FOR EACH ROW EXECUTE FUNCTION insert_user_links_fail_if_user_deleted();
@@ -4636,6 +4820,9 @@ ALTER TABLE ONLY chat_files
ALTER TABLE ONLY chat_files
ADD CONSTRAINT chat_files_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE;
ALTER TABLE ONLY chat_heartbeats
ADD CONSTRAINT chat_heartbeats_chat_id_fkey FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE;
ALTER TABLE ONLY chat_messages
ADD CONSTRAINT chat_messages_api_key_id_fkey FOREIGN KEY (api_key_id) REFERENCES api_keys(id) ON DELETE SET NULL;
+1
View File
@@ -22,6 +22,7 @@ const (
ForeignKeyChatFileLinksFileID ForeignKeyConstraint = "chat_file_links_file_id_fkey" // ALTER TABLE ONLY chat_file_links ADD CONSTRAINT chat_file_links_file_id_fkey FOREIGN KEY (file_id) REFERENCES chat_files(id) ON DELETE CASCADE;
ForeignKeyChatFilesOrganizationID ForeignKeyConstraint = "chat_files_organization_id_fkey" // ALTER TABLE ONLY chat_files ADD CONSTRAINT chat_files_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE;
ForeignKeyChatFilesOwnerID ForeignKeyConstraint = "chat_files_owner_id_fkey" // ALTER TABLE ONLY chat_files ADD CONSTRAINT chat_files_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE;
ForeignKeyChatHeartbeatsChatID ForeignKeyConstraint = "chat_heartbeats_chat_id_fkey" // ALTER TABLE ONLY chat_heartbeats ADD CONSTRAINT chat_heartbeats_chat_id_fkey FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE;
ForeignKeyChatMessagesAPIKeyID ForeignKeyConstraint = "chat_messages_api_key_id_fkey" // ALTER TABLE ONLY chat_messages ADD CONSTRAINT chat_messages_api_key_id_fkey FOREIGN KEY (api_key_id) REFERENCES api_keys(id) ON DELETE SET NULL;
ForeignKeyChatMessagesChatID ForeignKeyConstraint = "chat_messages_chat_id_fkey" // ALTER TABLE ONLY chat_messages ADD CONSTRAINT chat_messages_chat_id_fkey FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE;
ForeignKeyChatMessagesModelConfigID ForeignKeyConstraint = "chat_messages_model_config_id_fkey" // ALTER TABLE ONLY chat_messages ADD CONSTRAINT chat_messages_model_config_id_fkey FOREIGN KEY (model_config_id) REFERENCES chat_model_configs(id);
@@ -0,0 +1,106 @@
-- Rollback for the chatd core state machine foundation migration.
-- 1. Recreate chats_expanded without the new chat fields. We must drop
-- the view first because the subsequent column drops would fail with
-- "view depends on column".
DROP VIEW IF EXISTS chats_expanded;
-- 2. Drop the worker acquisition candidates index.
DROP INDEX IF EXISTS idx_chats_worker_acquisition_candidates;
-- 3. Drop the retry state trigger and function.
DROP TRIGGER IF EXISTS trigger_sync_chat_retry_state ON chats;
DROP FUNCTION IF EXISTS sync_chat_retry_state();
-- 4. Drop the queue version triggers and function.
DROP TRIGGER IF EXISTS trigger_bump_chat_queue_version_on_queued_message_delete ON chat_queued_messages;
DROP TRIGGER IF EXISTS trigger_bump_chat_queue_version_on_queued_message_update ON chat_queued_messages;
DROP TRIGGER IF EXISTS trigger_bump_chat_queue_version_on_queued_message_insert ON chat_queued_messages;
DROP FUNCTION IF EXISTS bump_chat_queue_version_on_queued_message_change();
-- 5. Drop the message revision triggers and functions.
DROP TRIGGER IF EXISTS trigger_update_chat_history_after_message_update ON chat_messages;
DROP TRIGGER IF EXISTS trigger_update_chat_history_after_message_insert ON chat_messages;
DROP TRIGGER IF EXISTS trigger_set_chat_message_revision_on_update ON chat_messages;
DROP TRIGGER IF EXISTS trigger_set_chat_message_revision_on_insert ON chat_messages;
DROP FUNCTION IF EXISTS update_chat_history_after_message_update();
DROP FUNCTION IF EXISTS update_chat_history_after_message_insert();
-- The pre-split function name is kept here for backward compatibility
-- with environments that may have applied an earlier draft of the up
-- migration. DROP FUNCTION IF EXISTS is a no-op if the function is
-- absent.
DROP FUNCTION IF EXISTS update_chat_history_after_message_changes();
DROP FUNCTION IF EXISTS set_chat_message_revision_before();
DROP FUNCTION IF EXISTS set_chat_message_revision();
-- 6. Drop chat_heartbeats (and its index by association).
DROP TABLE IF EXISTS chat_heartbeats;
-- 7. Drop chat_queued_messages.position and its default sequence, plus
-- created_by.
ALTER TABLE chat_queued_messages
ALTER COLUMN position DROP DEFAULT;
ALTER TABLE chat_queued_messages
DROP COLUMN IF EXISTS position,
DROP COLUMN IF EXISTS created_by;
DROP SEQUENCE IF EXISTS chat_queued_messages_position_seq;
-- 8. Drop chat_messages.revision.
ALTER TABLE chat_messages
DROP COLUMN IF EXISTS revision;
-- 9. Drop the new chats columns.
ALTER TABLE chats
DROP COLUMN IF EXISTS snapshot_version,
DROP COLUMN IF EXISTS history_version,
DROP COLUMN IF EXISTS queue_version,
DROP COLUMN IF EXISTS generation_attempt,
DROP COLUMN IF EXISTS retry_state,
DROP COLUMN IF EXISTS retry_state_version,
DROP COLUMN IF EXISTS runner_id,
DROP COLUMN IF EXISTS requires_action_deadline_at;
-- 10. Recreate chats_expanded with the pre-migration field list.
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.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.last_injected_context,
c.dynamic_tools,
c.organization_id,
c.plan_mode,
c.client_type,
c.last_turn_summary,
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
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;
-- 11. The `interrupting` chat_status enum value is intentionally left
-- in place. Postgres does not support dropping a single enum value
-- without recreating the entire type, which would require rewriting
-- every chat row and is unsafe inside a transactional rollback.
@@ -0,0 +1,358 @@
-- Adds the core chat state-machine storage model.
-- Adds new versioning fields to chats, a revision column to chat_messages,
-- positional ordering and creator tracking to chat_queued_messages, an
-- unlogged chat_heartbeats table for ownership leases, and Postgres
-- triggers that keep history/queue versioning consistent.
-- 1. Add `interrupting` to the chat_status enum.
ALTER TYPE chat_status ADD VALUE IF NOT EXISTS 'interrupting';
-- 2. Add new versioning, ownership, retry, and pending-action fields to chats.
ALTER TABLE chats
ADD COLUMN snapshot_version bigint NOT NULL DEFAULT 1,
ADD COLUMN history_version bigint NOT NULL DEFAULT 0,
ADD COLUMN queue_version bigint NOT NULL DEFAULT 0,
ADD COLUMN generation_attempt bigint NOT NULL DEFAULT 0,
ADD COLUMN retry_state jsonb,
ADD COLUMN retry_state_version bigint NOT NULL DEFAULT 0,
ADD COLUMN runner_id uuid,
ADD COLUMN requires_action_deadline_at timestamp with time zone;
COMMENT ON COLUMN chats.snapshot_version IS
'Monotonic version for the full chat snapshot. Starts at 1 so stream loops and workers can use 0 to mean they have not loaded the chat yet.';
COMMENT ON COLUMN chats.history_version IS
'Snapshot version of the latest durable history change. Starts at 0 until chat_messages triggers set it to the current snapshot_version.';
COMMENT ON COLUMN chats.queue_version IS
'Snapshot version of the latest queued-message change. Starts at 0 until chat_queued_messages triggers set it to the current snapshot_version.';
-- 3. Add `revision` to chat_messages. Adding the column as NOT NULL with
-- a constant default backfills existing rows through catalog metadata
-- only, so the highest-volume table is neither rewritten nor scanned for
-- NOT NULL validation while under ACCESS EXCLUSIVE. The default is
-- dropped immediately because the BEFORE INSERT trigger below rejects
-- inserts that pre-assign revision and assigns it from
-- chats.snapshot_version instead.
ALTER TABLE chat_messages
ADD COLUMN revision bigint NOT NULL DEFAULT 1;
ALTER TABLE chat_messages
ALTER COLUMN revision DROP DEFAULT;
-- 4. Backfill chats.history_version = 1 for chats that already have at
-- least one message. We avoid recursive trigger fire by performing the
-- backfill before the triggers are created.
UPDATE chats
SET history_version = 1
WHERE EXISTS (
SELECT 1 FROM chat_messages WHERE chat_messages.chat_id = chats.id
);
-- 5. Add `position` and `created_by` to chat_queued_messages.
ALTER TABLE chat_queued_messages
ADD COLUMN position bigint,
ADD COLUMN created_by uuid;
-- 6. Backfill chat_queued_messages.position per chat using row_number(),
-- ordering by created_at and breaking ties by id.
WITH ordered AS (
SELECT
id,
row_number() OVER (
PARTITION BY chat_id
ORDER BY created_at, id
) AS rn
FROM chat_queued_messages
)
UPDATE chat_queued_messages
SET position = ordered.rn
FROM ordered
WHERE chat_queued_messages.id = ordered.id;
-- 7. Backfill chat_queued_messages.created_by from chats.owner_id.
UPDATE chat_queued_messages
SET created_by = chats.owner_id
FROM chats
WHERE chat_queued_messages.chat_id = chats.id
AND chat_queued_messages.created_by IS NULL;
-- 8. Enforce NOT NULL on chat_queued_messages.position and
-- created_by. Legacy queued-message inserts are updated to populate
-- created_by from the chat owner when no explicit creator exists.
ALTER TABLE chat_queued_messages
ALTER COLUMN position SET NOT NULL,
ALTER COLUMN created_by SET NOT NULL;
-- 9. Default sequence for new queued-message positions.
-- A global sequence is acceptable because ordering only needs to be
-- stable within a chat.
CREATE SEQUENCE IF NOT EXISTS chat_queued_messages_position_seq AS bigint START WITH 1;
SELECT setval(
'chat_queued_messages_position_seq',
GREATEST((SELECT COALESCE(MAX(position), 0) FROM chat_queued_messages), 1)
);
ALTER TABLE chat_queued_messages
ALTER COLUMN position SET DEFAULT nextval('chat_queued_messages_position_seq');
-- 10. Backfill chats.queue_version = 1 for chats that already have queued
-- messages. Same trigger-avoidance reasoning as for history_version.
UPDATE chats
SET queue_version = 1
WHERE EXISTS (
SELECT 1 FROM chat_queued_messages WHERE chat_queued_messages.chat_id = chats.id
);
-- 11. chat_heartbeats: unlogged table for ownership leases. Keyed by
-- (chat_id, runner_id) so a single chat can briefly have entries from
-- multiple runners during failover.
CREATE UNLOGGED TABLE IF NOT EXISTS chat_heartbeats (
chat_id uuid NOT NULL REFERENCES chats(id) ON DELETE CASCADE,
runner_id uuid NOT NULL,
heartbeat_at timestamp with time zone NOT NULL,
PRIMARY KEY (chat_id, runner_id)
);
COMMENT ON TABLE chat_heartbeats IS
'Ephemeral runner ownership leases for runnable chats. The table is unlogged because losing heartbeat rows after a crash is safe: missing heartbeats are treated as stale ownership and cause workers to reacquire runnable chats.';
CREATE INDEX IF NOT EXISTS chat_heartbeats_heartbeat_at_idx
ON chat_heartbeats (heartbeat_at);
-- 12. Message revision trigger.
-- The BEFORE-trigger only assigns NEW.revision from chats.snapshot_version
-- and validates immutability. The chats.history_version /
-- generation_attempt update is performed by an AFTER STATEMENT trigger
-- so it doesn't conflict with CTE updates on the chats row in the same
-- command (the legacy InsertChatMessages query updates last_model_config_id
-- in a CTE on chats and then inserts messages).
CREATE FUNCTION set_chat_message_revision_before()
RETURNS trigger AS $$
DECLARE
chat_snapshot_version bigint;
BEGIN
IF TG_OP = 'INSERT' AND NEW.revision IS NOT NULL THEN
RAISE EXCEPTION 'chat_messages.revision must be assigned by trigger';
END IF;
IF TG_OP = 'UPDATE' THEN
IF OLD.chat_id IS DISTINCT FROM NEW.chat_id THEN
RAISE EXCEPTION 'chat_messages.chat_id is immutable';
END IF;
IF OLD.revision IS DISTINCT FROM NEW.revision THEN
RAISE EXCEPTION 'chat_messages.revision must be assigned by trigger';
END IF;
IF OLD IS NOT DISTINCT FROM NEW THEN
RETURN NEW;
END IF;
END IF;
SELECT snapshot_version INTO chat_snapshot_version
FROM chats WHERE id = NEW.chat_id;
IF chat_snapshot_version IS NULL THEN
RAISE EXCEPTION 'chat % does not exist', NEW.chat_id;
END IF;
NEW.revision = chat_snapshot_version;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- AFTER STATEMENT trigger functions. Use the transition tables to
-- update chats.history_version / generation_attempt once per chat per
-- command. Running AFTER row inserts/updates complete lets a CTE
-- update on the same chats row in the same command finalize before
-- this trigger needs to update it.
--
-- The INSERT and UPDATE variants are split so the UPDATE variant can
-- reference both the OLD and NEW transition tables and skip rows that
-- did not actually change. Without that filter, a no-op UPDATE on a
-- chat_messages row (one whose OLD IS NOT DISTINCT FROM NEW) would
-- still advance chats.history_version whenever the chat's snapshot
-- had previously been bumped.
CREATE FUNCTION update_chat_history_after_message_insert()
RETURNS trigger AS $$
BEGIN
UPDATE chats c
SET history_version = c.snapshot_version,
generation_attempt = 0
FROM (
SELECT DISTINCT chat_id FROM chat_message_history_new_rows
) AS affected
WHERE c.id = affected.chat_id
AND (
c.history_version IS DISTINCT FROM c.snapshot_version
OR c.generation_attempt <> 0
);
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE FUNCTION update_chat_history_after_message_update()
RETURNS trigger AS $$
BEGIN
UPDATE chats c
SET history_version = c.snapshot_version,
generation_attempt = 0
FROM (
SELECT DISTINCT n.chat_id
FROM chat_message_history_new_rows n
JOIN chat_message_history_old_rows o ON o.id = n.id
WHERE o IS DISTINCT FROM n
) AS affected
WHERE c.id = affected.chat_id
AND (
c.history_version IS DISTINCT FROM c.snapshot_version
OR c.generation_attempt <> 0
);
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trigger_set_chat_message_revision_on_insert
BEFORE INSERT ON chat_messages
FOR EACH ROW
EXECUTE FUNCTION set_chat_message_revision_before();
CREATE TRIGGER trigger_set_chat_message_revision_on_update
BEFORE UPDATE ON chat_messages
FOR EACH ROW
EXECUTE FUNCTION set_chat_message_revision_before();
CREATE TRIGGER trigger_update_chat_history_after_message_insert
AFTER INSERT ON chat_messages
REFERENCING NEW TABLE AS chat_message_history_new_rows
FOR EACH STATEMENT
EXECUTE FUNCTION update_chat_history_after_message_insert();
CREATE TRIGGER trigger_update_chat_history_after_message_update
AFTER UPDATE ON chat_messages
REFERENCING OLD TABLE AS chat_message_history_old_rows NEW TABLE AS chat_message_history_new_rows
FOR EACH STATEMENT
EXECUTE FUNCTION update_chat_history_after_message_update();
-- 13. Queue version trigger function.
CREATE FUNCTION bump_chat_queue_version_on_queued_message_change()
RETURNS trigger AS $$
DECLARE
changed_chat_id uuid;
BEGIN
IF TG_OP = 'DELETE' THEN
changed_chat_id = OLD.chat_id;
ELSE
changed_chat_id = NEW.chat_id;
END IF;
UPDATE chats
SET queue_version = snapshot_version
WHERE id = changed_chat_id;
IF TG_OP = 'DELETE' THEN
RETURN OLD;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trigger_bump_chat_queue_version_on_queued_message_insert
AFTER INSERT ON chat_queued_messages
FOR EACH ROW
EXECUTE FUNCTION bump_chat_queue_version_on_queued_message_change();
CREATE TRIGGER trigger_bump_chat_queue_version_on_queued_message_update
AFTER UPDATE OF content, model_config_id, position, created_by
ON chat_queued_messages
FOR EACH ROW
EXECUTE FUNCTION bump_chat_queue_version_on_queued_message_change();
CREATE TRIGGER trigger_bump_chat_queue_version_on_queued_message_delete
AFTER DELETE ON chat_queued_messages
FOR EACH ROW
EXECUTE FUNCTION bump_chat_queue_version_on_queued_message_change();
-- 14. Retry state trigger function.
CREATE FUNCTION sync_chat_retry_state()
RETURNS trigger AS $$
BEGIN
IF OLD.retry_state_version IS DISTINCT FROM NEW.retry_state_version THEN
RAISE EXCEPTION 'chats.retry_state_version must be assigned by trigger';
END IF;
IF NEW.generation_attempt IS DISTINCT FROM OLD.generation_attempt THEN
NEW.retry_state = NULL;
END IF;
IF NEW.retry_state IS DISTINCT FROM OLD.retry_state THEN
NEW.retry_state_version = NEW.snapshot_version;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trigger_sync_chat_retry_state
BEFORE UPDATE OF retry_state, retry_state_version, generation_attempt
ON chats
FOR EACH ROW
EXECUTE FUNCTION sync_chat_retry_state();
-- 15. Index for the chat worker acquisition scan, which runs every 30
-- seconds per replica plus on every worker wake. Leading on status lets
-- the scan touch only rows in the worker-runnable status set instead of
-- sequentially scanning the ever-growing chats table. The status set is
-- intentionally not part of the index predicate: 'interrupting' is added
-- to chat_status above, and Postgres forbids using a new enum value in
-- the same transaction, which all migrations share.
CREATE INDEX idx_chats_worker_acquisition_candidates ON chats
USING btree (status, updated_at, id)
WHERE archived = false;
-- 16. Refresh chats_expanded to include the new chat fields. Drop and
-- recreate so column ordering is stable.
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.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.last_injected_context,
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
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;
@@ -0,0 +1,17 @@
-- Fixture coverage for the chat_heartbeats table introduced in
-- migration 000500. The earlier chat fixtures already insert at least
-- one row into chats; we attach a heartbeat for the first such chat so
-- migration tests see a non-empty chat_heartbeats table without
-- hard-coding a specific chat ID.
INSERT INTO chat_heartbeats (
chat_id,
runner_id,
heartbeat_at
)
SELECT
chats.id,
'00000000-0000-0000-0000-0000000fea51'::uuid,
'2024-01-01 00:00:00+00'
FROM chats
ORDER BY created_at, id
LIMIT 1;
+16
View File
@@ -824,6 +824,14 @@ func (q *sqlQuerier) GetAuthorizedChats(ctx context.Context, arg GetChatsParams,
&i.Chat.PlanMode,
&i.Chat.ClientType,
&i.Chat.LastTurnSummary,
&i.Chat.SnapshotVersion,
&i.Chat.HistoryVersion,
&i.Chat.QueueVersion,
&i.Chat.GenerationAttempt,
&i.Chat.RetryState,
&i.Chat.RetryStateVersion,
&i.Chat.RunnerID,
&i.Chat.RequiresActionDeadlineAt,
&i.Chat.UserACL,
&i.Chat.GroupACL,
&i.Chat.OwnerUsername,
@@ -891,6 +899,14 @@ func (q *sqlQuerier) GetAuthorizedChatsByChatFileID(ctx context.Context, fileID
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
&i.GenerationAttempt,
&i.RetryState,
&i.RetryStateVersion,
&i.RunnerID,
&i.RequiresActionDeadlineAt,
&i.UserACL,
&i.GroupACL,
&i.OwnerUsername,
+65 -33
View File
@@ -1567,6 +1567,7 @@ const (
ChatStatusCompleted ChatStatus = "completed"
ChatStatusError ChatStatus = "error"
ChatStatusRequiresAction ChatStatus = "requires_action"
ChatStatusInterrupting ChatStatus = "interrupting"
)
func (e *ChatStatus) Scan(src interface{}) error {
@@ -1612,7 +1613,8 @@ func (e ChatStatus) Valid() bool {
ChatStatusPaused,
ChatStatusCompleted,
ChatStatusError,
ChatStatusRequiresAction:
ChatStatusRequiresAction,
ChatStatusInterrupting:
return true
}
return false
@@ -1627,6 +1629,7 @@ func AllChatStatusValues() []ChatStatus {
ChatStatusCompleted,
ChatStatusError,
ChatStatusRequiresAction,
ChatStatusInterrupting,
}
}
@@ -4607,38 +4610,46 @@ type BoundaryUsageStat struct {
}
type Chat struct {
ID uuid.UUID `db:"id" json:"id"`
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
WorkspaceID uuid.NullUUID `db:"workspace_id" json:"workspace_id"`
Title string `db:"title" json:"title"`
Status ChatStatus `db:"status" json:"status"`
WorkerID uuid.NullUUID `db:"worker_id" json:"worker_id"`
StartedAt sql.NullTime `db:"started_at" json:"started_at"`
HeartbeatAt sql.NullTime `db:"heartbeat_at" json:"heartbeat_at"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
ParentChatID uuid.NullUUID `db:"parent_chat_id" json:"parent_chat_id"`
RootChatID uuid.NullUUID `db:"root_chat_id" json:"root_chat_id"`
LastModelConfigID uuid.UUID `db:"last_model_config_id" json:"last_model_config_id"`
Archived bool `db:"archived" json:"archived"`
LastError pqtype.NullRawMessage `db:"last_error" json:"last_error"`
Mode NullChatMode `db:"mode" json:"mode"`
MCPServerIDs []uuid.UUID `db:"mcp_server_ids" json:"mcp_server_ids"`
Labels StringMap `db:"labels" json:"labels"`
BuildID uuid.NullUUID `db:"build_id" json:"build_id"`
AgentID uuid.NullUUID `db:"agent_id" json:"agent_id"`
PinOrder int32 `db:"pin_order" json:"pin_order"`
LastReadMessageID sql.NullInt64 `db:"last_read_message_id" json:"last_read_message_id"`
LastInjectedContext pqtype.NullRawMessage `db:"last_injected_context" json:"last_injected_context"`
DynamicTools pqtype.NullRawMessage `db:"dynamic_tools" json:"dynamic_tools"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
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"`
UserACL ChatACL `db:"user_acl" json:"user_acl"`
GroupACL ChatACL `db:"group_acl" json:"group_acl"`
OwnerUsername string `db:"owner_username" json:"owner_username"`
OwnerName string `db:"owner_name" json:"owner_name"`
ID uuid.UUID `db:"id" json:"id"`
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
WorkspaceID uuid.NullUUID `db:"workspace_id" json:"workspace_id"`
Title string `db:"title" json:"title"`
Status ChatStatus `db:"status" json:"status"`
WorkerID uuid.NullUUID `db:"worker_id" json:"worker_id"`
StartedAt sql.NullTime `db:"started_at" json:"started_at"`
HeartbeatAt sql.NullTime `db:"heartbeat_at" json:"heartbeat_at"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
ParentChatID uuid.NullUUID `db:"parent_chat_id" json:"parent_chat_id"`
RootChatID uuid.NullUUID `db:"root_chat_id" json:"root_chat_id"`
LastModelConfigID uuid.UUID `db:"last_model_config_id" json:"last_model_config_id"`
Archived bool `db:"archived" json:"archived"`
LastError pqtype.NullRawMessage `db:"last_error" json:"last_error"`
Mode NullChatMode `db:"mode" json:"mode"`
MCPServerIDs []uuid.UUID `db:"mcp_server_ids" json:"mcp_server_ids"`
Labels StringMap `db:"labels" json:"labels"`
BuildID uuid.NullUUID `db:"build_id" json:"build_id"`
AgentID uuid.NullUUID `db:"agent_id" json:"agent_id"`
PinOrder int32 `db:"pin_order" json:"pin_order"`
LastReadMessageID sql.NullInt64 `db:"last_read_message_id" json:"last_read_message_id"`
LastInjectedContext pqtype.NullRawMessage `db:"last_injected_context" json:"last_injected_context"`
DynamicTools pqtype.NullRawMessage `db:"dynamic_tools" json:"dynamic_tools"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
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"`
SnapshotVersion int64 `db:"snapshot_version" json:"snapshot_version"`
HistoryVersion int64 `db:"history_version" json:"history_version"`
QueueVersion int64 `db:"queue_version" json:"queue_version"`
GenerationAttempt int64 `db:"generation_attempt" json:"generation_attempt"`
RetryState pqtype.NullRawMessage `db:"retry_state" json:"retry_state"`
RetryStateVersion int64 `db:"retry_state_version" json:"retry_state_version"`
RunnerID uuid.NullUUID `db:"runner_id" json:"runner_id"`
RequiresActionDeadlineAt sql.NullTime `db:"requires_action_deadline_at" json:"requires_action_deadline_at"`
UserACL ChatACL `db:"user_acl" json:"user_acl"`
GroupACL ChatACL `db:"group_acl" json:"group_acl"`
OwnerUsername string `db:"owner_username" json:"owner_username"`
OwnerName string `db:"owner_name" json:"owner_name"`
}
type ChatDebugRun struct {
@@ -4720,6 +4731,13 @@ type ChatFileLink struct {
FileID uuid.UUID `db:"file_id" json:"file_id"`
}
// Ephemeral runner ownership leases for runnable chats. The table is unlogged because losing heartbeat rows after a crash is safe: missing heartbeats are treated as stale ownership and cause workers to reacquire runnable chats.
type ChatHeartbeat struct {
ChatID uuid.UUID `db:"chat_id" json:"chat_id"`
RunnerID uuid.UUID `db:"runner_id" json:"runner_id"`
HeartbeatAt time.Time `db:"heartbeat_at" json:"heartbeat_at"`
}
type ChatMessage struct {
ID int64 `db:"id" json:"id"`
ChatID uuid.UUID `db:"chat_id" json:"chat_id"`
@@ -4743,6 +4761,7 @@ type ChatMessage struct {
Deleted bool `db:"deleted" json:"deleted"`
ProviderResponseID sql.NullString `db:"provider_response_id" json:"provider_response_id"`
APIKeyID sql.NullString `db:"api_key_id" json:"api_key_id"`
Revision int64 `db:"revision" json:"revision"`
}
type ChatModelConfig struct {
@@ -4771,6 +4790,8 @@ type ChatQueuedMessage struct {
CreatedAt time.Time `db:"created_at" json:"created_at"`
ModelConfigID uuid.NullUUID `db:"model_config_id" json:"model_config_id"`
APIKeyID sql.NullString `db:"api_key_id" json:"api_key_id"`
Position int64 `db:"position" json:"position"`
CreatedBy uuid.UUID `db:"created_by" json:"created_by"`
}
type ChatTable struct {
@@ -4804,6 +4825,17 @@ type ChatTable struct {
LastTurnSummary sql.NullString `db:"last_turn_summary" json:"last_turn_summary"`
UserACL ChatACL `db:"user_acl" json:"user_acl"`
GroupACL ChatACL `db:"group_acl" json:"group_acl"`
// Monotonic version for the full chat snapshot. Starts at 1 so stream loops and workers can use 0 to mean they have not loaded the chat yet.
SnapshotVersion int64 `db:"snapshot_version" json:"snapshot_version"`
// Snapshot version of the latest durable history change. Starts at 0 until chat_messages triggers set it to the current snapshot_version.
HistoryVersion int64 `db:"history_version" json:"history_version"`
// Snapshot version of the latest queued-message change. Starts at 0 until chat_queued_messages triggers set it to the current snapshot_version.
QueueVersion int64 `db:"queue_version" json:"queue_version"`
GenerationAttempt int64 `db:"generation_attempt" json:"generation_attempt"`
RetryState pqtype.NullRawMessage `db:"retry_state" json:"retry_state"`
RetryStateVersion int64 `db:"retry_state_version" json:"retry_state_version"`
RunnerID uuid.NullUUID `db:"runner_id" json:"runner_id"`
RequiresActionDeadlineAt sql.NullTime `db:"requires_action_deadline_at" json:"requires_action_deadline_at"`
}
type ChatUsageLimitConfig struct {
+85 -5
View File
@@ -77,9 +77,12 @@ type sqlcQuerier interface {
// enforces that non-deleted rows always have a provider ID.
BackfillChatModelConfigProvider(ctx context.Context, arg BackfillChatModelConfigProviderParams) (sql.Result, error)
BackoffChatDiffStatus(ctx context.Context, arg BackoffChatDiffStatusParams) error
// Deletes heartbeat rows for the supplied (chat_id, runner_id) pairs.
BatchDeleteChatHeartbeats(ctx context.Context, arg BatchDeleteChatHeartbeatsParams) (int64, error)
BatchUpdateWorkspaceAgentMetadata(ctx context.Context, arg BatchUpdateWorkspaceAgentMetadataParams) error
BatchUpdateWorkspaceLastUsedAt(ctx context.Context, arg BatchUpdateWorkspaceLastUsedAtParams) error
BatchUpdateWorkspaceNextStartAt(ctx context.Context, arg BatchUpdateWorkspaceNextStartAtParams) error
BatchUpsertChatHeartbeats(ctx context.Context, arg BatchUpsertChatHeartbeatsParams) error
BatchUpsertConnectionLogs(ctx context.Context, arg BatchUpsertConnectionLogsParams) error
BulkMarkNotificationMessagesFailed(ctx context.Context, arg BulkMarkNotificationMessagesFailedParams) (int64, error)
BulkMarkNotificationMessagesSent(ctx context.Context, arg BulkMarkNotificationMessagesSentParams) (int64, error)
@@ -94,6 +97,9 @@ type sqlcQuerier interface {
ClearChatMessageProviderResponseIDsByChatID(ctx context.Context, chatID uuid.UUID) error
CountAIBridgeSessions(ctx context.Context, arg CountAIBridgeSessionsParams) (int64, error)
CountAuditLogs(ctx context.Context, arg CountAuditLogsParams) (int64, error)
// Cheap queue-length check used by ChatMachine.Update when deciding
// whether the chat is in a "1" sub-state.
CountChatQueuedMessages(ctx context.Context, chatID uuid.UUID) (int64, error)
CountConnectionLogs(ctx context.Context, arg CountConnectionLogsParams) (int64, error)
// Counts enabled, non-deleted model configs that lack both input and
// output pricing in their JSONB options.cost configuration.
@@ -111,7 +117,11 @@ type sqlcQuerier interface {
DeleteAIProviderKey(ctx context.Context, id uuid.UUID) error
DeleteAPIKeyByID(ctx context.Context, id string) error
DeleteAPIKeysByUserID(ctx context.Context, userID uuid.UUID) error
// Deletes all heartbeat rows for the chat. Used during ownership
// transitions that abandon a lease.
DeleteAllChatHeartbeats(ctx context.Context, chatID uuid.UUID) error
DeleteAllChatQueuedMessages(ctx context.Context, chatID uuid.UUID) error
DeleteAllChatQueuedMessagesReturningCount(ctx context.Context, chatID uuid.UUID) (int64, error)
DeleteAllTailnetTunnels(ctx context.Context, arg DeleteAllTailnetTunnelsParams) ([]DeleteAllTailnetTunnelsRow, error)
// Deletes all existing webpush subscriptions.
// This should be called when the VAPID keypair is regenerated, as the old
@@ -133,6 +143,10 @@ type sqlcQuerier interface {
DeleteChatModelConfigsByAIProviderID(ctx context.Context, aiProviderID uuid.UUID) error
DeleteChatModelConfigsByProvider(ctx context.Context, provider string) error
DeleteChatQueuedMessage(ctx context.Context, arg DeleteChatQueuedMessageParams) error
// Deletes a queued message, scoped to the parent chat. Returns the
// number of affected rows so callers can detect missing rows without
// a follow-up read.
DeleteChatQueuedMessageReturningCount(ctx context.Context, arg DeleteChatQueuedMessageReturningCountParams) (int64, error)
DeleteChatUsageLimitGroupOverride(ctx context.Context, groupID uuid.UUID) error
DeleteChatUsageLimitUserOverride(ctx context.Context, userID uuid.UUID) error
DeleteCryptoKey(ctx context.Context, arg DeleteCryptoKeyParams) (CryptoKey, error)
@@ -201,6 +215,7 @@ type sqlcQuerier interface {
DeleteProvisionerKey(ctx context.Context, id uuid.UUID) error
DeleteReplicasUpdatedBefore(ctx context.Context, updatedAt time.Time) error
DeleteRuntimeConfig(ctx context.Context, key string) error
DeleteStaleChatHeartbeats(ctx context.Context, staleSeconds int32) (int64, error)
DeleteTailnetPeer(ctx context.Context, arg DeleteTailnetPeerParams) (DeleteTailnetPeerRow, error)
DeleteTailnetTunnel(ctx context.Context, arg DeleteTailnetTunnelParams) (DeleteTailnetTunnelRow, error)
DeleteTask(ctx context.Context, arg DeleteTaskParams) (uuid.UUID, error)
@@ -323,6 +338,10 @@ type sqlcQuerier interface {
// This function returns roles for authorization purposes. Implied member roles
// are included.
GetAuthorizationUserRoles(ctx context.Context, userID uuid.UUID) (GetAuthorizationUserRolesRow, error)
// Returns read-only root chat candidates for state-machine-backed
// auto-archive. Activity is computed across the root family. The query
// limits roots, not total family members.
GetAutoArchiveInactiveChatCandidates(ctx context.Context, arg GetAutoArchiveInactiveChatCandidatesParams) ([]GetAutoArchiveInactiveChatCandidatesRow, error)
GetBoundaryLogByID(ctx context.Context, id uuid.UUID) (BoundaryLog, error)
GetBoundarySessionByID(ctx context.Context, id uuid.UUID) (BoundarySession, error)
GetChatACLByID(ctx context.Context, id uuid.UUID) (GetChatACLByIDRow, error)
@@ -334,6 +353,7 @@ type sqlcQuerier interface {
// Auto-archive window in days. 0 disables.
GetChatAutoArchiveDays(ctx context.Context, defaultAutoArchiveDays int32) (int32, error)
GetChatByID(ctx context.Context, id uuid.UUID) (Chat, error)
GetChatByIDForShare(ctx context.Context, id uuid.UUID) (Chat, error)
GetChatByIDForUpdate(ctx context.Context, id uuid.UUID) (Chat, error)
GetChatComputerUseProvider(ctx context.Context) (string, error)
// Per-root-chat cost breakdown for a single user within a date range.
@@ -371,6 +391,10 @@ type sqlcQuerier interface {
GetChatDiffStatusSummary(ctx context.Context) (GetChatDiffStatusSummaryRow, error)
GetChatDiffStatusesByChatIDs(ctx context.Context, chatIds []uuid.UUID) ([]ChatDiffStatus, error)
GetChatExploreModelOverride(ctx context.Context) (string, error)
// Returns the chat IDs of every chat in a family (root + all children)
// in deterministic order. The id parameter must be the root id; the
// query does not walk up from a child.
GetChatFamilyIDsByRootID(ctx context.Context, id uuid.UUID) ([]uuid.UUID, error)
GetChatFileByID(ctx context.Context, id uuid.UUID) (ChatFile, error)
// GetChatFileMetadataByChatID returns lightweight file metadata for
// all files linked to a chat. The data column is excluded to avoid
@@ -378,6 +402,7 @@ type sqlcQuerier interface {
GetChatFileMetadataByChatID(ctx context.Context, chatID uuid.UUID) ([]GetChatFileMetadataByChatIDRow, error)
GetChatFilesByIDs(ctx context.Context, ids []uuid.UUID) ([]ChatFile, error)
GetChatGeneralModelOverride(ctx context.Context) (string, error)
GetChatHeartbeat(ctx context.Context, arg GetChatHeartbeatParams) (ChatHeartbeat, error)
// GetChatIncludeDefaultSystemPrompt preserves the legacy default
// for deployments created before the explicit include-default toggle.
// When the toggle is unset, a non-empty custom prompt implies false;
@@ -391,6 +416,7 @@ type sqlcQuerier interface {
GetChatMessagesByChatID(ctx context.Context, arg GetChatMessagesByChatIDParams) ([]ChatMessage, error)
GetChatMessagesByChatIDAscPaginated(ctx context.Context, arg GetChatMessagesByChatIDAscPaginatedParams) ([]ChatMessage, error)
GetChatMessagesByChatIDDescPaginated(ctx context.Context, arg GetChatMessagesByChatIDDescPaginatedParams) ([]ChatMessage, error)
GetChatMessagesByRevisionForStream(ctx context.Context, arg GetChatMessagesByRevisionForStreamParams) ([]ChatMessage, error)
GetChatMessagesForPromptByChatID(ctx context.Context, chatID uuid.UUID) ([]ChatMessage, error)
GetChatModelConfigByID(ctx context.Context, id uuid.UUID) (ChatModelConfig, error)
GetChatModelConfigs(ctx context.Context) ([]ChatModelConfig, error)
@@ -400,12 +426,18 @@ type sqlcQuerier interface {
// personal chat model overrides. It defaults to false when unset.
GetChatPersonalModelOverridesEnabled(ctx context.Context) (bool, error)
GetChatPlanModeInstructions(ctx context.Context) (string, error)
GetChatQueuedMessageByID(ctx context.Context, arg GetChatQueuedMessageByIDParams) (ChatQueuedMessage, error)
// Returns the queue head (lowest position, then lowest id).
GetChatQueuedMessageHead(ctx context.Context, chatID uuid.UUID) (ChatQueuedMessage, error)
GetChatQueuedMessages(ctx context.Context, chatID uuid.UUID) ([]ChatQueuedMessage, error)
// Returns queued messages in state-machine order (position ASC, id ASC).
GetChatQueuedMessagesByPosition(ctx context.Context, chatID uuid.UUID) ([]ChatQueuedMessage, error)
// Returns the chat retention period in days. Chats archived longer
// than this and orphaned chat files older than this are purged by
// dbpurge. Returns 30 (days) when no value has been configured.
// A value of 0 disables chat purging entirely.
GetChatRetentionDays(ctx context.Context) (int32, error)
GetChatStreamSyncRows(ctx context.Context, ids []uuid.UUID) ([]GetChatStreamSyncRowsRow, error)
GetChatSystemPrompt(ctx context.Context) (string, error)
// GetChatSystemPromptConfig returns both chat system prompt settings in a
// single read to avoid torn reads between separate site-config lookups.
@@ -430,11 +462,24 @@ type sqlcQuerier interface {
// jsonb_array_elements never raises "cannot extract elements from a
// scalar". Backed by idx_chat_messages_user_prompts.
GetChatUserPromptsByChatID(ctx context.Context, arg GetChatUserPromptsByChatIDParams) ([]GetChatUserPromptsByChatIDRow, error)
// Returns chats that workers may try to acquire. Candidates must be:
// - in a worker-runnable execution status;
// - unarchived; and
// - missing ownership, carrying inconsistent ownership, or lacking a
// fresh heartbeat for the assigned runner.
//
// Missing ownership is worker_id IS NULL. Inconsistent ownership is
// runner_id IS NULL while worker_id is set. Stale ownership is no
// heartbeat row for (chat_id, runner_id), or one older than
// @stale_seconds by database time. Candidates are ordered by oldest
// updated_at first so workers drain stale runnable chats predictably.
GetChatWorkerAcquisitionCandidates(ctx context.Context, arg GetChatWorkerAcquisitionCandidatesParams) ([]GetChatWorkerAcquisitionCandidatesRow, error)
// Returns the global TTL for chat workspaces as a Go duration string.
// Returns "0s" (disabled) when no value has been configured.
GetChatWorkspaceTTL(ctx context.Context) (string, error)
GetChats(ctx context.Context, arg GetChatsParams) ([]GetChatsRow, error)
GetChatsByChatFileID(ctx context.Context, fileID uuid.UUID) ([]Chat, error)
GetChatsByIDsForRunnerSync(ctx context.Context, ids []uuid.UUID) ([]Chat, error)
GetChatsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]Chat, error)
// Retrieves chats updated after the given timestamp for telemetry
// snapshot collection. Uses updated_at so that long-running chats
@@ -451,6 +496,10 @@ type sqlcQuerier interface {
GetCryptoKeysByFeature(ctx context.Context, feature CryptoKeyFeature) ([]CryptoKey, error)
GetDBCryptKeys(ctx context.Context) ([]DBCryptKey, error)
GetDERPMeshKey(ctx context.Context) (string, error)
// Returns the current database timestamp. Used so transitions that
// record deadlines or heartbeats rely on a clock that is consistent
// with the database rather than the caller's local clock.
GetDatabaseNow(ctx context.Context) (time.Time, error)
GetDefaultChatModelConfig(ctx context.Context) (ChatModelConfig, error)
GetDefaultOrganization(ctx context.Context) (Organization, error)
GetDefaultProxyConfig(ctx context.Context) (GetDefaultProxyConfigRow, error)
@@ -931,6 +980,8 @@ type sqlcQuerier interface {
GetWorkspacesByTemplateID(ctx context.Context, templateID uuid.UUID) ([]WorkspaceTable, error)
GetWorkspacesEligibleForTransition(ctx context.Context, now time.Time) ([]GetWorkspacesEligibleForTransitionRow, error)
GetWorkspacesForWorkspaceMetrics(ctx context.Context) ([]GetWorkspacesForWorkspaceMetricsRow, error)
// Increments generation_attempt and returns the resulting value.
IncrementChatGenerationAttempt(ctx context.Context, id uuid.UUID) (int64, error)
InsertAIBridgeInterception(ctx context.Context, arg InsertAIBridgeInterceptionParams) (AIBridgeInterception, error)
InsertAIBridgeModelThought(ctx context.Context, arg InsertAIBridgeModelThoughtParams) (AIBridgeModelThought, error)
InsertAIBridgeTokenUsage(ctx context.Context, arg InsertAIBridgeTokenUsageParams) (AIBridgeTokenUsage, error)
@@ -961,7 +1012,14 @@ type sqlcQuerier interface {
InsertChatFile(ctx context.Context, arg InsertChatFileParams) (InsertChatFileRow, error)
InsertChatMessages(ctx context.Context, arg InsertChatMessagesParams) ([]ChatMessage, error)
InsertChatModelConfig(ctx context.Context, arg InsertChatModelConfigParams) (ChatModelConfig, error)
// Legacy queue insertion path. When no caller-supplied creator exists,
// preserve the created_by invariant by attributing the queued row to the
// chat owner.
InsertChatQueuedMessage(ctx context.Context, arg InsertChatQueuedMessageParams) (ChatQueuedMessage, error)
// Inserts a queued message that carries a position (from the default
// sequence) and an explicit created_by reference. Use this when the
// queued-message creator differs from the chat owner.
InsertChatQueuedMessageWithCreator(ctx context.Context, arg InsertChatQueuedMessageWithCreatorParams) (ChatQueuedMessage, error)
InsertCryptoKey(ctx context.Context, arg InsertCryptoKeyParams) (CryptoKey, error)
InsertCustomRole(ctx context.Context, arg InsertCustomRoleParams) (CustomRole, error)
InsertDBCryptKey(ctx context.Context, arg InsertDBCryptKeyParams) error
@@ -1041,6 +1099,11 @@ type sqlcQuerier interface {
InsertWorkspaceProxy(ctx context.Context, arg InsertWorkspaceProxyParams) (WorkspaceProxy, error)
InsertWorkspaceResource(ctx context.Context, arg InsertWorkspaceResourceParams) (WorkspaceResource, error)
InsertWorkspaceResourceMetadata(ctx context.Context, arg InsertWorkspaceResourceMetadataParams) ([]WorkspaceResourceMetadatum, error)
// Returns true when there is no heartbeat row for (chat_id, runner_id)
// or the existing row is older than @stale_seconds seconds by database
// time. chatstate calls this in a single query so the staleness check
// is atomic and does not depend on the caller's local clock.
IsChatHeartbeatStale(ctx context.Context, arg IsChatHeartbeatStaleParams) (bool, error)
// LinkChatFiles inserts file associations into the chat_file_links
// join table with deduplication (ON CONFLICT DO NOTHING). The INSERT
// is conditional: it only proceeds when the total number of links
@@ -1091,6 +1154,11 @@ type sqlcQuerier interface {
ListUserSecretsWithValues(ctx context.Context, userID uuid.UUID) ([]UserSecret, error)
ListUserSkillMetadataByUserID(ctx context.Context, userID uuid.UUID) ([]ListUserSkillMetadataByUserIDRow, error)
ListWorkspaceAgentPortShares(ctx context.Context, workspaceID uuid.UUID) ([]WorkspaceAgentPortShare, error)
// Locks the chat row with FOR UPDATE and atomically increments its
// snapshot_version, returning the post-bump chat. This is the single
// entry point ChatMachine.Update uses to acquire the row lock and
// allocate a new snapshot version in one round trip.
LockChatAndBumpSnapshotVersion(ctx context.Context, id uuid.UUID) (Chat, error)
MarkAllInboxNotificationsAsRead(ctx context.Context, arg MarkAllInboxNotificationsAsReadParams) error
OIDCClaimFieldValues(ctx context.Context, arg OIDCClaimFieldValuesParams) ([]string, error)
// OIDCClaimFields returns a list of distinct keys in the the merged_claims fields.
@@ -1115,6 +1183,9 @@ type sqlcQuerier interface {
// Mutates only created_at on the target row; ids are unchanged so
// consumers can keep tracking queued messages by id.
ReorderChatQueuedMessageToFront(ctx context.Context, arg ReorderChatQueuedMessageToFrontParams) (int64, error)
// Sets the target queued message's position to one less than the
// current minimum position for that chat, moving it to the head.
ReorderChatQueuedMessageToHead(ctx context.Context, arg ReorderChatQueuedMessageToHeadParams) (int64, error)
// Resolves the effective spend limit for a user using the hierarchy:
// 1. Individual user override (highest priority, applies globally across
// all organizations since it lives on the users table)
@@ -1216,6 +1287,11 @@ type sqlcQuerier interface {
// parameter keeps updated_at under the caller's clock, matching
// the injectable quartz.Clock used by FinalizeStale sweeps.
UpdateChatDebugStep(ctx context.Context, arg UpdateChatDebugStepParams) (ChatDebugStep, error)
// Atomically updates the execution-state-managed fields on a chat:
// status, archived, last_error, ownership identifiers, and the
// requires-action deadline. Callers compose this with transition
// mutations inside a single ChatMachine.Update transaction.
UpdateChatExecutionState(ctx context.Context, arg UpdateChatExecutionStateParams) (Chat, error)
// Bumps the heartbeat timestamp for the given set of chat IDs,
// provided they are still running and owned by the specified
// worker. Returns the IDs that were actually updated so the
@@ -1234,11 +1310,9 @@ type sqlcQuerier interface {
// Updates the cached last completed turn summary for sidebar display.
// Empty or whitespace-only summaries are stored as NULL here so direct
// query callers cannot accidentally persist blank sidebar text.
// This intentionally preserves updated_at. The staleness guard relies on
// every new-turn query, such as UpdateChatStatus and AcquireChats, bumping
// updated_at. Future chat-field updates that do not bump updated_at can let
// stale summaries persist. If this query ever bumps updated_at, later
// goroutine summary writes will be rejected as stale.
// This intentionally preserves updated_at. The staleness guard uses
// history_version so worker lifecycle transitions that do not change the
// active message history cannot reject final turn summary writes.
// Two summary workers using the same freshness marker are last-write-wins.
UpdateChatLastTurnSummary(ctx context.Context, arg UpdateChatLastTurnSummaryParams) (int64, error)
UpdateChatMCPServerIDs(ctx context.Context, arg UpdateChatMCPServerIDsParams) (Chat, error)
@@ -1246,6 +1320,9 @@ type sqlcQuerier interface {
UpdateChatModelConfig(ctx context.Context, arg UpdateChatModelConfigParams) (ChatModelConfig, error)
UpdateChatPinOrder(ctx context.Context, arg UpdateChatPinOrderParams) error
UpdateChatPlanModeByID(ctx context.Context, arg UpdateChatPlanModeByIDParams) (Chat, error)
// Stores the client-visible retry payload. retry_state_version is
// assigned by trigger from the current snapshot_version.
UpdateChatRetryState(ctx context.Context, arg UpdateChatRetryStateParams) (Chat, error)
UpdateChatStatus(ctx context.Context, arg UpdateChatStatusParams) (Chat, error)
UpdateChatStatusPreserveUpdatedAt(ctx context.Context, arg UpdateChatStatusPreserveUpdatedAtParams) (Chat, error)
UpdateChatTitleByID(ctx context.Context, arg UpdateChatTitleByIDParams) (Chat, error)
@@ -1396,6 +1473,9 @@ type sqlcQuerier interface {
UpsertChatDiffStatusReference(ctx context.Context, arg UpsertChatDiffStatusReferenceParams) (ChatDiffStatus, error)
UpsertChatExploreModelOverride(ctx context.Context, value string) error
UpsertChatGeneralModelOverride(ctx context.Context, value string) error
// Upserts a heartbeat row for the (chat_id, runner_id) lease. Uses
// database time so callers do not depend on a local clock.
UpsertChatHeartbeat(ctx context.Context, arg UpsertChatHeartbeatParams) error
UpsertChatIncludeDefaultSystemPrompt(ctx context.Context, includeDefaultSystemPrompt bool) error
// UpsertChatPersonalModelOverridesEnabled updates whether users may configure
// personal chat model overrides.
+55 -14
View File
@@ -11063,10 +11063,12 @@ func TestInsertChatMessages(t *testing.T) {
insertMessage := func(t *testing.T, store database.Store, ctx context.Context, chatID, userID, modelConfigID uuid.UUID, content string) {
t.Helper()
apiKey, _ := dbgen.APIKey(t, store, database.APIKey{ID: uuid.NewString(), UserID: userID})
_, err := store.InsertChatMessages(ctx, database.InsertChatMessagesParams{
ChatID: chatID,
CreatedBy: []uuid.UUID{userID},
APIKeyID: []string{apiKey.ID},
ModelConfigID: []uuid.UUID{modelConfigID},
Role: []database.ChatMessageRole{database.ChatMessageRoleUser},
ContentVersion: []int16{chatprompt.CurrentContentVersion},
@@ -11125,10 +11127,12 @@ func TestInsertChatMessages(t *testing.T) {
t.Parallel()
store, ctx, user, chat, _, modelConfigA := setupChat(t)
apiKey, _ := dbgen.APIKey(t, store, database.APIKey{ID: uuid.NewString(), UserID: user.ID})
msgs, err := store.InsertChatMessages(ctx, database.InsertChatMessagesParams{
ChatID: chat.ID,
CreatedBy: []uuid.UUID{user.ID, uuid.Nil, uuid.Nil},
APIKeyID: []string{apiKey.ID, "", ""},
ModelConfigID: []uuid.UUID{modelConfigA.ID, modelConfigA.ID, modelConfigA.ID},
Role: []database.ChatMessageRole{database.ChatMessageRoleUser, database.ChatMessageRoleAssistant, database.ChatMessageRoleTool},
ContentVersion: []int16{chatprompt.CurrentContentVersion, chatprompt.CurrentContentVersion, chatprompt.CurrentContentVersion},
@@ -12744,9 +12748,9 @@ func TestUpdateChatLastTurnSummary(t *testing.T) {
require.NoError(t, err)
affected, err := db.UpdateChatLastTurnSummary(ctx, database.UpdateChatLastTurnSummaryParams{
ID: chat.ID,
ExpectedUpdatedAt: chat.UpdatedAt,
LastTurnSummary: sql.NullString{String: "resolved the issue", Valid: true},
ID: chat.ID,
ExpectedHistoryVersion: chat.HistoryVersion,
LastTurnSummary: sql.NullString{String: "resolved the issue", Valid: true},
})
require.NoError(t, err)
require.EqualValues(t, 1, affected)
@@ -12757,9 +12761,9 @@ func TestUpdateChatLastTurnSummary(t *testing.T) {
require.Equal(t, chat.UpdatedAt, fetched.UpdatedAt)
affected, err = db.UpdateChatLastTurnSummary(ctx, database.UpdateChatLastTurnSummaryParams{
ID: chat.ID,
ExpectedUpdatedAt: chat.UpdatedAt,
LastTurnSummary: sql.NullString{String: " \n\t ", Valid: true},
ID: chat.ID,
ExpectedHistoryVersion: chat.HistoryVersion,
LastTurnSummary: sql.NullString{String: " \n\t ", Valid: true},
})
require.NoError(t, err)
require.EqualValues(t, 1, affected)
@@ -12770,9 +12774,9 @@ func TestUpdateChatLastTurnSummary(t *testing.T) {
require.Equal(t, chat.UpdatedAt, fetched.UpdatedAt)
affected, err = db.UpdateChatLastTurnSummary(ctx, database.UpdateChatLastTurnSummaryParams{
ID: chat.ID,
ExpectedUpdatedAt: chat.UpdatedAt,
LastTurnSummary: sql.NullString{String: "fresh summary", Valid: true},
ID: chat.ID,
ExpectedHistoryVersion: chat.HistoryVersion,
LastTurnSummary: sql.NullString{String: "fresh summary", Valid: true},
})
require.NoError(t, err)
require.EqualValues(t, 1, affected)
@@ -12786,17 +12790,54 @@ func TestUpdateChatLastTurnSummary(t *testing.T) {
require.NoError(t, err)
affected, err = db.UpdateChatLastTurnSummary(ctx, database.UpdateChatLastTurnSummaryParams{
ID: chat.ID,
ExpectedUpdatedAt: chat.UpdatedAt,
LastTurnSummary: sql.NullString{String: "stale summary", Valid: true},
ID: chat.ID,
ExpectedHistoryVersion: chat.HistoryVersion,
LastTurnSummary: sql.NullString{String: "still fresh summary", 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: "still fresh summary", Valid: true}, fetched.LastTurnSummary)
require.Equal(t, advancedUpdatedAt, fetched.UpdatedAt)
_, 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},
ProviderResponseID: []string{""},
})
require.NoError(t, err)
affected, err = db.UpdateChatLastTurnSummary(ctx, database.UpdateChatLastTurnSummaryParams{
ID: chat.ID,
ExpectedHistoryVersion: chat.HistoryVersion,
LastTurnSummary: sql.NullString{String: "stale 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 summary", Valid: true}, fetched.LastTurnSummary)
require.Equal(t, advancedUpdatedAt, fetched.UpdatedAt)
require.Equal(t, sql.NullString{String: "still fresh summary", Valid: true}, fetched.LastTurnSummary)
require.NotEqual(t, chat.HistoryVersion, fetched.HistoryVersion)
}
func TestDeleteChatDebugDataAfterMessageIDIncludesTriggeredRuns(t *testing.T) {
+1825 -111
View File
File diff suppressed because it is too large Load Diff
+635 -12
View File
@@ -35,6 +35,14 @@ chats_expanded AS (
updated_chats.plan_mode,
updated_chats.client_type,
updated_chats.last_turn_summary,
updated_chats.snapshot_version,
updated_chats.history_version,
updated_chats.queue_version,
updated_chats.generation_attempt,
updated_chats.retry_state,
updated_chats.retry_state_version,
updated_chats.runner_id,
updated_chats.requires_action_deadline_at,
COALESCE(root.user_acl, updated_chats.user_acl) AS user_acl,
COALESCE(root.group_acl, updated_chats.group_acl) AS group_acl,
owner.username AS owner_username,
@@ -90,6 +98,14 @@ chats_expanded AS (
updated_chats.plan_mode,
updated_chats.client_type,
updated_chats.last_turn_summary,
updated_chats.snapshot_version,
updated_chats.history_version,
updated_chats.queue_version,
updated_chats.generation_attempt,
updated_chats.retry_state,
updated_chats.retry_state_version,
updated_chats.runner_id,
updated_chats.requires_action_deadline_at,
COALESCE(root.user_acl, updated_chats.user_acl) AS user_acl,
COALESCE(root.group_acl, updated_chats.group_acl) AS group_acl,
owner.username AS owner_username,
@@ -293,6 +309,15 @@ SELECT *
FROM chats_expanded
WHERE id = @id::uuid;
-- name: GetChatFamilyIDsByRootID :many
-- Returns the chat IDs of every chat in a family (root + all children)
-- in deterministic order. The id parameter must be the root id; the
-- query does not walk up from a child.
SELECT id
FROM chats
WHERE id = @id::uuid OR root_chat_id = @id::uuid
ORDER BY (id = @id::uuid) DESC, created_at ASC, id ASC;
-- name: GetChatACLByID :one
SELECT
user_acl AS users,
@@ -333,6 +358,18 @@ WHERE
ORDER BY
created_at ASC;
-- name: GetChatMessagesByRevisionForStream :many
SELECT
*
FROM
chat_messages
WHERE
chat_id = @chat_id::uuid
AND revision > @after_revision::bigint
AND visibility IN ('user', 'both')
ORDER BY
created_at ASC, id ASC;
-- name: GetChatMessagesByChatIDAscPaginated :many
SELECT
*
@@ -723,6 +760,14 @@ chats_expanded AS (
inserted_chat.plan_mode,
inserted_chat.client_type,
inserted_chat.last_turn_summary,
inserted_chat.snapshot_version,
inserted_chat.history_version,
inserted_chat.queue_version,
inserted_chat.generation_attempt,
inserted_chat.retry_state,
inserted_chat.retry_state_version,
inserted_chat.runner_id,
inserted_chat.requires_action_deadline_at,
COALESCE(root.user_acl, inserted_chat.user_acl) AS user_acl,
COALESCE(root.group_acl, inserted_chat.group_acl) AS group_acl,
owner.username AS owner_username,
@@ -860,6 +905,14 @@ chats_expanded AS (
updated_chat.plan_mode,
updated_chat.client_type,
updated_chat.last_turn_summary,
updated_chat.snapshot_version,
updated_chat.history_version,
updated_chat.queue_version,
updated_chat.generation_attempt,
updated_chat.retry_state,
updated_chat.retry_state_version,
updated_chat.runner_id,
updated_chat.requires_action_deadline_at,
COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
owner.username AS owner_username,
@@ -915,6 +968,14 @@ chats_expanded AS (
updated_chat.plan_mode,
updated_chat.client_type,
updated_chat.last_turn_summary,
updated_chat.snapshot_version,
updated_chat.history_version,
updated_chat.queue_version,
updated_chat.generation_attempt,
updated_chat.retry_state,
updated_chat.retry_state_version,
updated_chat.runner_id,
updated_chat.requires_action_deadline_at,
COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
owner.username AS owner_username,
@@ -968,6 +1029,14 @@ chats_expanded AS (
updated_chat.plan_mode,
updated_chat.client_type,
updated_chat.last_turn_summary,
updated_chat.snapshot_version,
updated_chat.history_version,
updated_chat.queue_version,
updated_chat.generation_attempt,
updated_chat.retry_state,
updated_chat.retry_state_version,
updated_chat.runner_id,
updated_chat.requires_action_deadline_at,
COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
owner.username AS owner_username,
@@ -1021,6 +1090,14 @@ chats_expanded AS (
updated_chat.plan_mode,
updated_chat.client_type,
updated_chat.last_turn_summary,
updated_chat.snapshot_version,
updated_chat.history_version,
updated_chat.queue_version,
updated_chat.generation_attempt,
updated_chat.retry_state,
updated_chat.retry_state_version,
updated_chat.runner_id,
updated_chat.requires_action_deadline_at,
COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
owner.username AS owner_username,
@@ -1074,6 +1151,14 @@ chats_expanded AS (
updated_chat.plan_mode,
updated_chat.client_type,
updated_chat.last_turn_summary,
updated_chat.snapshot_version,
updated_chat.history_version,
updated_chat.queue_version,
updated_chat.generation_attempt,
updated_chat.retry_state,
updated_chat.retry_state_version,
updated_chat.runner_id,
updated_chat.requires_action_deadline_at,
COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
owner.username AS owner_username,
@@ -1126,6 +1211,14 @@ chats_expanded AS (
updated_chat.plan_mode,
updated_chat.client_type,
updated_chat.last_turn_summary,
updated_chat.snapshot_version,
updated_chat.history_version,
updated_chat.queue_version,
updated_chat.generation_attempt,
updated_chat.retry_state,
updated_chat.retry_state_version,
updated_chat.runner_id,
updated_chat.requires_action_deadline_at,
COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
owner.username AS owner_username,
@@ -1178,6 +1271,14 @@ chats_expanded AS (
updated_chat.plan_mode,
updated_chat.client_type,
updated_chat.last_turn_summary,
updated_chat.snapshot_version,
updated_chat.history_version,
updated_chat.queue_version,
updated_chat.generation_attempt,
updated_chat.retry_state,
updated_chat.retry_state_version,
updated_chat.runner_id,
updated_chat.requires_action_deadline_at,
COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
owner.username AS owner_username,
@@ -1232,6 +1333,14 @@ chats_expanded AS (
updated_chat.plan_mode,
updated_chat.client_type,
updated_chat.last_turn_summary,
updated_chat.snapshot_version,
updated_chat.history_version,
updated_chat.queue_version,
updated_chat.generation_attempt,
updated_chat.retry_state,
updated_chat.retry_state_version,
updated_chat.runner_id,
updated_chat.requires_action_deadline_at,
COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
owner.username AS owner_username,
@@ -1248,11 +1357,9 @@ FROM chats_expanded;
-- Updates the cached last completed turn summary for sidebar display.
-- Empty or whitespace-only summaries are stored as NULL here so direct
-- query callers cannot accidentally persist blank sidebar text.
-- This intentionally preserves updated_at. The staleness guard relies on
-- every new-turn query, such as UpdateChatStatus and AcquireChats, bumping
-- updated_at. Future chat-field updates that do not bump updated_at can let
-- stale summaries persist. If this query ever bumps updated_at, later
-- goroutine summary writes will be rejected as stale.
-- This intentionally preserves updated_at. The staleness guard uses
-- history_version so worker lifecycle transitions that do not change the
-- active message history cannot reject final turn summary writes.
-- Two summary workers using the same freshness marker are last-write-wins.
UPDATE chats
SET
@@ -1261,7 +1368,7 @@ SET
), '')
WHERE
id = @id::uuid
AND updated_at = @expected_updated_at::timestamptz;
AND history_version = @expected_history_version::bigint;
-- name: UpdateChatMCPServerIDs :one
WITH updated_chat AS (
@@ -1304,6 +1411,14 @@ chats_expanded AS (
updated_chat.plan_mode,
updated_chat.client_type,
updated_chat.last_turn_summary,
updated_chat.snapshot_version,
updated_chat.history_version,
updated_chat.queue_version,
updated_chat.generation_attempt,
updated_chat.retry_state,
updated_chat.retry_state_version,
updated_chat.runner_id,
updated_chat.requires_action_deadline_at,
COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
owner.username AS owner_username,
@@ -1413,6 +1528,14 @@ chats_expanded AS (
acquired_chats.plan_mode,
acquired_chats.client_type,
acquired_chats.last_turn_summary,
acquired_chats.snapshot_version,
acquired_chats.history_version,
acquired_chats.queue_version,
acquired_chats.generation_attempt,
acquired_chats.retry_state,
acquired_chats.retry_state_version,
acquired_chats.runner_id,
acquired_chats.requires_action_deadline_at,
COALESCE(root.user_acl, acquired_chats.user_acl) AS user_acl,
COALESCE(root.group_acl, acquired_chats.group_acl) AS group_acl,
owner.username AS owner_username,
@@ -1470,6 +1593,14 @@ chats_expanded AS (
updated_chat.plan_mode,
updated_chat.client_type,
updated_chat.last_turn_summary,
updated_chat.snapshot_version,
updated_chat.history_version,
updated_chat.queue_version,
updated_chat.generation_attempt,
updated_chat.retry_state,
updated_chat.retry_state_version,
updated_chat.runner_id,
updated_chat.requires_action_deadline_at,
COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
owner.username AS owner_username,
@@ -1527,6 +1658,14 @@ chats_expanded AS (
updated_chat.plan_mode,
updated_chat.client_type,
updated_chat.last_turn_summary,
updated_chat.snapshot_version,
updated_chat.history_version,
updated_chat.queue_version,
updated_chat.generation_attempt,
updated_chat.retry_state,
updated_chat.retry_state_version,
updated_chat.runner_id,
updated_chat.requires_action_deadline_at,
COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
owner.username AS owner_username,
@@ -1694,13 +1833,18 @@ RETURNING
*;
-- name: InsertChatQueuedMessage :one
INSERT INTO chat_queued_messages (chat_id, content, model_config_id, api_key_id)
VALUES (
@chat_id,
@content,
-- Legacy queue insertion path. When no caller-supplied creator exists,
-- preserve the created_by invariant by attributing the queued row to the
-- chat owner.
INSERT INTO chat_queued_messages (chat_id, content, model_config_id, api_key_id, created_by)
SELECT
@chat_id::uuid,
@content::jsonb,
sqlc.narg('model_config_id')::uuid,
sqlc.narg('api_key_id')::text
)
sqlc.narg('api_key_id')::text,
chats.owner_id
FROM chats
WHERE chats.id = @chat_id::uuid
RETURNING *;
-- name: GetChatQueuedMessages :many
@@ -1786,6 +1930,14 @@ chats_expanded AS (
locked_chat.plan_mode,
locked_chat.client_type,
locked_chat.last_turn_summary,
locked_chat.snapshot_version,
locked_chat.history_version,
locked_chat.queue_version,
locked_chat.generation_attempt,
locked_chat.retry_state,
locked_chat.retry_state_version,
locked_chat.runner_id,
locked_chat.requires_action_deadline_at,
COALESCE(root.user_acl, locked_chat.user_acl) AS user_acl,
COALESCE(root.group_acl, locked_chat.group_acl) AS group_acl,
owner.username AS owner_username,
@@ -1798,6 +1950,63 @@ chats_expanded AS (
SELECT *
FROM chats_expanded;
-- name: GetChatByIDForShare :one
WITH shared_chat AS (
SELECT *
FROM chats
WHERE id = @id::uuid
FOR SHARE
),
chats_expanded AS (
SELECT
shared_chat.id,
shared_chat.owner_id,
shared_chat.workspace_id,
shared_chat.title,
shared_chat.status,
shared_chat.worker_id,
shared_chat.started_at,
shared_chat.heartbeat_at,
shared_chat.created_at,
shared_chat.updated_at,
shared_chat.parent_chat_id,
shared_chat.root_chat_id,
shared_chat.last_model_config_id,
shared_chat.archived,
shared_chat.last_error,
shared_chat.mode,
shared_chat.mcp_server_ids,
shared_chat.labels,
shared_chat.build_id,
shared_chat.agent_id,
shared_chat.pin_order,
shared_chat.last_read_message_id,
shared_chat.last_injected_context,
shared_chat.dynamic_tools,
shared_chat.organization_id,
shared_chat.plan_mode,
shared_chat.client_type,
shared_chat.last_turn_summary,
shared_chat.snapshot_version,
shared_chat.history_version,
shared_chat.queue_version,
shared_chat.generation_attempt,
shared_chat.retry_state,
shared_chat.retry_state_version,
shared_chat.runner_id,
shared_chat.requires_action_deadline_at,
COALESCE(root.user_acl, shared_chat.user_acl) AS user_acl,
COALESCE(root.group_acl, shared_chat.group_acl) AS group_acl,
owner.username AS owner_username,
owner.name AS owner_name
FROM
shared_chat
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 *
FROM chats_expanded;
-- name: GetChatsByChatFileID :many
SELECT
*
@@ -2317,6 +2526,420 @@ WHERE chat_id = @chat_id::uuid
AND deleted = false
AND content::jsonb @> '[{"type": "context-file"}]';
-- name: GetChatWorkerAcquisitionCandidates :many
-- Returns chats that workers may try to acquire. Candidates must be:
-- - in a worker-runnable execution status;
-- - unarchived; and
-- - missing ownership, carrying inconsistent ownership, or lacking a
-- fresh heartbeat for the assigned runner.
--
-- Missing ownership is worker_id IS NULL. Inconsistent ownership is
-- runner_id IS NULL while worker_id is set. Stale ownership is no
-- heartbeat row for (chat_id, runner_id), or one older than
-- @stale_seconds by database time. Candidates are ordered by oldest
-- updated_at first so workers drain stale runnable chats predictably.
SELECT
chats_expanded.*,
chat_heartbeats.heartbeat_at AS current_heartbeat_at,
NOT EXISTS (
SELECT 1
FROM chat_heartbeats current_lease
WHERE current_lease.chat_id = chats_expanded.id
AND current_lease.runner_id = chats_expanded.runner_id
AND current_lease.heartbeat_at > NOW() - (INTERVAL '1 second' * @stale_seconds::int)
) AS heartbeat_stale
FROM chats_expanded
LEFT JOIN chat_heartbeats
ON chat_heartbeats.chat_id = chats_expanded.id
AND chat_heartbeats.runner_id = chats_expanded.runner_id
WHERE
chats_expanded.status IN ('running'::chat_status, 'interrupting'::chat_status, 'requires_action'::chat_status)
AND chats_expanded.archived = false
AND (
chats_expanded.worker_id IS NULL
OR chats_expanded.runner_id IS NULL
OR NOT EXISTS (
SELECT 1
FROM chat_heartbeats current_lease
WHERE current_lease.chat_id = chats_expanded.id
AND current_lease.runner_id = chats_expanded.runner_id
AND current_lease.heartbeat_at > NOW() - (INTERVAL '1 second' * @stale_seconds::int)
)
)
ORDER BY chats_expanded.updated_at ASC, chats_expanded.id ASC
LIMIT @limit_count::int;
-- name: GetChatsByIDsForRunnerSync :many
SELECT *
FROM chats_expanded
WHERE id = ANY(@ids::uuid[])
ORDER BY id ASC;
-- name: BatchUpsertChatHeartbeats :exec
INSERT INTO chat_heartbeats (chat_id, runner_id, heartbeat_at)
SELECT chat_ids.chat_id, runner_ids.runner_id, NOW()
FROM unnest(@chat_ids::uuid[]) WITH ORDINALITY AS chat_ids(chat_id, ord)
JOIN unnest(@runner_ids::uuid[]) WITH ORDINALITY AS runner_ids(runner_id, ord) USING (ord)
ON CONFLICT (chat_id, runner_id) DO UPDATE
SET heartbeat_at = EXCLUDED.heartbeat_at;
-- name: DeleteStaleChatHeartbeats :execrows
DELETE FROM chat_heartbeats
WHERE heartbeat_at < NOW() - (INTERVAL '1 second' * @stale_seconds::int);
-- name: GetAutoArchiveInactiveChatCandidates :many
-- Returns read-only root chat candidates for state-machine-backed
-- auto-archive. Activity is computed across the root family. The query
-- limits roots, not total family members.
SELECT
chats_expanded.*,
COALESCE(activity.last_activity_at, chats_expanded.created_at)::timestamptz AS last_activity_at
FROM chats_expanded
LEFT JOIN LATERAL (
SELECT MAX(chat_messages.created_at) AS last_activity_at
FROM chat_messages
JOIN chats family_chat ON family_chat.id = chat_messages.chat_id
WHERE (family_chat.id = chats_expanded.id OR family_chat.root_chat_id = chats_expanded.id)
AND chat_messages.deleted = false
) activity ON TRUE
WHERE
chats_expanded.archived = false
AND chats_expanded.pin_order = 0
AND chats_expanded.parent_chat_id IS NULL
AND chats_expanded.created_at < @archive_cutoff::timestamptz
AND chats_expanded.status NOT IN (
'running'::chat_status,
'interrupting'::chat_status,
'pending'::chat_status,
'paused'::chat_status,
'requires_action'::chat_status
)
AND COALESCE(activity.last_activity_at, chats_expanded.created_at) < @archive_cutoff::timestamptz
ORDER BY chats_expanded.created_at ASC
LIMIT @limit_count::int;
-- name: LockChatAndBumpSnapshotVersion :one
-- Locks the chat row with FOR UPDATE and atomically increments its
-- snapshot_version, returning the post-bump chat. This is the single
-- entry point ChatMachine.Update uses to acquire the row lock and
-- allocate a new snapshot version in one round trip.
WITH bumped_chat AS (
UPDATE chats
SET snapshot_version = snapshot_version + 1
WHERE id = (
SELECT id FROM chats
WHERE id = @id::uuid
FOR UPDATE
)
RETURNING *
),
chats_expanded AS (
SELECT
bumped_chat.id,
bumped_chat.owner_id,
bumped_chat.workspace_id,
bumped_chat.title,
bumped_chat.status,
bumped_chat.worker_id,
bumped_chat.started_at,
bumped_chat.heartbeat_at,
bumped_chat.created_at,
bumped_chat.updated_at,
bumped_chat.parent_chat_id,
bumped_chat.root_chat_id,
bumped_chat.last_model_config_id,
bumped_chat.archived,
bumped_chat.last_error,
bumped_chat.mode,
bumped_chat.mcp_server_ids,
bumped_chat.labels,
bumped_chat.build_id,
bumped_chat.agent_id,
bumped_chat.pin_order,
bumped_chat.last_read_message_id,
bumped_chat.last_injected_context,
bumped_chat.dynamic_tools,
bumped_chat.organization_id,
bumped_chat.plan_mode,
bumped_chat.client_type,
bumped_chat.last_turn_summary,
bumped_chat.snapshot_version,
bumped_chat.history_version,
bumped_chat.queue_version,
bumped_chat.generation_attempt,
bumped_chat.retry_state,
bumped_chat.retry_state_version,
bumped_chat.runner_id,
bumped_chat.requires_action_deadline_at,
COALESCE(root.user_acl, bumped_chat.user_acl) AS user_acl,
COALESCE(root.group_acl, bumped_chat.group_acl) AS group_acl,
owner.username AS owner_username,
owner.name AS owner_name
FROM bumped_chat
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 *
FROM chats_expanded;
-- name: UpdateChatExecutionState :one
-- Atomically updates the execution-state-managed fields on a chat:
-- status, archived, last_error, ownership identifiers, and the
-- requires-action deadline. Callers compose this with transition
-- mutations inside a single ChatMachine.Update transaction.
WITH updated_chat AS (
UPDATE chats
SET
status = @status::chat_status,
archived = @archived::boolean,
worker_id = sqlc.narg('worker_id')::uuid,
runner_id = sqlc.narg('runner_id')::uuid,
last_error = sqlc.narg('last_error')::jsonb,
requires_action_deadline_at = sqlc.narg('requires_action_deadline_at')::timestamptz,
pin_order = CASE WHEN @archived::boolean THEN 0 ELSE pin_order END,
updated_at = NOW()
WHERE id = @id::uuid
RETURNING *
),
chats_expanded AS (
SELECT
updated_chat.id,
updated_chat.owner_id,
updated_chat.workspace_id,
updated_chat.title,
updated_chat.status,
updated_chat.worker_id,
updated_chat.started_at,
updated_chat.heartbeat_at,
updated_chat.created_at,
updated_chat.updated_at,
updated_chat.parent_chat_id,
updated_chat.root_chat_id,
updated_chat.last_model_config_id,
updated_chat.archived,
updated_chat.last_error,
updated_chat.mode,
updated_chat.mcp_server_ids,
updated_chat.labels,
updated_chat.build_id,
updated_chat.agent_id,
updated_chat.pin_order,
updated_chat.last_read_message_id,
updated_chat.last_injected_context,
updated_chat.dynamic_tools,
updated_chat.organization_id,
updated_chat.plan_mode,
updated_chat.client_type,
updated_chat.last_turn_summary,
updated_chat.snapshot_version,
updated_chat.history_version,
updated_chat.queue_version,
updated_chat.generation_attempt,
updated_chat.retry_state,
updated_chat.retry_state_version,
updated_chat.runner_id,
updated_chat.requires_action_deadline_at,
COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
owner.username AS owner_username,
owner.name AS owner_name
FROM updated_chat
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 *
FROM chats_expanded;
-- name: UpdateChatRetryState :one
-- Stores the client-visible retry payload. retry_state_version is
-- assigned by trigger from the current snapshot_version.
WITH updated_chat AS (
UPDATE chats
SET
retry_state = @retry_state::jsonb,
updated_at = NOW()
WHERE id = @id::uuid
RETURNING *
),
chats_expanded AS (
SELECT
updated_chat.id,
updated_chat.owner_id,
updated_chat.workspace_id,
updated_chat.title,
updated_chat.status,
updated_chat.worker_id,
updated_chat.started_at,
updated_chat.heartbeat_at,
updated_chat.created_at,
updated_chat.updated_at,
updated_chat.parent_chat_id,
updated_chat.root_chat_id,
updated_chat.last_model_config_id,
updated_chat.archived,
updated_chat.last_error,
updated_chat.mode,
updated_chat.mcp_server_ids,
updated_chat.labels,
updated_chat.build_id,
updated_chat.agent_id,
updated_chat.pin_order,
updated_chat.last_read_message_id,
updated_chat.last_injected_context,
updated_chat.dynamic_tools,
updated_chat.organization_id,
updated_chat.plan_mode,
updated_chat.client_type,
updated_chat.last_turn_summary,
updated_chat.snapshot_version,
updated_chat.history_version,
updated_chat.queue_version,
updated_chat.generation_attempt,
updated_chat.retry_state,
updated_chat.retry_state_version,
updated_chat.runner_id,
updated_chat.requires_action_deadline_at,
COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
owner.username AS owner_username,
owner.name AS owner_name
FROM updated_chat
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 *
FROM chats_expanded;
-- name: IncrementChatGenerationAttempt :one
-- Increments generation_attempt and returns the resulting value.
UPDATE chats
SET generation_attempt = generation_attempt + 1, updated_at = NOW()
WHERE id = @id::uuid
RETURNING generation_attempt;
-- name: GetDatabaseNow :one
-- Returns the current database timestamp. Used so transitions that
-- record deadlines or heartbeats rely on a clock that is consistent
-- with the database rather than the caller's local clock.
SELECT NOW()::timestamptz AS now;
-- name: InsertChatQueuedMessageWithCreator :one
-- Inserts a queued message that carries a position (from the default
-- sequence) and an explicit created_by reference. Use this when the
-- queued-message creator differs from the chat owner.
INSERT INTO chat_queued_messages (chat_id, content, model_config_id, api_key_id, created_by)
VALUES (
@chat_id::uuid,
@content::jsonb,
sqlc.narg('model_config_id')::uuid,
sqlc.narg('api_key_id')::text,
@created_by::uuid
)
RETURNING *;
-- name: GetChatQueuedMessagesByPosition :many
-- Returns queued messages in state-machine order (position ASC, id ASC).
SELECT * FROM chat_queued_messages
WHERE chat_id = @chat_id::uuid
ORDER BY position ASC, id ASC;
-- name: CountChatQueuedMessages :one
-- Cheap queue-length check used by ChatMachine.Update when deciding
-- whether the chat is in a "1" sub-state.
SELECT COUNT(*)::bigint AS count
FROM chat_queued_messages
WHERE chat_id = @chat_id::uuid;
-- name: GetChatQueuedMessageHead :one
-- Returns the queue head (lowest position, then lowest id).
SELECT * FROM chat_queued_messages
WHERE chat_id = @chat_id::uuid
ORDER BY position ASC, id ASC
LIMIT 1;
-- name: GetChatQueuedMessageByID :one
SELECT * FROM chat_queued_messages
WHERE id = @id::bigint AND chat_id = @chat_id::uuid;
-- name: DeleteChatQueuedMessageReturningCount :execrows
-- Deletes a queued message, scoped to the parent chat. Returns the
-- number of affected rows so callers can detect missing rows without
-- a follow-up read.
DELETE FROM chat_queued_messages
WHERE id = @id::bigint AND chat_id = @chat_id::uuid;
-- name: DeleteAllChatQueuedMessagesReturningCount :execrows
DELETE FROM chat_queued_messages
WHERE chat_id = @chat_id::uuid;
-- name: ReorderChatQueuedMessageToHead :execrows
-- Sets the target queued message's position to one less than the
-- current minimum position for that chat, moving it to the head.
UPDATE chat_queued_messages AS target
SET position = COALESCE(
(SELECT MIN(position) FROM chat_queued_messages WHERE chat_id = @chat_id::uuid),
0
) - 1
WHERE target.id = @id::bigint
AND target.chat_id = @chat_id::uuid
AND target.position > COALESCE(
(SELECT MIN(position) FROM chat_queued_messages WHERE chat_id = @chat_id::uuid),
target.position
);
-- name: UpsertChatHeartbeat :exec
-- Upserts a heartbeat row for the (chat_id, runner_id) lease. Uses
-- database time so callers do not depend on a local clock.
INSERT INTO chat_heartbeats (chat_id, runner_id, heartbeat_at)
VALUES (@chat_id::uuid, @runner_id::uuid, NOW())
ON CONFLICT (chat_id, runner_id) DO UPDATE
SET heartbeat_at = EXCLUDED.heartbeat_at;
-- name: GetChatHeartbeat :one
SELECT * FROM chat_heartbeats
WHERE chat_id = @chat_id::uuid AND runner_id = @runner_id::uuid;
-- name: IsChatHeartbeatStale :one
-- Returns true when there is no heartbeat row for (chat_id, runner_id)
-- or the existing row is older than @stale_seconds seconds by database
-- time. chatstate calls this in a single query so the staleness check
-- is atomic and does not depend on the caller's local clock.
SELECT NOT EXISTS (
SELECT 1 FROM chat_heartbeats
WHERE chat_id = @chat_id::uuid
AND runner_id = @runner_id::uuid
AND heartbeat_at > NOW() - (INTERVAL '1 second' * @stale_seconds::int)
) AS stale;
-- name: BatchDeleteChatHeartbeats :execrows
-- Deletes heartbeat rows for the supplied (chat_id, runner_id) pairs.
DELETE FROM chat_heartbeats
USING unnest(@chat_ids::uuid[]) WITH ORDINALITY AS chat_ids(chat_id, ord)
JOIN unnest(@runner_ids::uuid[]) WITH ORDINALITY AS runner_ids(runner_id, ord) USING (ord)
WHERE chat_heartbeats.chat_id = chat_ids.chat_id
AND chat_heartbeats.runner_id = runner_ids.runner_id;
-- name: DeleteAllChatHeartbeats :exec
-- Deletes all heartbeat rows for the chat. Used during ownership
-- transitions that abandon a lease.
DELETE FROM chat_heartbeats WHERE chat_id = @chat_id::uuid;
-- name: GetChatStreamSyncRows :many
SELECT
id,
snapshot_version,
history_version,
queue_version,
retry_state_version,
generation_attempt,
status,
worker_id
FROM chats
WHERE id = ANY(@ids::uuid[])
ORDER BY id ASC;
-- name: AutoArchiveInactiveChats :many
-- Archives inactive root chats (pinned and already-archived chats skipped),
-- cascading to children via root_chat_id. Limits apply to roots, not total
+1
View File
@@ -26,6 +26,7 @@ const (
UniqueChatDiffStatusesPkey UniqueConstraint = "chat_diff_statuses_pkey" // ALTER TABLE ONLY chat_diff_statuses ADD CONSTRAINT chat_diff_statuses_pkey PRIMARY KEY (chat_id);
UniqueChatFileLinksChatIDFileIDKey UniqueConstraint = "chat_file_links_chat_id_file_id_key" // ALTER TABLE ONLY chat_file_links ADD CONSTRAINT chat_file_links_chat_id_file_id_key UNIQUE (chat_id, file_id);
UniqueChatFilesPkey UniqueConstraint = "chat_files_pkey" // ALTER TABLE ONLY chat_files ADD CONSTRAINT chat_files_pkey PRIMARY KEY (id);
UniqueChatHeartbeatsPkey UniqueConstraint = "chat_heartbeats_pkey" // ALTER TABLE ONLY chat_heartbeats ADD CONSTRAINT chat_heartbeats_pkey PRIMARY KEY (chat_id, runner_id);
UniqueChatMessagesPkey UniqueConstraint = "chat_messages_pkey" // ALTER TABLE ONLY chat_messages ADD CONSTRAINT chat_messages_pkey PRIMARY KEY (id);
UniqueChatModelConfigsPkey UniqueConstraint = "chat_model_configs_pkey" // ALTER TABLE ONLY chat_model_configs ADD CONSTRAINT chat_model_configs_pkey PRIMARY KEY (id);
UniqueChatQueuedMessagesPkey UniqueConstraint = "chat_queued_messages_pkey" // ALTER TABLE ONLY chat_queued_messages ADD CONSTRAINT chat_queued_messages_pkey PRIMARY KEY (id);