diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index d084514dd8..78e7a6fc6b 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -1845,6 +1845,10 @@ 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) { + panic("not implemented") +} + 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) @@ -1942,6 +1946,10 @@ 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 { + panic("not implemented") +} + func (q *querier) DeleteAllChatQueuedMessages(ctx context.Context, chatID uuid.UUID) error { chat, err := q.db.GetChatByID(ctx, chatID) if err != nil { @@ -1953,6 +1961,10 @@ 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) { + panic("not implemented") +} + 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 @@ -1999,6 +2011,10 @@ func (q *querier) DeleteChatDebugDataByChatID(ctx context.Context, arg database. return q.db.DeleteChatDebugDataByChatID(ctx, arg) } +func (q *querier) DeleteChatHeartbeats(ctx context.Context, arg database.DeleteChatHeartbeatsParams) (int64, error) { + panic("not implemented") +} + func (q *querier) DeleteChatModelConfigByID(ctx context.Context, id uuid.UUID) error { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceDeploymentConfig); err != nil { return err @@ -2031,6 +2047,10 @@ 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) { + panic("not implemented") +} + func (q *querier) DeleteChatUsageLimitGroupOverride(ctx context.Context, groupID uuid.UUID) error { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceDeploymentConfig); err != nil { return err @@ -2303,6 +2323,10 @@ 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) { + panic("not implemented") +} + 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 @@ -2814,6 +2838,10 @@ 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) { + panic("not implemented") +} + 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 @@ -2865,6 +2893,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) { + panic("not implemented") +} + func (q *querier) GetChatByIDForUpdate(ctx context.Context, id uuid.UUID) (database.Chat, error) { return fetch(q.log, q.auth, q.db.GetChatByIDForUpdate)(ctx, id) } @@ -3036,6 +3068,10 @@ 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) { + panic("not implemented") +} + func (q *querier) GetChatFileByID(ctx context.Context, id uuid.UUID) (database.ChatFile, error) { file, err := q.db.GetChatFileByID(ctx, id) if err != nil { @@ -3102,6 +3138,10 @@ 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) { + panic("not implemented") +} + 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 @@ -3162,6 +3202,10 @@ 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) { + panic("not implemented") +} + func (q *querier) GetChatMessagesForPromptByChatID(ctx context.Context, chatID uuid.UUID) ([]database.ChatMessage, error) { // Authorize read on the parent chat. _, err := q.GetChatByID(ctx, chatID) @@ -3210,6 +3254,14 @@ 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) { + panic("not implemented") +} + +func (q *querier) GetChatQueuedMessageHead(ctx context.Context, chatID uuid.UUID) (database.ChatQueuedMessage, error) { + panic("not implemented") +} + func (q *querier) GetChatQueuedMessages(ctx context.Context, chatID uuid.UUID) ([]database.ChatQueuedMessage, error) { _, err := q.GetChatByID(ctx, chatID) if err != nil { @@ -3218,6 +3270,10 @@ 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) { + panic("not implemented") +} + 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. @@ -3227,6 +3283,10 @@ 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) { + panic("not implemented") +} + 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 @@ -3299,6 +3359,10 @@ 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) { + panic("not implemented") +} + 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 @@ -3321,6 +3385,10 @@ 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) { + panic("not implemented") +} + func (q *querier) GetChatsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]database.Chat, error) { return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetChatsByWorkspaceIDs)(ctx, ids) } @@ -3391,6 +3459,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) { + panic("not implemented") +} + 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. @@ -5435,6 +5507,10 @@ 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) { + panic("not implemented") +} + 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) } @@ -5605,6 +5681,10 @@ 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) { + panic("not implemented") +} + 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 @@ -6163,6 +6243,10 @@ 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) { + panic("not implemented") +} + func (q *querier) LinkChatFiles(ctx context.Context, arg database.LinkChatFilesParams) (int32, error) { chat, err := q.db.GetChatByID(ctx, arg.ChatID) if err != nil { @@ -6355,6 +6439,10 @@ 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) { + panic("not implemented") +} + func (q *querier) MarkAllInboxNotificationsAsRead(ctx context.Context, arg database.MarkAllInboxNotificationsAsReadParams) error { resource := rbac.ResourceInboxNotification.WithOwner(arg.UserID.String()) @@ -6461,6 +6549,10 @@ 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) { + panic("not implemented") +} + 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 @@ -6703,6 +6795,10 @@ 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) { + panic("not implemented") +} + 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 @@ -6825,6 +6921,10 @@ 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) { + panic("not implemented") +} + 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. @@ -8164,6 +8264,14 @@ 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 { + panic("not implemented") +} + +func (q *querier) UpsertChatHeartbeats(ctx context.Context, arg database.UpsertChatHeartbeatsParams) error { + panic("not implemented") +} + func (q *querier) UpsertChatIncludeDefaultSystemPrompt(ctx context.Context, includeDefaultSystemPrompt bool) error { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceDeploymentConfig); err != nil { return err diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go index 7f68852baf..00c535f8b3 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -321,6 +321,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) @@ -417,6 +425,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) @@ -425,6 +441,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) @@ -465,6 +489,14 @@ func (m queryMetricsStore) DeleteChatDebugDataByChatID(ctx context.Context, chat return r0, r1 } +func (m queryMetricsStore) DeleteChatHeartbeats(ctx context.Context, arg database.DeleteChatHeartbeatsParams) (int64, error) { + start := time.Now() + r0, r1 := m.s.DeleteChatHeartbeats(ctx, arg) + m.queryLatencies.WithLabelValues("DeleteChatHeartbeats").Observe(time.Since(start).Seconds()) + m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "DeleteChatHeartbeats").Inc() + return r0, r1 +} + func (m queryMetricsStore) DeleteChatModelConfigByID(ctx context.Context, id uuid.UUID) error { start := time.Now() r0 := m.s.DeleteChatModelConfigByID(ctx, id) @@ -497,6 +529,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) @@ -777,6 +817,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) @@ -1273,6 +1321,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) @@ -1321,6 +1377,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) @@ -1449,6 +1513,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) @@ -1481,6 +1553,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) @@ -1529,6 +1609,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) @@ -1577,6 +1665,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) @@ -1585,6 +1689,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) @@ -1593,6 +1705,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) @@ -1657,6 +1777,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) @@ -1681,6 +1809,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) @@ -1753,6 +1889,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) @@ -3689,6 +3833,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) @@ -3849,6 +4001,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) @@ -4345,6 +4505,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) @@ -4537,6 +4705,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) @@ -4625,6 +4801,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) @@ -4817,6 +5001,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) @@ -4905,6 +5097,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) @@ -5849,6 +6049,22 @@ 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) UpsertChatHeartbeats(ctx context.Context, arg database.UpsertChatHeartbeatsParams) error { + start := time.Now() + r0 := m.s.UpsertChatHeartbeats(ctx, arg) + m.queryLatencies.WithLabelValues("UpsertChatHeartbeats").Observe(time.Since(start).Seconds()) + m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpsertChatHeartbeats").Inc() + return r0 +} + func (m queryMetricsStore) UpsertChatIncludeDefaultSystemPrompt(ctx context.Context, includeDefaultSystemPrompt bool) error { start := time.Now() r0 := m.s.UpsertChatIncludeDefaultSystemPrompt(ctx, includeDefaultSystemPrompt) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 8321983028..97209c6bc1 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -498,6 +498,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() @@ -674,6 +689,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() @@ -688,6 +717,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() @@ -761,6 +805,21 @@ func (mr *MockStoreMockRecorder) DeleteChatDebugDataByChatID(ctx, arg any) *gomo return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteChatDebugDataByChatID", reflect.TypeOf((*MockStore)(nil).DeleteChatDebugDataByChatID), ctx, arg) } +// DeleteChatHeartbeats mocks base method. +func (m *MockStore) DeleteChatHeartbeats(ctx context.Context, arg database.DeleteChatHeartbeatsParams) (int64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteChatHeartbeats", ctx, arg) + ret0, _ := ret[0].(int64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DeleteChatHeartbeats indicates an expected call of DeleteChatHeartbeats. +func (mr *MockStoreMockRecorder) DeleteChatHeartbeats(ctx, arg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteChatHeartbeats", reflect.TypeOf((*MockStore)(nil).DeleteChatHeartbeats), ctx, arg) +} + // DeleteChatModelConfigByID mocks base method. func (m *MockStore) DeleteChatModelConfigByID(ctx context.Context, id uuid.UUID) error { m.ctrl.T.Helper() @@ -817,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() @@ -1319,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() @@ -2355,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() @@ -2445,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() @@ -2685,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() @@ -2745,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() @@ -2835,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() @@ -2925,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() @@ -2940,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() @@ -2955,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() @@ -3075,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() @@ -3120,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() @@ -3255,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() @@ -6929,6 +7198,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() @@ -7229,6 +7513,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() @@ -8144,6 +8443,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() @@ -8579,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() @@ -8771,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() @@ -9117,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() @@ -9280,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() @@ -10989,6 +11363,34 @@ 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) +} + +// UpsertChatHeartbeats mocks base method. +func (m *MockStore) UpsertChatHeartbeats(ctx context.Context, arg database.UpsertChatHeartbeatsParams) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpsertChatHeartbeats", ctx, arg) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpsertChatHeartbeats indicates an expected call of UpsertChatHeartbeats. +func (mr *MockStoreMockRecorder) UpsertChatHeartbeats(ctx, arg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertChatHeartbeats", reflect.TypeOf((*MockStore)(nil).UpsertChatHeartbeats), ctx, arg) +} + // UpsertChatIncludeDefaultSystemPrompt mocks base method. func (m *MockStore) UpsertChatIncludeDefaultSystemPrompt(ctx context.Context, includeDefaultSystemPrompt bool) error { m.ctrl.T.Helper() diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index ea09f4300f..35c04ab5c1 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -341,7 +341,8 @@ CREATE TYPE chat_status AS ENUM ( 'paused', 'completed', 'error', - 'requires_action' + 'requires_action', + 'interrupting' ); CREATE TYPE connection_status AS ENUM ( @@ -715,6 +716,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 $$ @@ -1292,6 +1316,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, @@ -1669,6 +1790,12 @@ 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 +); + CREATE TABLE chat_messages ( id bigint NOT NULL, chat_id uuid NOT NULL, @@ -1691,7 +1818,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 @@ -1725,13 +1853,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 @@ -1796,6 +1933,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))), @@ -1883,6 +2028,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, @@ -3844,6 +3997,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); @@ -4186,6 +4342,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); @@ -4298,6 +4456,8 @@ CREATE UNIQUE INDEX idx_chat_model_configs_single_default ON chat_model_configs CREATE INDEX idx_chat_queued_messages_chat_id ON chat_queued_messages USING btree (chat_id); +CREATE INDEX idx_chat_queued_messages_chat_position_id ON chat_queued_messages USING btree (chat_id, "position", id); + CREATE INDEX idx_chats_agent_id ON chats USING btree (agent_id) WHERE (agent_id IS NOT NULL); CREATE INDEX idx_chats_auto_archive_candidates ON chats USING btree (created_at) WHERE ((archived = false) AND (pin_order = 0) AND (parent_chat_id IS NULL)); @@ -4546,6 +4706,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(); @@ -4562,6 +4728,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(); @@ -4632,6 +4808,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; diff --git a/coderd/database/foreign_key_constraint.go b/coderd/database/foreign_key_constraint.go index 8109f2564f..159040d142 100644 --- a/coderd/database/foreign_key_constraint.go +++ b/coderd/database/foreign_key_constraint.go @@ -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); diff --git a/coderd/database/models.go b/coderd/database/models.go index 8a6aa5fd4d..3d93e54aa8 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -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, } } @@ -4604,38 +4607,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 { @@ -4717,6 +4728,12 @@ type ChatFileLink struct { FileID uuid.UUID `db:"file_id" json:"file_id"` } +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"` @@ -4740,6 +4757,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 { @@ -4768,39 +4786,49 @@ 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 { - 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"` + 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"` + 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"` } type ChatUsageLimitConfig struct { diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 4b9fa58e01..5b616f653f 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -89,6 +89,9 @@ type sqlcQuerier interface { CountAIBridgeInterceptions(ctx context.Context, arg CountAIBridgeInterceptionsParams) (int64, 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. @@ -106,7 +109,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 @@ -124,10 +131,16 @@ type sqlcQuerier interface { // window (for example, after an unarchive races with a pending // archive-cleanup retry). DeleteChatDebugDataByChatID(ctx context.Context, arg DeleteChatDebugDataByChatIDParams) (int64, error) + // Deletes heartbeat rows for the supplied (chat_id, runner_id) pairs. + DeleteChatHeartbeats(ctx context.Context, arg DeleteChatHeartbeatsParams) (int64, error) DeleteChatModelConfigByID(ctx context.Context, id uuid.UUID) error 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) @@ -196,6 +209,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) @@ -318,6 +332,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) @@ -329,6 +347,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. @@ -366,6 +385,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 @@ -373,6 +396,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; @@ -386,6 +410,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) @@ -395,12 +420,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. @@ -425,11 +456,18 @@ 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 worker-runnable chats whose ownership is missing or whose + // current runner heartbeat is stale. The runner_id IS NULL predicate is + // a robustness extension for inconsistent rows where a worker_id exists + // without a runner_id; normal missing ownership is worker_id IS NULL or + // a missing or stale heartbeat row. + 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 @@ -446,6 +484,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) @@ -910,6 +952,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) @@ -940,7 +984,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 @@ -1020,6 +1071,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 @@ -1071,6 +1127,19 @@ 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) + // ===================================================================== + // chatd core state machine queries. + // + // These are consumed by the coderd/x/chatd/chatstate package. They + // are intentionally kept side-by-side with the legacy chatd queries + // above so the existing runtime keeps working while the state machine + // lands behind it. + // ===================================================================== + // 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. @@ -1095,6 +1164,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) @@ -1196,6 +1268,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 @@ -1214,11 +1291,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) @@ -1226,6 +1301,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) @@ -1372,6 +1450,10 @@ 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 + UpsertChatHeartbeats(ctx context.Context, arg UpsertChatHeartbeatsParams) error UpsertChatIncludeDefaultSystemPrompt(ctx context.Context, includeDefaultSystemPrompt bool) error // UpsertChatPersonalModelOverridesEnabled updates whether users may configure // personal chat model overrides. diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 6b7b39c021..6c1406c049 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -5982,7 +5982,7 @@ WHERE LIMIT $3::int ) -RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl +RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at ), chats_expanded AS ( SELECT @@ -6014,6 +6014,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, @@ -6023,7 +6031,7 @@ chats_expanded AS ( LEFT JOIN chats root ON root.id = COALESCE(acquired_chats.root_chat_id, acquired_chats.parent_chat_id) JOIN visible_users owner ON owner.id = acquired_chats.owner_id ) -SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name +SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name FROM chats_expanded ` @@ -6073,6 +6081,14 @@ func (q *sqlQuerier) AcquireChats(ctx context.Context, arg AcquireChatsParams) ( &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, @@ -6215,7 +6231,7 @@ WITH updated_chats AS ( UPDATE chats SET archived = true, pin_order = 0, updated_at = NOW() WHERE id = $1::uuid OR root_chat_id = $1::uuid - RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl + RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at ), chats_expanded AS ( SELECT @@ -6247,6 +6263,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, @@ -6256,7 +6280,7 @@ chats_expanded AS ( LEFT JOIN chats root ON root.id = COALESCE(updated_chats.root_chat_id, updated_chats.parent_chat_id) JOIN visible_users owner ON owner.id = updated_chats.owner_id ) -SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name +SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name FROM chats_expanded ORDER BY (chats_expanded.id = $1::uuid) DESC, chats_expanded.created_at ASC, chats_expanded.id ASC ` @@ -6299,6 +6323,14 @@ func (q *sqlQuerier) ArchiveChatByID(ctx context.Context, id uuid.UUID) ([]Chat, &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, @@ -6353,10 +6385,10 @@ archived AS ( FROM to_archive t WHERE (c.id = t.id OR c.root_chat_id = t.id) -- cascade to children AND c.archived = false - RETURNING c.id, c.owner_id, c.workspace_id, c.title, c.status, c.worker_id, c.started_at, c.heartbeat_at, c.created_at, c.updated_at, c.parent_chat_id, c.root_chat_id, c.last_model_config_id, c.archived, c.last_error, c.mode, c.mcp_server_ids, c.labels, c.build_id, c.agent_id, c.pin_order, c.last_read_message_id, c.last_injected_context, c.dynamic_tools, c.organization_id, c.plan_mode, c.client_type, c.last_turn_summary, c.user_acl, c.group_acl + RETURNING c.id, c.owner_id, c.workspace_id, c.title, c.status, c.worker_id, c.started_at, c.heartbeat_at, c.created_at, c.updated_at, c.parent_chat_id, c.root_chat_id, c.last_model_config_id, c.archived, c.last_error, c.mode, c.mcp_server_ids, c.labels, c.build_id, c.agent_id, c.pin_order, c.last_read_message_id, c.last_injected_context, c.dynamic_tools, c.organization_id, c.plan_mode, c.client_type, c.last_turn_summary, c.user_acl, c.group_acl, c.snapshot_version, c.history_version, c.queue_version, c.generation_attempt, c.retry_state, c.retry_state_version, c.runner_id, c.requires_action_deadline_at ) SELECT - a.id, a.owner_id, a.workspace_id, a.title, a.status, a.worker_id, a.started_at, a.heartbeat_at, a.created_at, a.updated_at, a.parent_chat_id, a.root_chat_id, a.last_model_config_id, a.archived, a.last_error, a.mode, a.mcp_server_ids, a.labels, a.build_id, a.agent_id, a.pin_order, a.last_read_message_id, a.last_injected_context, a.dynamic_tools, a.organization_id, a.plan_mode, a.client_type, a.last_turn_summary, a.user_acl, a.group_acl, + a.id, a.owner_id, a.workspace_id, a.title, a.status, a.worker_id, a.started_at, a.heartbeat_at, a.created_at, a.updated_at, a.parent_chat_id, a.root_chat_id, a.last_model_config_id, a.archived, a.last_error, a.mode, a.mcp_server_ids, a.labels, a.build_id, a.agent_id, a.pin_order, a.last_read_message_id, a.last_injected_context, a.dynamic_tools, a.organization_id, a.plan_mode, a.client_type, a.last_turn_summary, a.user_acl, a.group_acl, a.snapshot_version, a.history_version, a.queue_version, a.generation_attempt, a.retry_state, a.retry_state_version, a.runner_id, a.requires_action_deadline_at, -- Children inherit their root's activity so last_activity_at is never null. COALESCE( t.last_activity_at, @@ -6374,37 +6406,45 @@ type AutoArchiveInactiveChatsParams struct { } type AutoArchiveInactiveChatsRow 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 json.RawMessage `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 json.RawMessage `db:"user_acl" json:"user_acl"` - GroupACL json.RawMessage `db:"group_acl" json:"group_acl"` - LastActivityAt time.Time `db:"last_activity_at" json:"last_activity_at"` + 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 json.RawMessage `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 json.RawMessage `db:"user_acl" json:"user_acl"` + GroupACL json.RawMessage `db:"group_acl" json:"group_acl"` + 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"` + LastActivityAt time.Time `db:"last_activity_at" json:"last_activity_at"` } // Archives inactive root chats (pinned and already-archived chats skipped), @@ -6454,6 +6494,14 @@ func (q *sqlQuerier) AutoArchiveInactiveChats(ctx context.Context, arg AutoArchi &i.LastTurnSummary, &i.UserACL, &i.GroupACL, + &i.SnapshotVersion, + &i.HistoryVersion, + &i.QueueVersion, + &i.GenerationAttempt, + &i.RetryState, + &i.RetryStateVersion, + &i.RunnerID, + &i.RequiresActionDeadlineAt, &i.LastActivityAt, ); err != nil { return nil, err @@ -6505,6 +6553,21 @@ func (q *sqlQuerier) ClearChatMessageProviderResponseIDsByChatID(ctx context.Con return err } +const countChatQueuedMessages = `-- name: CountChatQueuedMessages :one +SELECT COUNT(*)::bigint AS count +FROM chat_queued_messages +WHERE chat_id = $1::uuid +` + +// Cheap queue-length check used by ChatMachine.Update when deciding +// whether the chat is in a "1" sub-state. +func (q *sqlQuerier) CountChatQueuedMessages(ctx context.Context, chatID uuid.UUID) (int64, error) { + row := q.db.QueryRowContext(ctx, countChatQueuedMessages, chatID) + var count int64 + err := row.Scan(&count) + return count, err +} + const countEnabledModelsWithoutPricing = `-- name: CountEnabledModelsWithoutPricing :one SELECT COUNT(*)::bigint AS count FROM chat_model_configs @@ -6529,6 +6592,17 @@ func (q *sqlQuerier) CountEnabledModelsWithoutPricing(ctx context.Context) (int6 return count, err } +const deleteAllChatHeartbeats = `-- name: DeleteAllChatHeartbeats :exec +DELETE FROM chat_heartbeats WHERE chat_id = $1::uuid +` + +// Deletes all heartbeat rows for the chat. Used during ownership +// transitions that abandon a lease. +func (q *sqlQuerier) DeleteAllChatHeartbeats(ctx context.Context, chatID uuid.UUID) error { + _, err := q.db.ExecContext(ctx, deleteAllChatHeartbeats, chatID) + return err +} + const deleteAllChatQueuedMessages = `-- name: DeleteAllChatQueuedMessages :exec DELETE FROM chat_queued_messages WHERE chat_id = $1 ` @@ -6538,6 +6612,41 @@ func (q *sqlQuerier) DeleteAllChatQueuedMessages(ctx context.Context, chatID uui return err } +const deleteAllChatQueuedMessagesReturningCount = `-- name: DeleteAllChatQueuedMessagesReturningCount :execrows +DELETE FROM chat_queued_messages +WHERE chat_id = $1::uuid +` + +func (q *sqlQuerier) DeleteAllChatQueuedMessagesReturningCount(ctx context.Context, chatID uuid.UUID) (int64, error) { + result, err := q.db.ExecContext(ctx, deleteAllChatQueuedMessagesReturningCount, chatID) + if err != nil { + return 0, err + } + return result.RowsAffected() +} + +const deleteChatHeartbeats = `-- name: DeleteChatHeartbeats :execrows +DELETE FROM chat_heartbeats +USING unnest($1::uuid[]) WITH ORDINALITY AS chat_ids(chat_id, ord) +JOIN unnest($2::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 +` + +type DeleteChatHeartbeatsParams struct { + ChatIds []uuid.UUID `db:"chat_ids" json:"chat_ids"` + RunnerIds []uuid.UUID `db:"runner_ids" json:"runner_ids"` +} + +// Deletes heartbeat rows for the supplied (chat_id, runner_id) pairs. +func (q *sqlQuerier) DeleteChatHeartbeats(ctx context.Context, arg DeleteChatHeartbeatsParams) (int64, error) { + result, err := q.db.ExecContext(ctx, deleteChatHeartbeats, pq.Array(arg.ChatIds), pq.Array(arg.RunnerIds)) + if err != nil { + return 0, err + } + return result.RowsAffected() +} + const deleteChatQueuedMessage = `-- name: DeleteChatQueuedMessage :exec DELETE FROM chat_queued_messages WHERE id = $1 AND chat_id = $2 ` @@ -6552,6 +6661,27 @@ func (q *sqlQuerier) DeleteChatQueuedMessage(ctx context.Context, arg DeleteChat return err } +const deleteChatQueuedMessageReturningCount = `-- name: DeleteChatQueuedMessageReturningCount :execrows +DELETE FROM chat_queued_messages +WHERE id = $1::bigint AND chat_id = $2::uuid +` + +type DeleteChatQueuedMessageReturningCountParams struct { + ID int64 `db:"id" json:"id"` + ChatID uuid.UUID `db:"chat_id" json:"chat_id"` +} + +// 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. +func (q *sqlQuerier) DeleteChatQueuedMessageReturningCount(ctx context.Context, arg DeleteChatQueuedMessageReturningCountParams) (int64, error) { + result, err := q.db.ExecContext(ctx, deleteChatQueuedMessageReturningCount, arg.ID, arg.ChatID) + if err != nil { + return 0, err + } + return result.RowsAffected() +} + const deleteChatUsageLimitGroupOverride = `-- name: DeleteChatUsageLimitGroupOverride :exec UPDATE groups SET chat_spend_limit_micros = NULL WHERE id = $1::uuid ` @@ -6603,8 +6733,21 @@ func (q *sqlQuerier) DeleteOldChats(ctx context.Context, arg DeleteOldChatsParam return result.RowsAffected() } +const deleteStaleChatHeartbeats = `-- name: DeleteStaleChatHeartbeats :execrows +DELETE FROM chat_heartbeats +WHERE heartbeat_at < NOW() - (INTERVAL '1 second' * $1::int) +` + +func (q *sqlQuerier) DeleteStaleChatHeartbeats(ctx context.Context, staleSeconds int32) (int64, error) { + result, err := q.db.ExecContext(ctx, deleteStaleChatHeartbeats, staleSeconds) + if err != nil { + return 0, err + } + return result.RowsAffected() +} + const getActiveChatsByAgentID = `-- name: GetActiveChatsByAgentID :many -SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name +SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name FROM chats_expanded WHERE agent_id = $1::uuid AND archived = false @@ -6653,6 +6796,14 @@ func (q *sqlQuerier) GetActiveChatsByAgentID(ctx context.Context, agentID uuid.U &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, @@ -6671,6 +6822,152 @@ func (q *sqlQuerier) GetActiveChatsByAgentID(ctx context.Context, agentID uuid.U return items, nil } +const getAutoArchiveInactiveChatCandidates = `-- name: GetAutoArchiveInactiveChatCandidates :many +SELECT + chats_expanded.id, chats_expanded.owner_id, chats_expanded.workspace_id, chats_expanded.title, chats_expanded.status, chats_expanded.worker_id, chats_expanded.started_at, chats_expanded.heartbeat_at, chats_expanded.created_at, chats_expanded.updated_at, chats_expanded.parent_chat_id, chats_expanded.root_chat_id, chats_expanded.last_model_config_id, chats_expanded.archived, chats_expanded.last_error, chats_expanded.mode, chats_expanded.mcp_server_ids, chats_expanded.labels, chats_expanded.build_id, chats_expanded.agent_id, chats_expanded.pin_order, chats_expanded.last_read_message_id, chats_expanded.last_injected_context, chats_expanded.dynamic_tools, chats_expanded.organization_id, chats_expanded.plan_mode, chats_expanded.client_type, chats_expanded.last_turn_summary, chats_expanded.snapshot_version, chats_expanded.history_version, chats_expanded.queue_version, chats_expanded.generation_attempt, chats_expanded.retry_state, chats_expanded.retry_state_version, chats_expanded.runner_id, chats_expanded.requires_action_deadline_at, chats_expanded.user_acl, chats_expanded.group_acl, chats_expanded.owner_username, chats_expanded.owner_name, + 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 < $1::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) < $1::timestamptz +ORDER BY chats_expanded.created_at ASC +LIMIT $2::int +` + +type GetAutoArchiveInactiveChatCandidatesParams struct { + ArchiveCutoff time.Time `db:"archive_cutoff" json:"archive_cutoff"` + LimitCount int32 `db:"limit_count" json:"limit_count"` +} + +type GetAutoArchiveInactiveChatCandidatesRow 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"` + 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"` + LastActivityAt time.Time `db:"last_activity_at" json:"last_activity_at"` +} + +// 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. +func (q *sqlQuerier) GetAutoArchiveInactiveChatCandidates(ctx context.Context, arg GetAutoArchiveInactiveChatCandidatesParams) ([]GetAutoArchiveInactiveChatCandidatesRow, error) { + rows, err := q.db.QueryContext(ctx, getAutoArchiveInactiveChatCandidates, arg.ArchiveCutoff, arg.LimitCount) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetAutoArchiveInactiveChatCandidatesRow + for rows.Next() { + var i GetAutoArchiveInactiveChatCandidatesRow + if err := rows.Scan( + &i.ID, + &i.OwnerID, + &i.WorkspaceID, + &i.Title, + &i.Status, + &i.WorkerID, + &i.StartedAt, + &i.HeartbeatAt, + &i.CreatedAt, + &i.UpdatedAt, + &i.ParentChatID, + &i.RootChatID, + &i.LastModelConfigID, + &i.Archived, + &i.LastError, + &i.Mode, + pq.Array(&i.MCPServerIDs), + &i.Labels, + &i.BuildID, + &i.AgentID, + &i.PinOrder, + &i.LastReadMessageID, + &i.LastInjectedContext, + &i.DynamicTools, + &i.OrganizationID, + &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, + &i.OwnerName, + &i.LastActivityAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getChatACLByID = `-- name: GetChatACLByID :one SELECT user_acl AS users, @@ -6694,7 +6991,7 @@ func (q *sqlQuerier) GetChatACLByID(ctx context.Context, id uuid.UUID) (GetChatA } const getChatByID = `-- name: GetChatByID :one -SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name +SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name FROM chats_expanded WHERE id = $1::uuid ` @@ -6731,6 +7028,120 @@ func (q *sqlQuerier) GetChatByID(ctx context.Context, id uuid.UUID) (Chat, error &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, + &i.OwnerName, + ) + return i, err +} + +const getChatByIDForShare = `-- name: GetChatByIDForShare :one +WITH shared_chat AS ( + SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at + FROM chats + WHERE id = $1::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 id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name +FROM chats_expanded +` + +func (q *sqlQuerier) GetChatByIDForShare(ctx context.Context, id uuid.UUID) (Chat, error) { + row := q.db.QueryRowContext(ctx, getChatByIDForShare, id) + var i Chat + err := row.Scan( + &i.ID, + &i.OwnerID, + &i.WorkspaceID, + &i.Title, + &i.Status, + &i.WorkerID, + &i.StartedAt, + &i.HeartbeatAt, + &i.CreatedAt, + &i.UpdatedAt, + &i.ParentChatID, + &i.RootChatID, + &i.LastModelConfigID, + &i.Archived, + &i.LastError, + &i.Mode, + pq.Array(&i.MCPServerIDs), + &i.Labels, + &i.BuildID, + &i.AgentID, + &i.PinOrder, + &i.LastReadMessageID, + &i.LastInjectedContext, + &i.DynamicTools, + &i.OrganizationID, + &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, @@ -6741,7 +7152,7 @@ func (q *sqlQuerier) GetChatByID(ctx context.Context, id uuid.UUID) (Chat, error const getChatByIDForUpdate = `-- name: GetChatByIDForUpdate :one WITH locked_chat AS ( - SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl + SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at FROM chats WHERE id = $1::uuid FOR UPDATE @@ -6776,6 +7187,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, @@ -6785,7 +7204,7 @@ chats_expanded AS ( LEFT JOIN chats root ON root.id = COALESCE(locked_chat.root_chat_id, locked_chat.parent_chat_id) JOIN visible_users owner ON owner.id = locked_chat.owner_id ) -SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name +SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name FROM chats_expanded ` @@ -6821,6 +7240,14 @@ func (q *sqlQuerier) GetChatByIDForUpdate(ctx context.Context, id uuid.UUID) (Ch &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, @@ -7357,9 +7784,59 @@ func (q *sqlQuerier) GetChatDiffStatusesByChatIDs(ctx context.Context, chatIds [ return items, nil } +const getChatFamilyIDsByRootID = `-- name: GetChatFamilyIDsByRootID :many +SELECT id +FROM chats +WHERE id = $1::uuid OR root_chat_id = $1::uuid +ORDER BY (id = $1::uuid) DESC, created_at ASC, id ASC +` + +// 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. +func (q *sqlQuerier) GetChatFamilyIDsByRootID(ctx context.Context, id uuid.UUID) ([]uuid.UUID, error) { + rows, err := q.db.QueryContext(ctx, getChatFamilyIDsByRootID, id) + if err != nil { + return nil, err + } + defer rows.Close() + var items []uuid.UUID + for rows.Next() { + var id uuid.UUID + if err := rows.Scan(&id); err != nil { + return nil, err + } + items = append(items, id) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getChatHeartbeat = `-- name: GetChatHeartbeat :one +SELECT chat_id, runner_id, heartbeat_at FROM chat_heartbeats +WHERE chat_id = $1::uuid AND runner_id = $2::uuid +` + +type GetChatHeartbeatParams struct { + ChatID uuid.UUID `db:"chat_id" json:"chat_id"` + RunnerID uuid.UUID `db:"runner_id" json:"runner_id"` +} + +func (q *sqlQuerier) GetChatHeartbeat(ctx context.Context, arg GetChatHeartbeatParams) (ChatHeartbeat, error) { + row := q.db.QueryRowContext(ctx, getChatHeartbeat, arg.ChatID, arg.RunnerID) + var i ChatHeartbeat + err := row.Scan(&i.ChatID, &i.RunnerID, &i.HeartbeatAt) + return i, err +} + const getChatMessageByID = `-- name: GetChatMessageByID :one SELECT - id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted, provider_response_id, api_key_id + id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted, provider_response_id, api_key_id, revision FROM chat_messages WHERE @@ -7393,6 +7870,7 @@ func (q *sqlQuerier) GetChatMessageByID(ctx context.Context, id int64) (ChatMess &i.Deleted, &i.ProviderResponseID, &i.APIKeyID, + &i.Revision, ) return i, err } @@ -7482,7 +7960,7 @@ func (q *sqlQuerier) GetChatMessageSummariesPerChat(ctx context.Context, created const getChatMessagesByChatID = `-- name: GetChatMessagesByChatID :many SELECT - id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted, provider_response_id, api_key_id + id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted, provider_response_id, api_key_id, revision FROM chat_messages WHERE @@ -7531,6 +8009,7 @@ func (q *sqlQuerier) GetChatMessagesByChatID(ctx context.Context, arg GetChatMes &i.Deleted, &i.ProviderResponseID, &i.APIKeyID, + &i.Revision, ); err != nil { return nil, err } @@ -7547,7 +8026,7 @@ func (q *sqlQuerier) GetChatMessagesByChatID(ctx context.Context, arg GetChatMes const getChatMessagesByChatIDAscPaginated = `-- name: GetChatMessagesByChatIDAscPaginated :many SELECT - id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted, provider_response_id, api_key_id + id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted, provider_response_id, api_key_id, revision FROM chat_messages WHERE @@ -7599,6 +8078,7 @@ func (q *sqlQuerier) GetChatMessagesByChatIDAscPaginated(ctx context.Context, ar &i.Deleted, &i.ProviderResponseID, &i.APIKeyID, + &i.Revision, ); err != nil { return nil, err } @@ -7615,7 +8095,7 @@ func (q *sqlQuerier) GetChatMessagesByChatIDAscPaginated(ctx context.Context, ar const getChatMessagesByChatIDDescPaginated = `-- name: GetChatMessagesByChatIDDescPaginated :many SELECT - id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted, provider_response_id, api_key_id + id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted, provider_response_id, api_key_id, revision FROM chat_messages WHERE @@ -7680,6 +8160,72 @@ func (q *sqlQuerier) GetChatMessagesByChatIDDescPaginated(ctx context.Context, a &i.Deleted, &i.ProviderResponseID, &i.APIKeyID, + &i.Revision, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getChatMessagesByRevisionForStream = `-- name: GetChatMessagesByRevisionForStream :many +SELECT + id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted, provider_response_id, api_key_id, revision +FROM + chat_messages +WHERE + chat_id = $1::uuid + AND revision > $2::bigint + AND visibility IN ('user', 'both') +ORDER BY + created_at ASC, id ASC +` + +type GetChatMessagesByRevisionForStreamParams struct { + ChatID uuid.UUID `db:"chat_id" json:"chat_id"` + AfterRevision int64 `db:"after_revision" json:"after_revision"` +} + +func (q *sqlQuerier) GetChatMessagesByRevisionForStream(ctx context.Context, arg GetChatMessagesByRevisionForStreamParams) ([]ChatMessage, error) { + rows, err := q.db.QueryContext(ctx, getChatMessagesByRevisionForStream, arg.ChatID, arg.AfterRevision) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ChatMessage + for rows.Next() { + var i ChatMessage + if err := rows.Scan( + &i.ID, + &i.ChatID, + &i.ModelConfigID, + &i.CreatedAt, + &i.Role, + &i.Content, + &i.Visibility, + &i.InputTokens, + &i.OutputTokens, + &i.TotalTokens, + &i.ReasoningTokens, + &i.CacheCreationTokens, + &i.CacheReadTokens, + &i.ContextLimit, + &i.Compressed, + &i.CreatedBy, + &i.ContentVersion, + &i.TotalCostMicros, + &i.RuntimeMs, + &i.Deleted, + &i.ProviderResponseID, + &i.APIKeyID, + &i.Revision, ); err != nil { return nil, err } @@ -7712,7 +8258,7 @@ WITH latest_compressed_summary AS ( 1 ) SELECT - id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted, provider_response_id, api_key_id + id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted, provider_response_id, api_key_id, revision FROM chat_messages WHERE @@ -7785,6 +8331,7 @@ func (q *sqlQuerier) GetChatMessagesForPromptByChatID(ctx context.Context, chatI &i.Deleted, &i.ProviderResponseID, &i.APIKeyID, + &i.Revision, ); err != nil { return nil, err } @@ -7845,8 +8392,58 @@ func (q *sqlQuerier) GetChatModelConfigsForTelemetry(ctx context.Context) ([]Get return items, nil } +const getChatQueuedMessageByID = `-- name: GetChatQueuedMessageByID :one +SELECT id, chat_id, content, created_at, model_config_id, api_key_id, position, created_by FROM chat_queued_messages +WHERE id = $1::bigint AND chat_id = $2::uuid +` + +type GetChatQueuedMessageByIDParams struct { + ID int64 `db:"id" json:"id"` + ChatID uuid.UUID `db:"chat_id" json:"chat_id"` +} + +func (q *sqlQuerier) GetChatQueuedMessageByID(ctx context.Context, arg GetChatQueuedMessageByIDParams) (ChatQueuedMessage, error) { + row := q.db.QueryRowContext(ctx, getChatQueuedMessageByID, arg.ID, arg.ChatID) + var i ChatQueuedMessage + err := row.Scan( + &i.ID, + &i.ChatID, + &i.Content, + &i.CreatedAt, + &i.ModelConfigID, + &i.APIKeyID, + &i.Position, + &i.CreatedBy, + ) + return i, err +} + +const getChatQueuedMessageHead = `-- name: GetChatQueuedMessageHead :one +SELECT id, chat_id, content, created_at, model_config_id, api_key_id, position, created_by FROM chat_queued_messages +WHERE chat_id = $1::uuid +ORDER BY position ASC, id ASC +LIMIT 1 +` + +// Returns the queue head (lowest position, then lowest id). +func (q *sqlQuerier) GetChatQueuedMessageHead(ctx context.Context, chatID uuid.UUID) (ChatQueuedMessage, error) { + row := q.db.QueryRowContext(ctx, getChatQueuedMessageHead, chatID) + var i ChatQueuedMessage + err := row.Scan( + &i.ID, + &i.ChatID, + &i.Content, + &i.CreatedAt, + &i.ModelConfigID, + &i.APIKeyID, + &i.Position, + &i.CreatedBy, + ) + return i, err +} + const getChatQueuedMessages = `-- name: GetChatQueuedMessages :many -SELECT id, chat_id, content, created_at, model_config_id, api_key_id FROM chat_queued_messages +SELECT id, chat_id, content, created_at, model_config_id, api_key_id, position, created_by FROM chat_queued_messages WHERE chat_id = $1 ORDER BY created_at ASC, id ASC ` @@ -7867,6 +8464,105 @@ func (q *sqlQuerier) GetChatQueuedMessages(ctx context.Context, chatID uuid.UUID &i.CreatedAt, &i.ModelConfigID, &i.APIKeyID, + &i.Position, + &i.CreatedBy, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getChatQueuedMessagesByPosition = `-- name: GetChatQueuedMessagesByPosition :many +SELECT id, chat_id, content, created_at, model_config_id, api_key_id, position, created_by FROM chat_queued_messages +WHERE chat_id = $1::uuid +ORDER BY position ASC, id ASC +` + +// Returns queued messages in state-machine order (position ASC, id ASC). +func (q *sqlQuerier) GetChatQueuedMessagesByPosition(ctx context.Context, chatID uuid.UUID) ([]ChatQueuedMessage, error) { + rows, err := q.db.QueryContext(ctx, getChatQueuedMessagesByPosition, chatID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ChatQueuedMessage + for rows.Next() { + var i ChatQueuedMessage + if err := rows.Scan( + &i.ID, + &i.ChatID, + &i.Content, + &i.CreatedAt, + &i.ModelConfigID, + &i.APIKeyID, + &i.Position, + &i.CreatedBy, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getChatStreamSyncRows = `-- name: GetChatStreamSyncRows :many +SELECT + id, + snapshot_version, + history_version, + queue_version, + retry_state_version, + generation_attempt, + status, + worker_id +FROM chats +WHERE id = ANY($1::uuid[]) +ORDER BY id ASC +` + +type GetChatStreamSyncRowsRow struct { + ID uuid.UUID `db:"id" json:"id"` + SnapshotVersion int64 `db:"snapshot_version" json:"snapshot_version"` + HistoryVersion int64 `db:"history_version" json:"history_version"` + QueueVersion int64 `db:"queue_version" json:"queue_version"` + RetryStateVersion int64 `db:"retry_state_version" json:"retry_state_version"` + GenerationAttempt int64 `db:"generation_attempt" json:"generation_attempt"` + Status ChatStatus `db:"status" json:"status"` + WorkerID uuid.NullUUID `db:"worker_id" json:"worker_id"` +} + +func (q *sqlQuerier) GetChatStreamSyncRows(ctx context.Context, ids []uuid.UUID) ([]GetChatStreamSyncRowsRow, error) { + rows, err := q.db.QueryContext(ctx, getChatStreamSyncRows, pq.Array(ids)) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetChatStreamSyncRowsRow + for rows.Next() { + var i GetChatStreamSyncRowsRow + if err := rows.Scan( + &i.ID, + &i.SnapshotVersion, + &i.HistoryVersion, + &i.QueueVersion, + &i.RetryStateVersion, + &i.GenerationAttempt, + &i.Status, + &i.WorkerID, ); err != nil { return nil, err } @@ -8002,6 +8698,160 @@ func (q *sqlQuerier) GetChatUserPromptsByChatID(ctx context.Context, arg GetChat return items, nil } +const getChatWorkerAcquisitionCandidates = `-- name: GetChatWorkerAcquisitionCandidates :many +SELECT + chats_expanded.id, chats_expanded.owner_id, chats_expanded.workspace_id, chats_expanded.title, chats_expanded.status, chats_expanded.worker_id, chats_expanded.started_at, chats_expanded.heartbeat_at, chats_expanded.created_at, chats_expanded.updated_at, chats_expanded.parent_chat_id, chats_expanded.root_chat_id, chats_expanded.last_model_config_id, chats_expanded.archived, chats_expanded.last_error, chats_expanded.mode, chats_expanded.mcp_server_ids, chats_expanded.labels, chats_expanded.build_id, chats_expanded.agent_id, chats_expanded.pin_order, chats_expanded.last_read_message_id, chats_expanded.last_injected_context, chats_expanded.dynamic_tools, chats_expanded.organization_id, chats_expanded.plan_mode, chats_expanded.client_type, chats_expanded.last_turn_summary, chats_expanded.snapshot_version, chats_expanded.history_version, chats_expanded.queue_version, chats_expanded.generation_attempt, chats_expanded.retry_state, chats_expanded.retry_state_version, chats_expanded.runner_id, chats_expanded.requires_action_deadline_at, chats_expanded.user_acl, chats_expanded.group_acl, chats_expanded.owner_username, chats_expanded.owner_name, + 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' * $1::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' * $1::int) + ) + ) +ORDER BY chats_expanded.updated_at ASC, chats_expanded.id ASC +LIMIT $2::int +` + +type GetChatWorkerAcquisitionCandidatesParams struct { + StaleSeconds int32 `db:"stale_seconds" json:"stale_seconds"` + LimitCount int32 `db:"limit_count" json:"limit_count"` +} + +type GetChatWorkerAcquisitionCandidatesRow 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"` + 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"` + CurrentHeartbeatAt sql.NullTime `db:"current_heartbeat_at" json:"current_heartbeat_at"` + HeartbeatStale bool `db:"heartbeat_stale" json:"heartbeat_stale"` +} + +// Returns worker-runnable chats whose ownership is missing or whose +// current runner heartbeat is stale. The runner_id IS NULL predicate is +// a robustness extension for inconsistent rows where a worker_id exists +// without a runner_id; normal missing ownership is worker_id IS NULL or +// a missing or stale heartbeat row. +func (q *sqlQuerier) GetChatWorkerAcquisitionCandidates(ctx context.Context, arg GetChatWorkerAcquisitionCandidatesParams) ([]GetChatWorkerAcquisitionCandidatesRow, error) { + rows, err := q.db.QueryContext(ctx, getChatWorkerAcquisitionCandidates, arg.StaleSeconds, arg.LimitCount) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetChatWorkerAcquisitionCandidatesRow + for rows.Next() { + var i GetChatWorkerAcquisitionCandidatesRow + if err := rows.Scan( + &i.ID, + &i.OwnerID, + &i.WorkspaceID, + &i.Title, + &i.Status, + &i.WorkerID, + &i.StartedAt, + &i.HeartbeatAt, + &i.CreatedAt, + &i.UpdatedAt, + &i.ParentChatID, + &i.RootChatID, + &i.LastModelConfigID, + &i.Archived, + &i.LastError, + &i.Mode, + pq.Array(&i.MCPServerIDs), + &i.Labels, + &i.BuildID, + &i.AgentID, + &i.PinOrder, + &i.LastReadMessageID, + &i.LastInjectedContext, + &i.DynamicTools, + &i.OrganizationID, + &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, + &i.OwnerName, + &i.CurrentHeartbeatAt, + &i.HeartbeatStale, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getChats = `-- name: GetChats :many WITH cursor_chat AS ( SELECT @@ -8012,7 +8862,7 @@ WITH cursor_chat AS ( WHERE id = $5 ) SELECT - chats_expanded.id, chats_expanded.owner_id, chats_expanded.workspace_id, chats_expanded.title, chats_expanded.status, chats_expanded.worker_id, chats_expanded.started_at, chats_expanded.heartbeat_at, chats_expanded.created_at, chats_expanded.updated_at, chats_expanded.parent_chat_id, chats_expanded.root_chat_id, chats_expanded.last_model_config_id, chats_expanded.archived, chats_expanded.last_error, chats_expanded.mode, chats_expanded.mcp_server_ids, chats_expanded.labels, chats_expanded.build_id, chats_expanded.agent_id, chats_expanded.pin_order, chats_expanded.last_read_message_id, chats_expanded.last_injected_context, chats_expanded.dynamic_tools, chats_expanded.organization_id, chats_expanded.plan_mode, chats_expanded.client_type, chats_expanded.last_turn_summary, chats_expanded.user_acl, chats_expanded.group_acl, chats_expanded.owner_username, chats_expanded.owner_name, + chats_expanded.id, chats_expanded.owner_id, chats_expanded.workspace_id, chats_expanded.title, chats_expanded.status, chats_expanded.worker_id, chats_expanded.started_at, chats_expanded.heartbeat_at, chats_expanded.created_at, chats_expanded.updated_at, chats_expanded.parent_chat_id, chats_expanded.root_chat_id, chats_expanded.last_model_config_id, chats_expanded.archived, chats_expanded.last_error, chats_expanded.mode, chats_expanded.mcp_server_ids, chats_expanded.labels, chats_expanded.build_id, chats_expanded.agent_id, chats_expanded.pin_order, chats_expanded.last_read_message_id, chats_expanded.last_injected_context, chats_expanded.dynamic_tools, chats_expanded.organization_id, chats_expanded.plan_mode, chats_expanded.client_type, chats_expanded.last_turn_summary, chats_expanded.snapshot_version, chats_expanded.history_version, chats_expanded.queue_version, chats_expanded.generation_attempt, chats_expanded.retry_state, chats_expanded.retry_state_version, chats_expanded.runner_id, chats_expanded.requires_action_deadline_at, chats_expanded.user_acl, chats_expanded.group_acl, chats_expanded.owner_username, chats_expanded.owner_name, EXISTS ( SELECT 1 FROM chat_messages cm WHERE cm.chat_id = chats_expanded.id @@ -8244,6 +9094,14 @@ func (q *sqlQuerier) GetChats(ctx context.Context, arg GetChatsParams) ([]GetCha &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, @@ -8265,7 +9123,7 @@ func (q *sqlQuerier) GetChats(ctx context.Context, arg GetChatsParams) ([]GetCha const getChatsByChatFileID = `-- name: GetChatsByChatFileID :many SELECT - id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name + id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name FROM chats_expanded WHERE @@ -8316,6 +9174,85 @@ func (q *sqlQuerier) GetChatsByChatFileID(ctx context.Context, fileID uuid.UUID) &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, + &i.OwnerName, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getChatsByIDsForRunnerSync = `-- name: GetChatsByIDsForRunnerSync :many +SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name +FROM chats_expanded +WHERE id = ANY($1::uuid[]) +ORDER BY id ASC +` + +func (q *sqlQuerier) GetChatsByIDsForRunnerSync(ctx context.Context, ids []uuid.UUID) ([]Chat, error) { + rows, err := q.db.QueryContext(ctx, getChatsByIDsForRunnerSync, pq.Array(ids)) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Chat + for rows.Next() { + var i Chat + if err := rows.Scan( + &i.ID, + &i.OwnerID, + &i.WorkspaceID, + &i.Title, + &i.Status, + &i.WorkerID, + &i.StartedAt, + &i.HeartbeatAt, + &i.CreatedAt, + &i.UpdatedAt, + &i.ParentChatID, + &i.RootChatID, + &i.LastModelConfigID, + &i.Archived, + &i.LastError, + &i.Mode, + pq.Array(&i.MCPServerIDs), + &i.Labels, + &i.BuildID, + &i.AgentID, + &i.PinOrder, + &i.LastReadMessageID, + &i.LastInjectedContext, + &i.DynamicTools, + &i.OrganizationID, + &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, @@ -8335,7 +9272,7 @@ func (q *sqlQuerier) GetChatsByChatFileID(ctx context.Context, fileID uuid.UUID) } const getChatsByWorkspaceIDs = `-- name: GetChatsByWorkspaceIDs :many -SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name +SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name FROM chats_expanded WHERE archived = false AND workspace_id = ANY($1::uuid[]) @@ -8380,6 +9317,14 @@ func (q *sqlQuerier) GetChatsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID &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, @@ -8468,7 +9413,7 @@ func (q *sqlQuerier) GetChatsUpdatedAfter(ctx context.Context, updatedAfter time const getChildChatsByParentIDs = `-- name: GetChildChatsByParentIDs :many SELECT - chats_expanded.id, chats_expanded.owner_id, chats_expanded.workspace_id, chats_expanded.title, chats_expanded.status, chats_expanded.worker_id, chats_expanded.started_at, chats_expanded.heartbeat_at, chats_expanded.created_at, chats_expanded.updated_at, chats_expanded.parent_chat_id, chats_expanded.root_chat_id, chats_expanded.last_model_config_id, chats_expanded.archived, chats_expanded.last_error, chats_expanded.mode, chats_expanded.mcp_server_ids, chats_expanded.labels, chats_expanded.build_id, chats_expanded.agent_id, chats_expanded.pin_order, chats_expanded.last_read_message_id, chats_expanded.last_injected_context, chats_expanded.dynamic_tools, chats_expanded.organization_id, chats_expanded.plan_mode, chats_expanded.client_type, chats_expanded.last_turn_summary, chats_expanded.user_acl, chats_expanded.group_acl, chats_expanded.owner_username, chats_expanded.owner_name, + chats_expanded.id, chats_expanded.owner_id, chats_expanded.workspace_id, chats_expanded.title, chats_expanded.status, chats_expanded.worker_id, chats_expanded.started_at, chats_expanded.heartbeat_at, chats_expanded.created_at, chats_expanded.updated_at, chats_expanded.parent_chat_id, chats_expanded.root_chat_id, chats_expanded.last_model_config_id, chats_expanded.archived, chats_expanded.last_error, chats_expanded.mode, chats_expanded.mcp_server_ids, chats_expanded.labels, chats_expanded.build_id, chats_expanded.agent_id, chats_expanded.pin_order, chats_expanded.last_read_message_id, chats_expanded.last_injected_context, chats_expanded.dynamic_tools, chats_expanded.organization_id, chats_expanded.plan_mode, chats_expanded.client_type, chats_expanded.last_turn_summary, chats_expanded.snapshot_version, chats_expanded.history_version, chats_expanded.queue_version, chats_expanded.generation_attempt, chats_expanded.retry_state, chats_expanded.retry_state_version, chats_expanded.runner_id, chats_expanded.requires_action_deadline_at, chats_expanded.user_acl, chats_expanded.group_acl, chats_expanded.owner_username, chats_expanded.owner_name, EXISTS ( SELECT 1 FROM chat_messages cm WHERE cm.chat_id = chats_expanded.id @@ -8541,6 +9486,14 @@ func (q *sqlQuerier) GetChildChatsByParentIDs(ctx context.Context, arg GetChildC &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, @@ -8560,9 +9513,23 @@ func (q *sqlQuerier) GetChildChatsByParentIDs(ctx context.Context, arg GetChildC return items, nil } +const getDatabaseNow = `-- name: GetDatabaseNow :one +SELECT NOW()::timestamptz AS now +` + +// 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. +func (q *sqlQuerier) GetDatabaseNow(ctx context.Context) (time.Time, error) { + row := q.db.QueryRowContext(ctx, getDatabaseNow) + var now time.Time + err := row.Scan(&now) + return now, err +} + const getLastChatMessageByRole = `-- name: GetLastChatMessageByRole :one SELECT - id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted, provider_response_id, api_key_id + id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted, provider_response_id, api_key_id, revision FROM chat_messages WHERE @@ -8606,13 +9573,14 @@ func (q *sqlQuerier) GetLastChatMessageByRole(ctx context.Context, arg GetLastCh &i.Deleted, &i.ProviderResponseID, &i.APIKeyID, + &i.Revision, ) return i, err } const getStaleChats = `-- name: GetStaleChats :many SELECT - id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name + id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name FROM chats_expanded WHERE @@ -8673,6 +9641,14 @@ func (q *sqlQuerier) GetStaleChats(ctx context.Context, staleThreshold time.Time &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, @@ -8753,6 +9729,21 @@ func (q *sqlQuerier) GetUserGroupSpendLimit(ctx context.Context, arg GetUserGrou return limit_micros, err } +const incrementChatGenerationAttempt = `-- name: IncrementChatGenerationAttempt :one +UPDATE chats +SET generation_attempt = generation_attempt + 1, updated_at = NOW() +WHERE id = $1::uuid +RETURNING generation_attempt +` + +// Increments generation_attempt and returns the resulting value. +func (q *sqlQuerier) IncrementChatGenerationAttempt(ctx context.Context, id uuid.UUID) (int64, error) { + row := q.db.QueryRowContext(ctx, incrementChatGenerationAttempt, id) + var generation_attempt int64 + err := row.Scan(&generation_attempt) + return generation_attempt, err +} + const insertChat = `-- name: InsertChat :one WITH inserted_chat AS ( INSERT INTO chats ( @@ -8790,7 +9781,7 @@ INSERT INTO chats ( $15::jsonb, $16::chat_client_type ) -RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl +RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at ), chats_expanded AS ( SELECT @@ -8822,6 +9813,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, @@ -8831,7 +9830,7 @@ chats_expanded AS ( LEFT JOIN chats root ON root.id = COALESCE(inserted_chat.root_chat_id, inserted_chat.parent_chat_id) JOIN visible_users owner ON owner.id = inserted_chat.owner_id ) -SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name +SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name FROM chats_expanded ` @@ -8903,6 +9902,14 @@ func (q *sqlQuerier) InsertChat(ctx context.Context, arg InsertChatParams) (Chat &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, @@ -8982,7 +9989,7 @@ SELECT NULLIF(UNNEST($18::bigint[]), 0), NULLIF(UNNEST($19::text[]), '') RETURNING - id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted, provider_response_id, api_key_id + id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted, provider_response_id, api_key_id, revision ` type InsertChatMessagesParams struct { @@ -9059,6 +10066,7 @@ func (q *sqlQuerier) InsertChatMessages(ctx context.Context, arg InsertChatMessa &i.Deleted, &i.ProviderResponseID, &i.APIKeyID, + &i.Revision, ); err != nil { return nil, err } @@ -9074,14 +10082,16 @@ func (q *sqlQuerier) InsertChatMessages(ctx context.Context, arg InsertChatMessa } const insertChatQueuedMessage = `-- name: InsertChatQueuedMessage :one -INSERT INTO chat_queued_messages (chat_id, content, model_config_id, api_key_id) -VALUES ( - $1, - $2, +INSERT INTO chat_queued_messages (chat_id, content, model_config_id, api_key_id, created_by) +SELECT + $1::uuid, + $2::jsonb, $3::uuid, - $4::text -) -RETURNING id, chat_id, content, created_at, model_config_id, api_key_id + $4::text, + chats.owner_id +FROM chats +WHERE chats.id = $1::uuid +RETURNING id, chat_id, content, created_at, model_config_id, api_key_id, position, created_by ` type InsertChatQueuedMessageParams struct { @@ -9091,6 +10101,9 @@ type InsertChatQueuedMessageParams struct { APIKeyID sql.NullString `db:"api_key_id" json:"api_key_id"` } +// Legacy queue insertion path. When no caller-supplied creator exists, +// preserve the created_by invariant by attributing the queued row to the +// chat owner. func (q *sqlQuerier) InsertChatQueuedMessage(ctx context.Context, arg InsertChatQueuedMessageParams) (ChatQueuedMessage, error) { row := q.db.QueryRowContext(ctx, insertChatQueuedMessage, arg.ChatID, @@ -9106,10 +10119,83 @@ func (q *sqlQuerier) InsertChatQueuedMessage(ctx context.Context, arg InsertChat &i.CreatedAt, &i.ModelConfigID, &i.APIKeyID, + &i.Position, + &i.CreatedBy, ) return i, err } +const insertChatQueuedMessageWithCreator = `-- name: InsertChatQueuedMessageWithCreator :one +INSERT INTO chat_queued_messages (chat_id, content, model_config_id, api_key_id, created_by) +VALUES ( + $1::uuid, + $2::jsonb, + $3::uuid, + $4::text, + $5::uuid +) +RETURNING id, chat_id, content, created_at, model_config_id, api_key_id, position, created_by +` + +type InsertChatQueuedMessageWithCreatorParams struct { + ChatID uuid.UUID `db:"chat_id" json:"chat_id"` + Content json.RawMessage `db:"content" json:"content"` + ModelConfigID uuid.NullUUID `db:"model_config_id" json:"model_config_id"` + APIKeyID sql.NullString `db:"api_key_id" json:"api_key_id"` + CreatedBy uuid.UUID `db:"created_by" json:"created_by"` +} + +// 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. +func (q *sqlQuerier) InsertChatQueuedMessageWithCreator(ctx context.Context, arg InsertChatQueuedMessageWithCreatorParams) (ChatQueuedMessage, error) { + row := q.db.QueryRowContext(ctx, insertChatQueuedMessageWithCreator, + arg.ChatID, + arg.Content, + arg.ModelConfigID, + arg.APIKeyID, + arg.CreatedBy, + ) + var i ChatQueuedMessage + err := row.Scan( + &i.ID, + &i.ChatID, + &i.Content, + &i.CreatedAt, + &i.ModelConfigID, + &i.APIKeyID, + &i.Position, + &i.CreatedBy, + ) + return i, err +} + +const isChatHeartbeatStale = `-- name: IsChatHeartbeatStale :one +SELECT NOT EXISTS ( + SELECT 1 FROM chat_heartbeats + WHERE chat_id = $1::uuid + AND runner_id = $2::uuid + AND heartbeat_at > NOW() - (INTERVAL '1 second' * $3::int) +) AS stale +` + +type IsChatHeartbeatStaleParams struct { + ChatID uuid.UUID `db:"chat_id" json:"chat_id"` + RunnerID uuid.UUID `db:"runner_id" json:"runner_id"` + StaleSeconds int32 `db:"stale_seconds" json:"stale_seconds"` +} + +// 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. +func (q *sqlQuerier) IsChatHeartbeatStale(ctx context.Context, arg IsChatHeartbeatStaleParams) (bool, error) { + row := q.db.QueryRowContext(ctx, isChatHeartbeatStale, arg.ChatID, arg.RunnerID, arg.StaleSeconds) + var stale bool + err := row.Scan(&stale) + return stale, err +} + const linkChatFiles = `-- name: LinkChatFiles :one WITH current AS ( SELECT COUNT(*) AS cnt @@ -9261,6 +10347,128 @@ func (q *sqlQuerier) ListChatUsageLimitOverrides(ctx context.Context) ([]ListCha return items, nil } +const lockChatAndBumpSnapshotVersion = `-- name: LockChatAndBumpSnapshotVersion :one + +WITH bumped_chat AS ( + UPDATE chats + SET snapshot_version = snapshot_version + 1 + WHERE id = ( + SELECT id FROM chats + WHERE id = $1::uuid + FOR UPDATE + ) + RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at +), +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 id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name +FROM chats_expanded +` + +// ===================================================================== +// chatd core state machine queries. +// +// These are consumed by the coderd/x/chatd/chatstate package. They +// are intentionally kept side-by-side with the legacy chatd queries +// above so the existing runtime keeps working while the state machine +// lands behind it. +// ===================================================================== +// 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. +func (q *sqlQuerier) LockChatAndBumpSnapshotVersion(ctx context.Context, id uuid.UUID) (Chat, error) { + row := q.db.QueryRowContext(ctx, lockChatAndBumpSnapshotVersion, id) + var i Chat + err := row.Scan( + &i.ID, + &i.OwnerID, + &i.WorkspaceID, + &i.Title, + &i.Status, + &i.WorkerID, + &i.StartedAt, + &i.HeartbeatAt, + &i.CreatedAt, + &i.UpdatedAt, + &i.ParentChatID, + &i.RootChatID, + &i.LastModelConfigID, + &i.Archived, + &i.LastError, + &i.Mode, + pq.Array(&i.MCPServerIDs), + &i.Labels, + &i.BuildID, + &i.AgentID, + &i.PinOrder, + &i.LastReadMessageID, + &i.LastInjectedContext, + &i.DynamicTools, + &i.OrganizationID, + &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, + &i.OwnerName, + ) + return i, err +} + const pinChatByID = `-- name: PinChatByID :exec WITH target_chat AS ( SELECT @@ -9330,7 +10538,7 @@ WHERE id = ( ORDER BY cqm.created_at ASC, cqm.id ASC LIMIT 1 ) -RETURNING id, chat_id, content, created_at, model_config_id, api_key_id +RETURNING id, chat_id, content, created_at, model_config_id, api_key_id, position, created_by ` func (q *sqlQuerier) PopNextQueuedMessage(ctx context.Context, chatID uuid.UUID) (ChatQueuedMessage, error) { @@ -9343,6 +10551,8 @@ func (q *sqlQuerier) PopNextQueuedMessage(ctx context.Context, chatID uuid.UUID) &i.CreatedAt, &i.ModelConfigID, &i.APIKeyID, + &i.Position, + &i.CreatedBy, ) return i, err } @@ -9372,6 +10582,35 @@ func (q *sqlQuerier) ReorderChatQueuedMessageToFront(ctx context.Context, arg Re return result.RowsAffected() } +const reorderChatQueuedMessageToHead = `-- name: ReorderChatQueuedMessageToHead :execrows +UPDATE chat_queued_messages AS target +SET position = COALESCE( + (SELECT MIN(position) FROM chat_queued_messages WHERE chat_id = $1::uuid), + 0 +) - 1 +WHERE target.id = $2::bigint + AND target.chat_id = $1::uuid + AND target.position > COALESCE( + (SELECT MIN(position) FROM chat_queued_messages WHERE chat_id = $1::uuid), + target.position + ) +` + +type ReorderChatQueuedMessageToHeadParams struct { + ChatID uuid.UUID `db:"chat_id" json:"chat_id"` + ID int64 `db:"id" json:"id"` +} + +// Sets the target queued message's position to one less than the +// current minimum position for that chat, moving it to the head. +func (q *sqlQuerier) ReorderChatQueuedMessageToHead(ctx context.Context, arg ReorderChatQueuedMessageToHeadParams) (int64, error) { + result, err := q.db.ExecContext(ctx, reorderChatQueuedMessageToHead, arg.ChatID, arg.ID) + if err != nil { + return 0, err + } + return result.RowsAffected() +} + const resolveUserChatSpendLimit = `-- name: ResolveUserChatSpendLimit :one SELECT CASE WHEN NOT cfg.enabled THEN -1 @@ -9481,7 +10720,7 @@ WITH updated_chats AS ( archived = false, updated_at = NOW() WHERE id = $1::uuid OR root_chat_id = $1::uuid - RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl + RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at ), chats_expanded AS ( SELECT @@ -9513,6 +10752,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, @@ -9522,7 +10769,7 @@ chats_expanded AS ( LEFT JOIN chats root ON root.id = COALESCE(updated_chats.root_chat_id, updated_chats.parent_chat_id) JOIN visible_users owner ON owner.id = updated_chats.owner_id ) -SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name +SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name FROM chats_expanded ORDER BY (chats_expanded.id = $1::uuid) DESC, chats_expanded.created_at ASC, chats_expanded.id ASC ` @@ -9569,6 +10816,14 @@ func (q *sqlQuerier) UnarchiveChatByID(ctx context.Context, id uuid.UUID) ([]Cha &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, @@ -9675,7 +10930,7 @@ UPDATE chats SET updated_at = NOW() WHERE id = $3::uuid -RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl +RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at ), chats_expanded AS ( SELECT @@ -9707,6 +10962,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, @@ -9716,7 +10979,7 @@ chats_expanded AS ( LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id) JOIN visible_users owner ON owner.id = updated_chat.owner_id ) -SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name +SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name FROM chats_expanded ` @@ -9758,6 +11021,14 @@ func (q *sqlQuerier) UpdateChatBuildAgentBinding(ctx context.Context, arg Update &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, @@ -9775,7 +11046,7 @@ SET updated_at = NOW() WHERE id = $2::uuid -RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl +RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at ), chats_expanded AS ( SELECT @@ -9807,6 +11078,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, @@ -9816,7 +11095,7 @@ chats_expanded AS ( LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id) JOIN visible_users owner ON owner.id = updated_chat.owner_id ) -SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name +SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name FROM chats_expanded ` @@ -9857,6 +11136,149 @@ func (q *sqlQuerier) UpdateChatByID(ctx context.Context, arg UpdateChatByIDParam &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, + &i.OwnerName, + ) + return i, err +} + +const updateChatExecutionState = `-- name: UpdateChatExecutionState :one +WITH updated_chat AS ( + UPDATE chats + SET + status = $1::chat_status, + archived = $2::boolean, + worker_id = $3::uuid, + runner_id = $4::uuid, + last_error = $5::jsonb, + requires_action_deadline_at = $6::timestamptz, + pin_order = CASE WHEN $2::boolean THEN 0 ELSE pin_order END, + updated_at = NOW() + WHERE id = $7::uuid + RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at +), +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 id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name +FROM chats_expanded +` + +type UpdateChatExecutionStateParams struct { + Status ChatStatus `db:"status" json:"status"` + Archived bool `db:"archived" json:"archived"` + WorkerID uuid.NullUUID `db:"worker_id" json:"worker_id"` + RunnerID uuid.NullUUID `db:"runner_id" json:"runner_id"` + LastError pqtype.NullRawMessage `db:"last_error" json:"last_error"` + RequiresActionDeadlineAt sql.NullTime `db:"requires_action_deadline_at" json:"requires_action_deadline_at"` + ID uuid.UUID `db:"id" json:"id"` +} + +// 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. +func (q *sqlQuerier) UpdateChatExecutionState(ctx context.Context, arg UpdateChatExecutionStateParams) (Chat, error) { + row := q.db.QueryRowContext(ctx, updateChatExecutionState, + arg.Status, + arg.Archived, + arg.WorkerID, + arg.RunnerID, + arg.LastError, + arg.RequiresActionDeadlineAt, + arg.ID, + ) + var i Chat + err := row.Scan( + &i.ID, + &i.OwnerID, + &i.WorkspaceID, + &i.Title, + &i.Status, + &i.WorkerID, + &i.StartedAt, + &i.HeartbeatAt, + &i.CreatedAt, + &i.UpdatedAt, + &i.ParentChatID, + &i.RootChatID, + &i.LastModelConfigID, + &i.Archived, + &i.LastError, + &i.Mode, + pq.Array(&i.MCPServerIDs), + &i.Labels, + &i.BuildID, + &i.AgentID, + &i.PinOrder, + &i.LastReadMessageID, + &i.LastInjectedContext, + &i.DynamicTools, + &i.OrganizationID, + &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, @@ -9919,7 +11341,7 @@ SET updated_at = NOW() WHERE id = $2::uuid -RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl +RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at ), chats_expanded AS ( SELECT @@ -9951,6 +11373,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, @@ -9960,7 +11390,7 @@ chats_expanded AS ( LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id) JOIN visible_users owner ON owner.id = updated_chat.owner_id ) -SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name +SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name FROM chats_expanded ` @@ -10001,6 +11431,14 @@ func (q *sqlQuerier) UpdateChatLabelsByID(ctx context.Context, arg UpdateChatLab &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, @@ -10015,7 +11453,7 @@ UPDATE chats SET last_injected_context = $1::jsonb WHERE id = $2::uuid -RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl +RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at ), chats_expanded AS ( SELECT @@ -10047,6 +11485,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, @@ -10056,7 +11502,7 @@ chats_expanded AS ( LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id) JOIN visible_users owner ON owner.id = updated_chat.owner_id ) -SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name +SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name FROM chats_expanded ` @@ -10101,6 +11547,14 @@ func (q *sqlQuerier) UpdateChatLastInjectedContext(ctx context.Context, arg Upda &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, @@ -10118,7 +11572,7 @@ SET last_model_config_id = $1::uuid WHERE id = $2::uuid -RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl +RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at ), chats_expanded AS ( SELECT @@ -10150,6 +11604,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, @@ -10159,7 +11621,7 @@ chats_expanded AS ( LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id) JOIN visible_users owner ON owner.id = updated_chat.owner_id ) -SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name +SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name FROM chats_expanded ` @@ -10200,6 +11662,14 @@ func (q *sqlQuerier) UpdateChatLastModelConfigByID(ctx context.Context, arg Upda &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, @@ -10234,26 +11704,24 @@ SET ), '') WHERE id = $2::uuid - AND updated_at = $3::timestamptz + AND history_version = $3::bigint ` type UpdateChatLastTurnSummaryParams struct { - LastTurnSummary sql.NullString `db:"last_turn_summary" json:"last_turn_summary"` - ID uuid.UUID `db:"id" json:"id"` - ExpectedUpdatedAt time.Time `db:"expected_updated_at" json:"expected_updated_at"` + LastTurnSummary sql.NullString `db:"last_turn_summary" json:"last_turn_summary"` + ID uuid.UUID `db:"id" json:"id"` + ExpectedHistoryVersion int64 `db:"expected_history_version" json:"expected_history_version"` } // 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. func (q *sqlQuerier) UpdateChatLastTurnSummary(ctx context.Context, arg UpdateChatLastTurnSummaryParams) (int64, error) { - result, err := q.db.ExecContext(ctx, updateChatLastTurnSummary, arg.LastTurnSummary, arg.ID, arg.ExpectedUpdatedAt) + result, err := q.db.ExecContext(ctx, updateChatLastTurnSummary, arg.LastTurnSummary, arg.ID, arg.ExpectedHistoryVersion) if err != nil { return 0, err } @@ -10269,7 +11737,7 @@ SET updated_at = NOW() WHERE id = $2::uuid -RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl +RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at ), chats_expanded AS ( SELECT @@ -10301,6 +11769,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, @@ -10310,7 +11786,7 @@ chats_expanded AS ( LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id) JOIN visible_users owner ON owner.id = updated_chat.owner_id ) -SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name +SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name FROM chats_expanded ` @@ -10351,6 +11827,14 @@ func (q *sqlQuerier) UpdateChatMCPServerIDs(ctx context.Context, arg UpdateChatM &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, @@ -10368,7 +11852,7 @@ SET WHERE id = $3::bigint RETURNING - id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted, provider_response_id, api_key_id + id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted, provider_response_id, api_key_id, revision ` type UpdateChatMessageByIDParams struct { @@ -10403,6 +11887,7 @@ func (q *sqlQuerier) UpdateChatMessageByID(ctx context.Context, arg UpdateChatMe &i.Deleted, &i.ProviderResponseID, &i.APIKeyID, + &i.Revision, ) return i, err } @@ -10487,7 +11972,7 @@ SET plan_mode = $1::chat_plan_mode WHERE id = $2::uuid -RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl +RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at ), chats_expanded AS ( SELECT @@ -10519,6 +12004,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, @@ -10528,7 +12021,7 @@ chats_expanded AS ( LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id) JOIN visible_users owner ON owner.id = updated_chat.owner_id ) -SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name +SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name FROM chats_expanded ` @@ -10569,6 +12062,14 @@ func (q *sqlQuerier) UpdateChatPlanModeByID(ctx context.Context, arg UpdateChatP &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, @@ -10577,20 +12078,14 @@ func (q *sqlQuerier) UpdateChatPlanModeByID(ctx context.Context, arg UpdateChatP return i, err } -const updateChatStatus = `-- name: UpdateChatStatus :one +const updateChatRetryState = `-- name: UpdateChatRetryState :one WITH updated_chat AS ( -UPDATE - chats -SET - status = $1::chat_status, - worker_id = $2::uuid, - started_at = $3::timestamptz, - heartbeat_at = $4::timestamptz, - last_error = $5::jsonb, - updated_at = NOW() -WHERE - id = $6::uuid -RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl + UPDATE chats + SET + retry_state = $1::jsonb, + updated_at = NOW() + WHERE id = $2::uuid + RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at ), chats_expanded AS ( SELECT @@ -10622,6 +12117,134 @@ 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, + 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 id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name +FROM chats_expanded +` + +type UpdateChatRetryStateParams struct { + RetryState json.RawMessage `db:"retry_state" json:"retry_state"` + ID uuid.UUID `db:"id" json:"id"` +} + +// Stores the client-visible retry payload. retry_state_version is +// assigned by trigger from the current snapshot_version. +func (q *sqlQuerier) UpdateChatRetryState(ctx context.Context, arg UpdateChatRetryStateParams) (Chat, error) { + row := q.db.QueryRowContext(ctx, updateChatRetryState, arg.RetryState, arg.ID) + var i Chat + err := row.Scan( + &i.ID, + &i.OwnerID, + &i.WorkspaceID, + &i.Title, + &i.Status, + &i.WorkerID, + &i.StartedAt, + &i.HeartbeatAt, + &i.CreatedAt, + &i.UpdatedAt, + &i.ParentChatID, + &i.RootChatID, + &i.LastModelConfigID, + &i.Archived, + &i.LastError, + &i.Mode, + pq.Array(&i.MCPServerIDs), + &i.Labels, + &i.BuildID, + &i.AgentID, + &i.PinOrder, + &i.LastReadMessageID, + &i.LastInjectedContext, + &i.DynamicTools, + &i.OrganizationID, + &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, + &i.OwnerName, + ) + return i, err +} + +const updateChatStatus = `-- name: UpdateChatStatus :one +WITH updated_chat AS ( +UPDATE + chats +SET + status = $1::chat_status, + worker_id = $2::uuid, + started_at = $3::timestamptz, + heartbeat_at = $4::timestamptz, + last_error = $5::jsonb, + updated_at = NOW() +WHERE + id = $6::uuid +RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at +), +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, @@ -10631,7 +12254,7 @@ chats_expanded AS ( LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id) JOIN visible_users owner ON owner.id = updated_chat.owner_id ) -SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name +SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name FROM chats_expanded ` @@ -10683,6 +12306,14 @@ func (q *sqlQuerier) UpdateChatStatus(ctx context.Context, arg UpdateChatStatusP &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, @@ -10704,7 +12335,7 @@ SET updated_at = $6::timestamptz WHERE id = $7::uuid -RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl +RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at ), chats_expanded AS ( SELECT @@ -10736,6 +12367,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, @@ -10745,7 +12384,7 @@ chats_expanded AS ( LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id) JOIN visible_users owner ON owner.id = updated_chat.owner_id ) -SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name +SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name FROM chats_expanded ` @@ -10799,6 +12438,14 @@ func (q *sqlQuerier) UpdateChatStatusPreserveUpdatedAt(ctx context.Context, arg &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, @@ -10818,7 +12465,7 @@ SET title = $1::text WHERE id = $2::uuid -RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl +RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at ), chats_expanded AS ( SELECT @@ -10850,6 +12497,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, @@ -10859,7 +12514,7 @@ chats_expanded AS ( LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id) JOIN visible_users owner ON owner.id = updated_chat.owner_id ) -SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name +SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name FROM chats_expanded ` @@ -10900,6 +12555,14 @@ func (q *sqlQuerier) UpdateChatTitleByID(ctx context.Context, arg UpdateChatTitl &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, @@ -10916,7 +12579,7 @@ UPDATE chats SET agent_id = $3::uuid, updated_at = NOW() WHERE id = $4::uuid -RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl +RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at ), chats_expanded AS ( SELECT @@ -10948,6 +12611,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, @@ -10957,7 +12628,7 @@ chats_expanded AS ( LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id) JOIN visible_users owner ON owner.id = updated_chat.owner_id ) -SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name +SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name FROM chats_expanded ` @@ -11005,6 +12676,14 @@ func (q *sqlQuerier) UpdateChatWorkspaceBinding(ctx context.Context, arg UpdateC &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, @@ -11232,6 +12911,44 @@ func (q *sqlQuerier) UpsertChatDiffStatusReference(ctx context.Context, arg Upse return i, err } +const upsertChatHeartbeat = `-- name: UpsertChatHeartbeat :exec +INSERT INTO chat_heartbeats (chat_id, runner_id, heartbeat_at) +VALUES ($1::uuid, $2::uuid, NOW()) +ON CONFLICT (chat_id, runner_id) DO UPDATE +SET heartbeat_at = EXCLUDED.heartbeat_at +` + +type UpsertChatHeartbeatParams struct { + ChatID uuid.UUID `db:"chat_id" json:"chat_id"` + RunnerID uuid.UUID `db:"runner_id" json:"runner_id"` +} + +// Upserts a heartbeat row for the (chat_id, runner_id) lease. Uses +// database time so callers do not depend on a local clock. +func (q *sqlQuerier) UpsertChatHeartbeat(ctx context.Context, arg UpsertChatHeartbeatParams) error { + _, err := q.db.ExecContext(ctx, upsertChatHeartbeat, arg.ChatID, arg.RunnerID) + return err +} + +const upsertChatHeartbeats = `-- name: UpsertChatHeartbeats :exec +INSERT INTO chat_heartbeats (chat_id, runner_id, heartbeat_at) +SELECT chat_ids.chat_id, runner_ids.runner_id, NOW() +FROM unnest($1::uuid[]) WITH ORDINALITY AS chat_ids(chat_id, ord) +JOIN unnest($2::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 +` + +type UpsertChatHeartbeatsParams struct { + ChatIds []uuid.UUID `db:"chat_ids" json:"chat_ids"` + RunnerIds []uuid.UUID `db:"runner_ids" json:"runner_ids"` +} + +func (q *sqlQuerier) UpsertChatHeartbeats(ctx context.Context, arg UpsertChatHeartbeatsParams) error { + _, err := q.db.ExecContext(ctx, upsertChatHeartbeats, pq.Array(arg.ChatIds), pq.Array(arg.RunnerIds)) + return err +} + const upsertChatUsageLimitConfig = `-- name: UpsertChatUsageLimitConfig :one INSERT INTO chat_usage_limit_config (singleton, enabled, default_limit_micros, period, updated_at) VALUES (TRUE, $1::boolean, $2::bigint, $3::text, NOW()) diff --git a/coderd/database/unique_constraint.go b/coderd/database/unique_constraint.go index fd11ab2e06..347435f66a 100644 --- a/coderd/database/unique_constraint.go +++ b/coderd/database/unique_constraint.go @@ -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); diff --git a/coderd/x/chatd/chatd.go b/coderd/x/chatd/chatd.go index 5a5ba7fb60..d569d993fb 100644 --- a/coderd/x/chatd/chatd.go +++ b/coderd/x/chatd/chatd.go @@ -9803,9 +9803,8 @@ func (p *Server) updateLastTurnSummary( defer cancel() affected, err := p.db.UpdateChatLastTurnSummary(updateCtx, database.UpdateChatLastTurnSummaryParams{ - ID: chat.ID, - ExpectedUpdatedAt: expectedUpdatedAt, - LastTurnSummary: lastTurnSummary, + ID: chat.ID, + LastTurnSummary: lastTurnSummary, }) if err != nil { logger.Warn(updateCtx, "failed to update chat turn summary", diff --git a/docs/admin/security/audit-logs.md b/docs/admin/security/audit-logs.md index 45022ff253..fa7c688402 100644 --- a/docs/admin/security/audit-logs.md +++ b/docs/admin/security/audit-logs.md @@ -24,7 +24,7 @@ We track the following resources: | Group
create, write, delete | |
FieldTracked
avatar_urltrue
chat_spend_limit_microstrue
display_nametrue
idtrue
memberstrue
nametrue
organization_idfalse
quota_allowancetrue
sourcefalse
| | AuditableGroupAiBudget
write, delete | |
FieldTracked
created_atfalse
group_idfalse
group_namefalse
spend_limittrue
spend_limit_microsfalse
updated_atfalse
| | AuditableOrganizationMember
| |
FieldTracked
created_attrue
organization_idfalse
rolestrue
updated_attrue
user_idtrue
usernametrue
| -| Chat
create, write | |
FieldTracked
agent_idfalse
archivedtrue
build_idfalse
client_typefalse
created_atfalse
dynamic_toolsfalse
group_acltrue
heartbeat_atfalse
idtrue
labelstrue
last_errorfalse
last_injected_contextfalse
last_model_config_idfalse
last_read_message_idfalse
last_turn_summaryfalse
mcp_server_idstrue
modetrue
organization_idfalse
owner_idtrue
owner_namefalse
owner_usernamefalse
parent_chat_idfalse
pin_ordertrue
plan_modefalse
root_chat_idfalse
started_atfalse
statusfalse
titletrue
updated_atfalse
user_acltrue
worker_idfalse
workspace_idtrue
| +| Chat
create, write | |
FieldTracked
agent_idfalse
archivedtrue
build_idfalse
client_typefalse
created_atfalse
dynamic_toolsfalse
generation_attemptfalse
group_acltrue
heartbeat_atfalse
history_versionfalse
idtrue
labelstrue
last_errorfalse
last_injected_contextfalse
last_model_config_idfalse
last_read_message_idfalse
last_turn_summaryfalse
mcp_server_idstrue
modetrue
organization_idfalse
owner_idtrue
owner_namefalse
owner_usernamefalse
parent_chat_idfalse
pin_ordertrue
plan_modefalse
queue_versionfalse
requires_action_deadline_atfalse
retry_statefalse
retry_state_versionfalse
root_chat_idfalse
runner_idfalse
snapshot_versionfalse
started_atfalse
statusfalse
titletrue
updated_atfalse
user_acltrue
worker_idfalse
workspace_idtrue
| | CustomRole
| |
FieldTracked
created_atfalse
display_nametrue
idfalse
is_systemfalse
member_permissionstrue
nametrue
org_permissionstrue
organization_idfalse
site_permissionstrue
updated_atfalse
user_permissionstrue
| | GitSSHKey
create | |
FieldTracked
created_atfalse
private_keytrue
private_key_key_idfalse
public_keytrue
updated_atfalse
user_idtrue
| | GroupSyncSettings
| |
FieldTracked
auto_create_missing_groupstrue
fieldtrue
legacy_group_name_mappingfalse
mappingtrue
regex_filtertrue
|